From 22f95ad847efc7d6db4913cacb021fdf4e1b2595 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Tue, 17 Oct 2017 17:01:21 +1100 Subject: [PATCH] Issue #1901 Backport some java9 support to 9.3 --- .../jetty/annotations/AnnotationParser.java | 406 ++++++++---------- .../annotations/TestAnnotationParser.java | 4 +- 2 files changed, 182 insertions(+), 228 deletions(-) diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java index 6c8804dbb78..f8752d9866a 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java @@ -23,23 +23,23 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URL; -import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarInputStream; +import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.jetty.util.ConcurrentHashSet; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.MultiException; import org.eclipse.jetty.util.MultiReleaseJarFile; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.webapp.JarScanner; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -47,7 +47,6 @@ import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; - /** * AnnotationParser *

@@ -70,30 +69,9 @@ public class AnnotationParser { private static final Logger LOG = Log.getLogger(AnnotationParser.class); - private static final int JVM_MAJOR_VER; - - protected Set _parsedClassNames = new ConcurrentHashSet(); - protected static int ASM_OPCODE_VERSION = Opcodes.ASM5; //compatibility of api - - static - { - // Determine JVM spec version - // Using guidance from http://openjdk.java.net/jeps/223 - String jvmSpecVer = System.getProperty("java.vm.specification.version"); - - if (jvmSpecVer.indexOf('.') >= 0) - { - // Old spec version (Java 1.8 and older) - String parts[] = jvmSpecVer.split("\\."); - JVM_MAJOR_VER = Integer.parseInt(parts[1]); - } - else - { - // Newer spec version (Java 9+) - JVM_MAJOR_VER = Integer.parseInt(jvmSpecVer); - } - } + + protected Map> _parsedClassNames = new ConcurrentHashMap<>(); /** * Convert internal name to simple name @@ -132,12 +110,8 @@ public class AnnotationParser return normalList; } - /** - * ClassInfo - * * Immutable information gathered by parsing class header. - * */ public class ClassInfo { @@ -197,10 +171,7 @@ public class AnnotationParser } } - /** - * MethodInfo - * * Immutable information gathered by parsing a method on a class. */ public class MethodInfo @@ -253,14 +224,9 @@ public class AnnotationParser return _exceptions; } } - - - + /** - * FieldInfo - * * Immutable information gathered by parsing a field on a class. - * */ public class FieldInfo { @@ -314,8 +280,6 @@ public class AnnotationParser } /** - * Handler - * * Signature for all handlers that respond to parsing class files. */ public static interface Handler @@ -329,13 +293,10 @@ public class AnnotationParser } /** - * AbstractHandler - * * Convenience base class to provide no-ops for all Handler methods. */ public static abstract class AbstractHandler implements Handler { - @Override public void handle(ClassInfo classInfo) { @@ -369,15 +330,11 @@ public class AnnotationParser @Override public void handle(FieldInfo info, String annotationName) { - // no-op - } + // no-op + } } - - /** - * MyMethodVisitor - * * ASM Visitor for parsing a method. We are only interested in the annotations on methods. */ public class MyMethodVisitor extends MethodVisitor @@ -398,11 +355,8 @@ public class AnnotationParser _mi = new MethodInfo(classInfo, name, access, methodDesc,signature, exceptions); } - /** * We are only interested in finding the annotations on methods. - * - * @see org.objectweb.asm.MethodVisitor#visitAnnotation(java.lang.String, boolean) */ @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) @@ -414,21 +368,15 @@ public class AnnotationParser } } - - /** - * MyFieldVisitor - * - * An ASM visitor for parsing Fields. + * An ASM visitor for parsing Fields. * We are only interested in visiting annotations on Fields. - * */ public class MyFieldVisitor extends FieldVisitor { final FieldInfo _fieldInfo; final Set _handlers; - - + public MyFieldVisitor(final Set handlers, final ClassInfo classInfo, final int access, @@ -442,11 +390,8 @@ public class AnnotationParser _fieldInfo = new FieldInfo(classInfo, fieldName, access, fieldType, signature, value); } - /** * Parse an annotation found on a Field. - * - * @see org.objectweb.asm.FieldVisitor#visitAnnotation(java.lang.String, boolean) */ @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) @@ -459,17 +404,11 @@ public class AnnotationParser } } - - - /** - * MyClassVisitor - * * ASM visitor for a class. */ public class MyClassVisitor extends ClassVisitor { - final Resource _containingResource; final Set _handlers; ClassInfo _ci; @@ -481,7 +420,6 @@ public class AnnotationParser _containingResource = containingResource; } - @Override public void visit (final int version, final int access, @@ -491,18 +429,12 @@ public class AnnotationParser final String[] interfaces) { _ci = new ClassInfo(_containingResource, normalize(name), version, access, signature, normalize(superName), normalize(interfaces)); - - _parsedClassNames.add(_ci.getClassName()); - for (Handler h:_handlers) h.handle(_ci); } - /** * Visit an annotation on a Class - * - * @see org.objectweb.asm.ClassVisitor#visitAnnotation(java.lang.String, boolean) */ @Override public AnnotationVisitor visitAnnotation (String desc, boolean visible) @@ -510,15 +442,11 @@ public class AnnotationParser String annotationName = normalize(desc); for (Handler h : _handlers) h.handle(_ci, annotationName); - return null; } - /** * Visit a method to extract its annotations - * - * @see org.objectweb.asm.ClassVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) */ @Override public MethodVisitor visitMethod (final int access, @@ -527,14 +455,11 @@ public class AnnotationParser final String signature, final String[] exceptions) { - return new MyMethodVisitor(_handlers, _ci, access, name, methodDesc, signature, exceptions); } /** * Visit a field to extract its annotations - * - * @see org.objectweb.asm.ClassVisitor#visitField(int, java.lang.String, java.lang.String, java.lang.String, java.lang.Object) */ @Override public FieldVisitor visitField (final int access, @@ -547,7 +472,25 @@ public class AnnotationParser } } - + /** + * Add a class as having been parsed. + * + * @param classname the name of the class + * @param location the fully qualified location of the class + */ + public void addParsedClass (String classname, Resource location) + { + List list = new ArrayList<>(1); + if (location != null) + list.add(location.toString()); + + List existing = _parsedClassNames.putIfAbsent(classname, list); + if (existing != null) + { + existing.addAll(list); + LOG.warn("{} scanned from multiple locations: {}", classname, existing); + } + } /** * True if the class has already been processed, false otherwise @@ -556,11 +499,24 @@ public class AnnotationParser */ public boolean isParsed (String className) { - return _parsedClassNames.contains(className); + return _parsedClassNames.containsKey(className); + } + + /** + * Get the locations of the given classname. There may be more than one + * location if there are duplicates of the same class. + * + * @param classname the name of the class + * @return an immutable list of locations + */ + public List getParsedLocations (String classname) + { + List list = _parsedClassNames.get(classname); + if (list == null) + return Collections.emptyList(); + return Collections.unmodifiableList(list); } - - /** * Parse a given class * @@ -577,24 +533,24 @@ public class AnnotationParser if (!resolver.isExcluded(className)) { - if (!isParsed(className) || resolver.shouldOverride(className)) - { - className = className.replace('.', '/')+".class"; - URL resource = Loader.getResource(this.getClass(), className); - if (resource!= null) - { - Resource r = Resource.newResource(resource); - try (InputStream is = r.getInputStream()) - { - scanClass(handlers, null, is); - } - } - } + if (!isParsed(className) || resolver.shouldOverride(className)) + { + String tmp = className; + className = className.replace('.', '/')+".class"; + URL resource = Loader.getResource(this.getClass(),className); + if (resource!= null) + { + Resource r = Resource.newResource(resource); + addParsedClass(tmp, r); + try (InputStream is = r.getInputStream()) + { + scanClass(handlers, null, is); + } + } + } } } - - /** * Parse the given class, optionally walking its inheritance hierarchy * @@ -608,34 +564,33 @@ public class AnnotationParser throws Exception { Class cz = clazz; - while (cz != null) + while (cz != Object.class) { - if (!resolver.isExcluded(cz.getName())) - { - if (!isParsed(cz.getName()) || resolver.shouldOverride(cz.getName())) - { - String nameAsResource = cz.getName().replace('.', '/')+".class"; - URL resource = Loader.getResource(this.getClass(), nameAsResource); - if (resource!= null) - { - Resource r = Resource.newResource(resource); - try (InputStream is = r.getInputStream()) - { - scanClass(handlers, null, is); - } - } - } - } + if (!resolver.isExcluded(cz.getName())) + { + if (!isParsed(cz.getName()) || resolver.shouldOverride(cz.getName())) + { + String nameAsResource = cz.getName().replace('.', '/')+".class"; + URL resource = Loader.getResource(this.getClass(),nameAsResource); + if (resource!= null) + { + Resource r = Resource.newResource(resource); + addParsedClass(clazz.getName(), r); + try (InputStream is = r.getInputStream()) + { + scanClass(handlers, null, is); + } + } + } + } if (visitSuperClasses) cz = cz.getSuperclass(); else - cz = null; + break; } } - - /** * Parse the given classes * @@ -653,7 +608,6 @@ public class AnnotationParser parse(handlers, Arrays.asList(classNames), resolver); } - /** * Parse the given classes * @@ -666,24 +620,26 @@ public class AnnotationParser throws Exception { MultiException me = new MultiException(); - + for (String s:classNames) { try { - if ((resolver == null) || (!resolver.isExcluded(s) && (!isParsed(s) || resolver.shouldOverride(s)))) - { - s = s.replace('.', '/')+".class"; - URL resource = Loader.getResource(this.getClass(), s); - if (resource!= null) - { - Resource r = Resource.newResource(resource); - try (InputStream is = r.getInputStream()) - { - scanClass(handlers, null, is); - } - } - } + if ((resolver == null) || (!resolver.isExcluded(s) && (!isParsed(s) || resolver.shouldOverride(s)))) + { + String name = s; + s = s.replace('.', '/')+".class"; + URL resource = Loader.getResource(this.getClass(),s); + if (resource!= null) + { + Resource r = Resource.newResource(resource); + addParsedClass(name, r); + try (InputStream is = r.getInputStream()) + { + scanClass(handlers, null, is); + } + } + } } catch (Exception e) { @@ -693,7 +649,6 @@ public class AnnotationParser me.ifExceptionThrow(); } - /** * Parse all classes in a directory * @@ -702,59 +657,62 @@ public class AnnotationParser * @param resolver the class name resolver * @throws Exception if unable to parse */ - protected void parseDir (Set handlers, Resource dir, ClassNameResolver resolver) - throws Exception + protected void parseDir (Set handlers, Resource root, ClassNameResolver resolver) throws Exception { - // skip dirs whose name start with . (ie hidden) - if (!dir.isDirectory() || !dir.exists() || dir.getName().startsWith(".")) + if (!root.isDirectory() || !root.exists() || root.getName().startsWith(".")) return; - if (LOG.isDebugEnabled()) {LOG.debug("Scanning dir {}", dir);}; - - MultiException me = new MultiException(); + if (LOG.isDebugEnabled()) + LOG.debug("Scanning dir {}", root); - String[] files=dir.list(); - for (int f=0;files!=null && f resources = root.getAllResources(); + if (resources != null) { - Resource res = dir.addPath(files[f]); - if (res.isDirectory()) - parseDir(handlers, res, resolver); - else + for (Resource r:resources) { - //we've already verified the directories, so just verify the class file name - File file = res.getFile(); - if (isValidClassFileName((file==null?null:file.getName()))) + if (r.isDirectory()) + continue; + + String name = r.getName(); + if ((resolver == null)|| (!resolver.isExcluded(name) && (!isParsed(name) || resolver.shouldOverride(name)))) { - try - { - String name = res.getName(); - if ((resolver == null)|| (!resolver.isExcluded(name) && (!isParsed(name) || resolver.shouldOverride(name)))) - { - Resource r = Resource.newResource(res.getURL()); - if (LOG.isDebugEnabled()) {LOG.debug("Scanning class {}", r);}; - try (InputStream is=r.getInputStream()) - { - scanClass(handlers, dir, is); - } - } - } - catch (Exception ex) - { - if (LOG.isDebugEnabled()) LOG.debug("Error scanning file "+files[f], ex); - me.add(new RuntimeException("Error scanning file "+files[f],ex)); - } - } - else - { - if (LOG.isDebugEnabled()) LOG.debug("Skipping scan on invalid file {}", res); + File file = r.getFile(); + if (isValidClassFileName((file==null?null:file.getName()))) + { + Path classpath = rootFile.toPath().relativize(file.toPath()); + String str = classpath.toString(); + str = str.substring(0, str.lastIndexOf(".class")).replace('/', '.').replace('\\', '.'); + + try + { + if (LOG.isDebugEnabled()) + LOG.debug("Scanning class {}", r); + addParsedClass(str, r); + try (InputStream is=r.getInputStream()) + { + scanClass(handlers, Resource.newResource(file.getParentFile()), is); + } + } + catch (Exception ex) + { + if (LOG.isDebugEnabled()) LOG.debug("Error scanning file "+file, ex); + me.add(new RuntimeException("Error scanning file "+file,ex)); + } + } + else + { + if (LOG.isDebugEnabled()) LOG.debug("Skipping scan on invalid file {}", file); + } } } } - + me.ifExceptionThrow(); } - /** * Parse classes in the supplied classloader. * Only class files in jar files will be scanned. @@ -763,7 +721,6 @@ public class AnnotationParser * @param loader the classloader for the classes * @param visitParents if true, visit parent classloaders too * @param nullInclusive if true, an empty pattern means all names match, if false, none match - * @param resolver the class name resolver * @throws Exception if unable to parse */ @Deprecated @@ -772,7 +729,6 @@ public class AnnotationParser throw new UnsupportedOperationException(); } - /** * Parse classes in the supplied uris. * @@ -820,7 +776,6 @@ public class AnnotationParser parse (handlers, Resource.newResource(uri), resolver); } - /** * Parse a resource * @@ -860,9 +815,6 @@ public class AnnotationParser if (LOG.isDebugEnabled()) LOG.warn("Resource not scannable for classes: {}", r); } - - - /** * Parse a resource that is a jar file. * @@ -871,7 +823,7 @@ public class AnnotationParser * @param resolver the class name resolver * @throws Exception if unable to parse */ - protected void parseJar (Set handlers, Resource jarResource, final ClassNameResolver resolver) + protected void parseJar (Set handlers, Resource jarResource, final ClassNameResolver resolver) throws Exception { if (jarResource == null) @@ -879,15 +831,19 @@ public class AnnotationParser if (jarResource.toString().endsWith(".jar")) { - if (LOG.isDebugEnabled()) {LOG.debug("Scanning jar {}", jarResource);}; + if (LOG.isDebugEnabled()) + LOG.debug("Scanning jar {}", jarResource); MultiException me = new MultiException(); - JarFile jarFile = MultiReleaseJarFile.open(jarResource.getFile()); - MultiReleaseJarFile.stream(jarFile).forEach(e-> + + // TODO do not force version 8 once ASM can scan 9 + // TODO support a different target for quickstart generation + MultiReleaseJarFile jarFile = new MultiReleaseJarFile(jarResource.getFile(),8,false); + jarFile.stream().forEach(e-> { try { - parseJarEntry(handlers, jarResource, jarFile, e,resolver); + parseJarEntry(handlers, jarResource, e, resolver); } catch (Exception ex) { @@ -895,10 +851,6 @@ public class AnnotationParser } }); - finally - { - jar_in.close(); - } me.ifExceptionThrow(); } @@ -908,14 +860,13 @@ public class AnnotationParser * Parse a single entry in a jar file * * @param handlers the handlers to look for classes in - * @param jarFile the jar resource being parses - * @param entry the entry in the jar resource to parse + * @param entry the entry in the potentially MultiRelease jar resource to parse * @param resolver the class name resolver * @throws Exception if unable to parse */ - protected void parseJarEntry (Set handlers, Resource jar, JarFile jarFile, JarEntry entry,final ClassNameResolver resolver) throws Exception + protected void parseJarEntry (Set handlers, Resource jar, MultiReleaseJarFile.VersionedJarEntry entry,final ClassNameResolver resolver) throws Exception { - if (jarFile == null || entry == null) + if (jar == null || entry == null) return; //skip directories @@ -927,23 +878,21 @@ public class AnnotationParser //check file is a valid class file name if (isValidClassFileName(name) && isValidClassFilePath(name)) { - String shortName = name.replace('/', '.').substring(0,name.length()-6); - - if ((resolver == null) - || - (!resolver.isExcluded(shortName) && (!isParsed(shortName) || resolver.shouldOverride(shortName)))) - { - Resource clazz = Resource.newResource("jar:"+jar.getURI()+"!/"+name); - if (LOG.isDebugEnabled()) {LOG.debug("Scanning class from jar {}", clazz);}; - try (InputStream is = jarFile.getInputStream(entry)) - { - scanClass(handlers, jar, is); - } - } + String shortName = name.replace('/', '.').substring(0,name.length()-6); + if ((resolver == null) + || + (!resolver.isExcluded(shortName) && (!isParsed(shortName) || resolver.shouldOverride(shortName)))) + { + addParsedClass(shortName, Resource.newResource("jar:"+jar.getURI()+"!/"+entry.getNameInJar())); + if (LOG.isDebugEnabled()) + LOG.debug("Scanning class from jar {}!/{}", jar, entry); + try (InputStream is = entry.getInputStream()) + { + scanClass(handlers, jar, is); + } + } } } - - /** * Use ASM on a class @@ -953,13 +902,20 @@ public class AnnotationParser * @param is the input stream to parse * @throws IOException if unable to parse */ - protected void scanClass (Set handlers, Resource containingResource, InputStream is) - throws IOException + protected void scanClass (Set handlers, Resource containingResource, InputStream is) throws IOException { ClassReader reader = new ClassReader(is); reader.accept(new MyClassVisitor(handlers, containingResource), ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES); } + /** + * Remove any parsed class names. + */ + public void resetParsedClasses () + { + _parsedClassNames.clear(); + } + /** * Check that the given path represents a valid class file name. * The check is fairly cursory, checking that: @@ -968,8 +924,8 @@ public class AnnotationParser *

  • it isn't a dot file or in a hidden directory
  • *
  • the name of the class at least begins with a valid identifier for a class name
  • * - * @param name - * @return + * @param name the class file name + * @return whether the class file name is valid */ private boolean isValidClassFileName (String name) { @@ -1003,13 +959,12 @@ public class AnnotationParser return true; } - - + /** * Check that the given path does not contain hidden directories * - * @param path - * @return + * @param path the class file path + * @return whether the class file path is valid */ private boolean isValidClassFilePath (String path) { @@ -1027,4 +982,3 @@ public class AnnotationParser return true; } } - diff --git a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java index ccad1dfa405..89fa48d22e8 100644 --- a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java +++ b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java @@ -181,7 +181,7 @@ public class TestAnnotationParser File badClassesJar = MavenTestingUtils.getTestResourceFile("jdk9/slf4j-api-1.8.0-alpha2.jar"); AnnotationParser parser = new AnnotationParser(); Set emptySet = Collections.emptySet(); - parser.parse(emptySet, badClassesJar.toURI()); + parser.parse(emptySet, badClassesJar.toURI(),null); // Should throw no exceptions, and happily skip the module-info.class files } @@ -191,7 +191,7 @@ public class TestAnnotationParser File badClassesJar = MavenTestingUtils.getTestResourceFile("jdk9/log4j-api-2.9.0.jar"); AnnotationParser parser = new AnnotationParser(); Set emptySet = Collections.emptySet(); - parser.parse(emptySet, badClassesJar.toURI()); + parser.parse(emptySet, badClassesJar.toURI(),null); // Should throw no exceptions, and skip the META-INF/versions/9/* files }