View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.annotations;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.URI;
25  import java.net.URL;
26  import java.net.URLClassLoader;
27  import java.util.Arrays;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Set;
31  import java.util.jar.JarEntry;
32  import java.util.jar.JarInputStream;
33  
34  import org.eclipse.jetty.util.ConcurrentHashSet;
35  import org.eclipse.jetty.util.Loader;
36  import org.eclipse.jetty.util.MultiException;
37  import org.eclipse.jetty.util.log.Log;
38  import org.eclipse.jetty.util.log.Logger;
39  import org.eclipse.jetty.util.resource.Resource;
40  import org.eclipse.jetty.webapp.JarScanner;
41  import org.objectweb.asm.AnnotationVisitor;
42  import org.objectweb.asm.ClassReader;
43  import org.objectweb.asm.ClassVisitor;
44  import org.objectweb.asm.FieldVisitor;
45  import org.objectweb.asm.MethodVisitor;
46  import org.objectweb.asm.Opcodes;
47  
48  
49  /**
50   * AnnotationParser
51   * <p>
52   * Use asm to scan classes for annotations. A SAX-style parsing is done.
53   * Handlers are registered which will be called back when various types of
54   * entity are encountered, eg a class, a method, a field. 
55   * <p> 
56   * Handlers are not called back in any particular order and are assumed
57   * to be order-independent.
58   * <p>
59   * As a registered Handler will be called back for each annotation discovered
60   * on a class, a method, a field, the Handler should test to see if the annotation
61   * is one that it is interested in.
62   * <p>
63   * For the servlet spec, we are only interested in annotations on classes, methods and fields,
64   * so the callbacks for handling finding a class, a method a field are themselves
65   * not fully implemented.
66   */
67  public class AnnotationParser
68  {
69      private static final Logger LOG = Log.getLogger(AnnotationParser.class);
70  
71      protected Set<String> _parsedClassNames = new ConcurrentHashSet<String>();
72      
73      protected static int ASM_OPCODE_VERSION = Opcodes.ASM5; //compatibility of api
74     
75  
76      /**
77       * Convert internal name to simple name
78       * 
79       * @param name the internal name
80       * @return the simple name
81       */
82      public static String normalize (String name)
83      {
84          if (name==null)
85              return null;
86  
87          if (name.startsWith("L") && name.endsWith(";"))
88              name = name.substring(1, name.length()-1);
89  
90          if (name.endsWith(".class"))
91              name = name.substring(0, name.length()-".class".length());
92  
93          return name.replace('/', '.');
94      }
95      
96      /**
97       * Convert internal names to simple names.
98       * 
99       * @param list the list of internal names
100      * @return the list of simple names
101      */
102     public static String[] normalize (String[] list)
103     {
104         if (list == null)
105             return null;       
106         String[] normalList = new String[list.length];
107         int i=0;
108         for (String s : list)
109             normalList[i++] = normalize(s);
110         return normalList;
111     }
112 
113     
114     /**
115      * ClassInfo
116      * 
117      * Immutable information gathered by parsing class header.
118      * 
119      */
120     public class ClassInfo 
121     {
122         final Resource _containingResource;
123         final String _className;
124         final int _version;
125         final int _access;
126         final String _signature;
127         final String _superName; 
128         final String[] _interfaces;
129         
130         public ClassInfo(Resource resource, String className, int version, int access, String signature, String superName, String[] interfaces)
131         {
132             super();
133             _containingResource = resource;
134             _className = className;
135             _version = version;
136             _access = access;
137             _signature = signature;
138             _superName = superName;
139             _interfaces = interfaces;
140         }
141 
142         public String getClassName()
143         {
144             return _className;
145         }
146 
147         public int getVersion()
148         {
149             return _version;
150         }
151 
152         public int getAccess()
153         {
154             return _access;
155         }
156 
157         public String getSignature()
158         {
159             return _signature;
160         }
161 
162         public String getSuperName()
163         {
164             return _superName;
165         }
166 
167         public String[] getInterfaces()
168         {
169             return _interfaces;
170         }
171 
172         public Resource getContainingResource()
173         {
174             return _containingResource;
175         }
176     }
177 
178     
179     /**
180      * MethodInfo
181      * 
182      * Immutable information gathered by parsing a method on a class.
183      */
184     public class MethodInfo
185     {
186         final ClassInfo _classInfo;
187         final String _methodName; 
188         final int _access;
189         final String _desc; 
190         final String _signature;
191         final String[] _exceptions;
192         
193         public MethodInfo(ClassInfo classInfo, String methodName, int access, String desc, String signature, String[] exceptions)
194         {
195             super();
196             _classInfo = classInfo;
197             _methodName = methodName;
198             _access = access;
199             _desc = desc;
200             _signature = signature;
201             _exceptions = exceptions;
202         }
203 
204         public ClassInfo getClassInfo()
205         {
206             return _classInfo;
207         }
208 
209         public String getMethodName()
210         {
211             return _methodName;
212         }
213 
214         public int getAccess()
215         {
216             return _access;
217         }
218 
219         public String getDesc()
220         {
221             return _desc;
222         }
223 
224         public String getSignature()
225         {
226             return _signature;
227         }
228 
229         public String[] getExceptions()
230         {
231             return _exceptions;
232         }
233     }
234     
235     
236     
237     /**
238      * FieldInfo
239      *
240      * Immutable information gathered by parsing a field on a class.
241      * 
242      */
243     public class FieldInfo
244     {
245         final ClassInfo _classInfo;
246         final String _fieldName;
247         final int _access;
248         final String _fieldType;
249         final String _signature;
250         final Object _value;
251         
252         public FieldInfo(ClassInfo classInfo, String fieldName, int access, String fieldType, String signature, Object value)
253         {
254             super();
255             _classInfo = classInfo;
256             _fieldName = fieldName;
257             _access = access;
258             _fieldType = fieldType;
259             _signature = signature;
260             _value = value;
261         }
262 
263         public ClassInfo getClassInfo()
264         {
265             return _classInfo;
266         }
267 
268         public String getFieldName()
269         {
270             return _fieldName;
271         }
272 
273         public int getAccess()
274         {
275             return _access;
276         }
277 
278         public String getFieldType()
279         {
280             return _fieldType;
281         }
282 
283         public String getSignature()
284         {
285             return _signature;
286         }
287 
288         public Object getValue()
289         {
290             return _value;
291         }
292     }
293     
294     /**
295      * Handler
296      *
297      * Signature for all handlers that respond to parsing class files.
298      */
299     public static interface Handler
300     {
301         public void handle(ClassInfo classInfo);
302         public void handle(MethodInfo methodInfo);
303         public void handle (FieldInfo fieldInfo);
304         public void handle (ClassInfo info, String annotationName);
305         public void handle (MethodInfo info, String annotationName);
306         public void handle (FieldInfo info, String annotationName);
307     }
308     
309     /**
310      * AbstractHandler
311      *
312      * Convenience base class to provide no-ops for all Handler methods.
313      */
314     public static abstract class AbstractHandler implements Handler
315     {
316 
317         @Override
318         public void handle(ClassInfo classInfo)
319         {
320            //no-op
321         }
322 
323         @Override
324         public void handle(MethodInfo methodInfo)
325         {
326             // no-op           
327         }
328 
329         @Override
330         public void handle(FieldInfo fieldInfo)
331         {
332             // no-op 
333         }
334 
335         @Override
336         public void handle(ClassInfo info, String annotationName)
337         {
338             // no-op 
339         }
340 
341         @Override
342         public void handle(MethodInfo info, String annotationName)
343         {
344             // no-op            
345         }
346 
347         @Override
348         public void handle(FieldInfo info, String annotationName)
349         {
350            // no-op
351         }        
352     }
353 
354     
355     
356     /**
357      * MyMethodVisitor
358      * 
359      * ASM Visitor for parsing a method. We are only interested in the annotations on methods.
360      */
361     public class MyMethodVisitor extends MethodVisitor
362     {
363         final MethodInfo _mi;
364         final Set<? extends Handler> _handlers;
365         
366         public MyMethodVisitor(final Set<? extends Handler> handlers,
367                                final ClassInfo classInfo,
368                                final int access,
369                                final String name,
370                                final String methodDesc,
371                                final String signature,
372                                final String[] exceptions)
373         {
374             super(ASM_OPCODE_VERSION);
375             _handlers = handlers;
376             _mi = new MethodInfo(classInfo, name, access, methodDesc,signature, exceptions);
377         }
378 
379         
380         /**
381          * We are only interested in finding the annotations on methods.
382          * 
383          * @see org.objectweb.asm.MethodVisitor#visitAnnotation(java.lang.String, boolean)
384          */
385         @Override
386         public AnnotationVisitor visitAnnotation(String desc, boolean visible)
387         {
388             String annotationName = normalize(desc);
389             for (Handler h:_handlers)
390                 h.handle(_mi, annotationName);
391             return null;
392         }
393     }
394 
395 
396     
397     /**
398      * MyFieldVisitor
399      * 
400      * An ASM visitor for parsing Fields. 
401      * We are only interested in visiting annotations on Fields.
402      *
403      */
404     public class MyFieldVisitor extends FieldVisitor
405     {   
406         final FieldInfo _fieldInfo;
407         final Set<? extends Handler> _handlers;
408         
409     
410         public MyFieldVisitor(final Set<? extends Handler> handlers,
411                               final ClassInfo classInfo,
412                               final int access,
413                               final String fieldName,
414                               final String fieldType,
415                               final String signature,
416                               final Object value)
417         {
418             super(ASM_OPCODE_VERSION);
419             _handlers = handlers;
420             _fieldInfo = new FieldInfo(classInfo, fieldName, access, fieldType, signature, value);
421         }
422 
423 
424         /**
425          * Parse an annotation found on a Field.
426          * 
427          * @see org.objectweb.asm.FieldVisitor#visitAnnotation(java.lang.String, boolean)
428          */
429         @Override
430         public AnnotationVisitor visitAnnotation(String desc, boolean visible)
431         {
432             String annotationName = normalize(desc);
433             for (Handler h : _handlers)
434                h.handle(_fieldInfo, annotationName);
435 
436             return null;
437         }
438     }
439 
440   
441 
442 
443     /**
444      * MyClassVisitor
445      *
446      * ASM visitor for a class.
447      */
448     public class MyClassVisitor extends ClassVisitor
449     {
450 
451         final Resource _containingResource;
452         final Set<? extends Handler> _handlers;
453         ClassInfo _ci;
454         
455         public MyClassVisitor(Set<? extends Handler> handlers, Resource containingResource)
456         {
457             super(ASM_OPCODE_VERSION);
458             _handlers = handlers;
459             _containingResource = containingResource;
460         }
461 
462 
463         @Override
464         public void visit (final int version,
465                            final int access,
466                            final String name,
467                            final String signature,
468                            final String superName,
469                            final String[] interfaces)
470         {           
471             _ci = new ClassInfo(_containingResource, normalize(name), version, access, signature, normalize(superName), normalize(interfaces));
472             
473             _parsedClassNames.add(_ci.getClassName());                 
474 
475             for (Handler h:_handlers)
476                h.handle(_ci);
477         }
478         
479 
480         /**
481          * Visit an annotation on a Class
482          * 
483          * @see org.objectweb.asm.ClassVisitor#visitAnnotation(java.lang.String, boolean)
484          */
485         @Override
486         public AnnotationVisitor visitAnnotation (String desc, boolean visible)
487         {
488             String annotationName = normalize(desc);
489             for (Handler h : _handlers)
490                 h.handle(_ci, annotationName);
491 
492             return null;
493         }
494 
495 
496         /**
497          * Visit a method to extract its annotations
498          * 
499          * @see org.objectweb.asm.ClassVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
500          */
501         @Override
502         public MethodVisitor visitMethod (final int access,
503                                           final String name,
504                                           final String methodDesc,
505                                           final String signature,
506                                           final String[] exceptions)
507         {
508 
509             return new MyMethodVisitor(_handlers, _ci, access, name, methodDesc, signature, exceptions);
510         }
511 
512         /**
513          * Visit a field to extract its annotations
514          * 
515          * @see org.objectweb.asm.ClassVisitor#visitField(int, java.lang.String, java.lang.String, java.lang.String, java.lang.Object)
516          */
517         @Override
518         public FieldVisitor visitField (final int access,
519                                         final String fieldName,
520                                         final String fieldType,
521                                         final String signature,
522                                         final Object value)
523         {
524             return new MyFieldVisitor(_handlers, _ci, access, fieldName, fieldType, signature, value);
525         }
526     }
527 
528  
529 
530     /**
531      * True if the class has already been processed, false otherwise
532      * @param className the classname
533      * @return true if class was parsed, false if not
534      */
535     public boolean isParsed (String className)
536     {
537         return _parsedClassNames.contains(className);
538     }
539 
540     
541     
542     /**
543      * Parse a given class
544      * 
545      * @param handlers the set of handlers to find class
546      * @param className the class name to parse
547      * @param resolver the class name resolver to use 
548      * @throws Exception if unable to parse
549      */
550     public void parse (Set<? extends Handler> handlers, String className, ClassNameResolver resolver)
551     throws Exception
552     {
553         if (className == null)
554             return;
555 
556         if (!resolver.isExcluded(className))
557         {
558             if (!isParsed(className) || resolver.shouldOverride(className))
559             {
560                 className = className.replace('.', '/')+".class";
561                 URL resource = Loader.getResource(this.getClass(), className);
562                 if (resource!= null)
563                 {
564                     Resource r = Resource.newResource(resource);
565                     try (InputStream is = r.getInputStream())
566                     {
567                         scanClass(handlers, null, is);
568                     }
569                 }
570             }
571         }
572     }
573 
574     
575     
576     /**
577      * Parse the given class, optionally walking its inheritance hierarchy
578      * 
579      * @param handlers the handlers to look for class in 
580      * @param clazz the class to look for
581      * @param resolver the resolver to look up class with
582      * @param visitSuperClasses if true, also visit super classes for parse 
583      * @throws Exception if unable to parse class
584      */
585     public void parse (Set<? extends Handler> handlers, Class<?> clazz, ClassNameResolver resolver, boolean visitSuperClasses)
586     throws Exception
587     {
588         Class<?> cz = clazz;
589         while (cz != null)
590         {
591             if (!resolver.isExcluded(cz.getName()))
592             {
593                 if (!isParsed(cz.getName()) || resolver.shouldOverride(cz.getName()))
594                 {
595                     String nameAsResource = cz.getName().replace('.', '/')+".class";
596                     URL resource = Loader.getResource(this.getClass(), nameAsResource);
597                     if (resource!= null)
598                     {
599                         Resource r = Resource.newResource(resource);
600                         try (InputStream is =  r.getInputStream())
601                         {
602                             scanClass(handlers, null, is);
603                         }
604                     }
605                 }
606             }
607 
608             if (visitSuperClasses)
609                 cz = cz.getSuperclass();
610             else
611                 cz = null;
612         }
613     }
614 
615     
616     
617     /**
618      * Parse the given classes
619      * 
620      * @param handlers the set of handlers to look for class in 
621      * @param classNames the class name
622      * @param resolver the class name resolver
623      * @throws Exception if unable to parse
624      */
625     public void parse (Set<? extends Handler> handlers, String[] classNames, ClassNameResolver resolver)
626     throws Exception
627     {
628         if (classNames == null)
629             return;
630 
631         parse(handlers, Arrays.asList(classNames), resolver);
632     }
633 
634     
635     /**
636      * Parse the given classes
637      * 
638      * @param handlers the set of handlers to look for class in 
639      * @param classNames the class names
640      * @param resolver the class name resolver
641      * @throws Exception if unable to parse
642      */
643     public void parse (Set<? extends Handler> handlers, List<String> classNames, ClassNameResolver resolver)
644     throws Exception
645     {
646         MultiException me = new MultiException();
647         
648         for (String s:classNames)
649         {
650             try
651             {
652                 if ((resolver == null) || (!resolver.isExcluded(s) &&  (!isParsed(s) || resolver.shouldOverride(s))))
653                 {
654                     s = s.replace('.', '/')+".class";
655                     URL resource = Loader.getResource(this.getClass(), s);
656                     if (resource!= null)
657                     {
658                         Resource r = Resource.newResource(resource);
659                         try (InputStream is = r.getInputStream())
660                         {
661                             scanClass(handlers, null, is);
662                         }
663                     }
664                 }
665             }
666             catch (Exception e)
667             {
668                 me.add(new RuntimeException("Error scanning class "+s, e));
669             }
670         }
671         me.ifExceptionThrow();
672     }
673 
674     
675     /**
676      * Parse all classes in a directory
677      * 
678      * @param handlers the set of handlers to look for classes in 
679      * @param dir the resource directory to look for classes
680      * @param resolver the class name resolver
681      * @throws Exception if unable to parse
682      */
683     protected void parseDir (Set<? extends Handler> handlers, Resource dir, ClassNameResolver resolver)
684     throws Exception
685     {
686         // skip dirs whose name start with . (ie hidden)
687         if (!dir.isDirectory() || !dir.exists() || dir.getName().startsWith("."))
688             return;
689 
690         if (LOG.isDebugEnabled()) {LOG.debug("Scanning dir {}", dir);};
691 
692         MultiException me = new MultiException();
693         
694         String[] files=dir.list();
695         for (int f=0;files!=null && f<files.length;f++)
696         {
697             Resource res = dir.addPath(files[f]);
698             if (res.isDirectory())
699                 parseDir(handlers, res, resolver);
700             else
701             {
702                 //we've already verified the directories, so just verify the class file name
703                 File file = res.getFile();
704                 if (isValidClassFileName((file==null?null:file.getName())))
705                 {
706                     try
707                     {
708                         String name = res.getName();
709                         if ((resolver == null)|| (!resolver.isExcluded(name) && (!isParsed(name) || resolver.shouldOverride(name))))
710                         {
711                             Resource r = Resource.newResource(res.getURL());
712                             if (LOG.isDebugEnabled()) {LOG.debug("Scanning class {}", r);};
713                             try (InputStream is=r.getInputStream())
714                             {
715                                 scanClass(handlers, dir, is);
716                             }
717                         }
718                     }                  
719                     catch (Exception ex)
720                     {
721                         if (LOG.isDebugEnabled()) LOG.debug("Error scanning file "+files[f], ex);
722                         me.add(new RuntimeException("Error scanning file "+files[f],ex));
723                     }
724                 }
725                 else
726                 {
727                    if (LOG.isDebugEnabled()) LOG.debug("Skipping scan on invalid file {}", res);
728                 }
729             }
730         }
731 
732         me.ifExceptionThrow();
733     }
734 
735 
736     /**
737      * Parse classes in the supplied classloader. 
738      * Only class files in jar files will be scanned.
739      * 
740      * @param handlers the handlers to look for classes in 
741      * @param loader the classloader for the classes
742      * @param visitParents if true, visit parent classloaders too
743      * @param nullInclusive if true, an empty pattern means all names match, if false, none match
744      * @param resolver the class name resolver
745      * @throws Exception if unable to parse
746      */
747     public void parse (final Set<? extends Handler> handlers, ClassLoader loader, boolean visitParents, boolean nullInclusive, final ClassNameResolver resolver)
748     throws Exception
749     {
750         if (loader==null)
751             return;
752 
753         if (!(loader instanceof URLClassLoader))
754             return; //can't extract classes?
755 
756         final MultiException me = new MultiException();
757         
758         JarScanner scanner = new JarScanner()
759         {
760             @Override
761             public void processEntry(URI jarUri, JarEntry entry)
762             {
763                 try
764                 {
765                     parseJarEntry(handlers, Resource.newResource(jarUri), entry, resolver);
766                 }
767                 catch (Exception e)
768                 {
769                     me.add(new RuntimeException("Error parsing entry "+entry.getName()+" from jar "+ jarUri, e));
770                 }
771             }
772 
773         };
774 
775         scanner.scan(null, loader, nullInclusive, visitParents);
776         me.ifExceptionThrow();
777     }
778 
779 
780     /**
781      * Parse classes in the supplied uris.
782      * 
783      * @param handlers the handlers to look for classes in  
784      * @param uris the uris for the jars
785      * @param resolver the class name resolver
786      * @throws Exception if unable to parse
787      */
788     public void parse (final Set<? extends Handler> handlers, final URI[] uris, final ClassNameResolver resolver)
789     throws Exception
790     {
791         if (uris==null)
792             return;
793 
794         MultiException me = new MultiException();
795         
796         for (URI uri:uris)
797         {
798             try
799             {
800                 parse(handlers, uri, resolver);
801             }
802             catch (Exception e)
803             {
804                 me.add(new RuntimeException("Problem parsing classes from "+ uri, e));
805             }
806         }
807         me.ifExceptionThrow();
808     }
809 
810     /**
811      * Parse a particular uri
812      * 
813      * @param handlers the handlers to look for classes in 
814      * @param uri the uri for the jar 
815      * @param resolver the class name resolver
816      * @throws Exception if unable to parse
817      */
818     public void parse (final Set<? extends Handler> handlers, URI uri, final ClassNameResolver resolver)
819     throws Exception
820     {
821         if (uri == null)
822             return;
823 
824         parse (handlers, Resource.newResource(uri), resolver);
825     }
826 
827     
828     /**
829      * Parse a resource
830      * 
831      * @param handlers the handlers to look for classes in  
832      * @param r the resource to parse
833      * @param resolver the class name resolver
834      * @throws Exception if unable to parse
835      */
836     public void parse (final Set<? extends Handler> handlers, Resource r, final ClassNameResolver resolver)
837     throws Exception
838     {
839         if (r == null)
840             return;
841         
842         if (r.exists() && r.isDirectory())
843         {
844             parseDir(handlers, r, resolver);
845             return;
846         }
847 
848         String fullname = r.toString();
849         if (fullname.endsWith(".jar"))
850         {
851             parseJar(handlers, r, resolver);
852             return;
853         }
854 
855         if (fullname.endsWith(".class"))
856         {
857             try (InputStream is=r.getInputStream())
858             {
859                 scanClass(handlers, null, is);
860                 return;
861             }
862         }
863         
864         if (LOG.isDebugEnabled()) LOG.warn("Resource not scannable for classes: {}", r);
865     }
866 
867     
868   
869 
870     /**
871      * Parse a resource that is a jar file.
872      * 
873      * @param handlers the handlers to look for classes in  
874      * @param jarResource the jar resource to parse
875      * @param resolver the class name resolver
876      * @throws Exception if unable to parse
877      */
878     protected void parseJar (Set<? extends Handler> handlers, Resource jarResource,  final ClassNameResolver resolver)
879     throws Exception
880     {
881         if (jarResource == null)
882             return;
883        
884         if (jarResource.toString().endsWith(".jar"))
885         {
886             if (LOG.isDebugEnabled()) {LOG.debug("Scanning jar {}", jarResource);};
887 
888             //treat it as a jar that we need to open and scan all entries from  
889             //TODO alternative impl
890             /*
891             long start = System.nanoTime();
892             Collection<Resource> resources = Resource.newResource("jar:"+jarResource+"!/").getAllResources();
893             System.err.println(jarResource+String.valueOf(resources.size())+" resources listed in "+ ((TimeUnit.MILLISECONDS.convert(System.nanoTime()-start, TimeUnit.NANOSECONDS))));
894             for (Resource r:resources)
895             {
896                 //skip directories
897                 if (r.isDirectory())
898                     continue;
899 
900                 String name = r.getName();
901                 name = name.substring(name.indexOf("!/")+2);
902 
903                 //check file is a valid class file name
904                 if (isValidClassFileName(name) && isValidClassFilePath(name))
905                 {
906                     String shortName =  name.replace('/', '.').substring(0,name.length()-6);
907 
908                     if ((resolver == null)
909                             ||
910                         (!resolver.isExcluded(shortName) && (!isParsed(shortName) || resolver.shouldOverride(shortName))))
911                     {
912                         if (LOG.isDebugEnabled()) {LOG.debug("Scanning class from jar {}", r);};
913                         scanClass(handlers, jarResource, r.getInputStream());
914                     }
915                 }
916             }
917             */
918 
919            InputStream in = jarResource.getInputStream();
920             if (in==null)
921                 return;
922 
923             MultiException me = new MultiException();
924             
925             JarInputStream jar_in = new JarInputStream(in);
926             try
927             { 
928                 JarEntry entry = jar_in.getNextJarEntry();
929                 while (entry!=null)
930                 {      
931                     try
932                     {
933                         parseJarEntry(handlers, jarResource, entry, resolver);                        
934                     }
935                     catch (Exception e)
936                     {
937                         me.add(new RuntimeException("Error scanning entry "+entry.getName()+" from jar "+jarResource, e));
938                     }
939                     entry = jar_in.getNextJarEntry();
940                 }
941             }
942             finally
943             {
944                 jar_in.close();
945             }
946             me.ifExceptionThrow();
947         }        
948     }
949 
950     /**
951      * Parse a single entry in a jar file
952      * 
953      * @param handlers the handlers to look for classes in  
954      * @param jar the jar resource to parse
955      * @param entry the entry in the jar resource to parse
956      * @param resolver the class name resolver
957      * @throws Exception if unable to parse
958      */
959     protected void parseJarEntry (Set<? extends Handler> handlers, Resource jar, JarEntry entry, final ClassNameResolver resolver)
960     throws Exception
961     {
962         if (jar == null || entry == null)
963             return;
964 
965         //skip directories
966         if (entry.isDirectory())
967             return;
968 
969         String name = entry.getName();
970 
971         //check file is a valid class file name
972         if (isValidClassFileName(name) && isValidClassFilePath(name))
973         {
974             String shortName =  name.replace('/', '.').substring(0,name.length()-6);
975 
976             if ((resolver == null)
977                     ||
978                (!resolver.isExcluded(shortName) && (!isParsed(shortName) || resolver.shouldOverride(shortName))))
979             {
980                 Resource clazz = Resource.newResource("jar:"+jar.getURI()+"!/"+name);
981                 if (LOG.isDebugEnabled()) {LOG.debug("Scanning class from jar {}", clazz);};
982                 try (InputStream is = clazz.getInputStream())
983                 {
984                     scanClass(handlers, jar, is);
985                 }
986             }
987         }
988     }
989     
990     
991 
992     /**
993      * Use ASM on a class
994      * 
995      * @param handlers the handlers to look for classes in  
996      * @param containingResource the dir or jar that the class is contained within, can be null if not known
997      * @param is the input stream to parse
998      * @throws IOException if unable to parse
999      */
1000     protected void scanClass (Set<? extends Handler> handlers, Resource containingResource, InputStream is)
1001     throws IOException
1002     {
1003         ClassReader reader = new ClassReader(is);
1004         reader.accept(new MyClassVisitor(handlers, containingResource), ClassReader.SKIP_CODE|ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
1005     }
1006     
1007     /**
1008      * Check that the given path represents a valid class file name.
1009      * The check is fairly cursory, checking that:
1010      * <ul>
1011      * <li> the name ends with .class</li>
1012      * <li> it isn't a dot file or in a hidden directory </li>
1013      * <li> the name of the class at least begins with a valid identifier for a class name </li>
1014      * </ul>
1015      * @param name
1016      * @return
1017      */
1018     private boolean isValidClassFileName (String name)
1019     {
1020         //no name cannot be valid
1021         if (name == null || name.length()==0)
1022             return false;
1023 
1024         //skip anything that is not a class file
1025         if (!name.toLowerCase(Locale.ENGLISH).endsWith(".class"))
1026         {
1027             if (LOG.isDebugEnabled()) LOG.debug("Not a class: {}",name);
1028             return false;
1029         }
1030 
1031         //skip any classfiles that are not a valid java identifier
1032         int c0 = 0;      
1033         int ldir = name.lastIndexOf('/', name.length()-6);
1034         c0 = (ldir > -1 ? ldir+1 : c0);
1035         if (!Character.isJavaIdentifierStart(name.charAt(c0)))
1036         {
1037             if (LOG.isDebugEnabled()) LOG.debug("Not a java identifier: {}"+name);
1038             return false;
1039         }
1040    
1041         return true;
1042     }
1043     
1044     
1045     /**
1046      * Check that the given path does not contain hidden directories
1047      *
1048      * @param path
1049      * @return
1050      */
1051     private boolean isValidClassFilePath (String path)
1052     {
1053         //no path is not valid
1054         if (path == null || path.length()==0)
1055             return false;
1056 
1057         //skip any classfiles that are in a hidden directory
1058         if (path.startsWith(".") || path.contains("/."))
1059         {
1060             if (LOG.isDebugEnabled()) LOG.debug("Contains hidden dirs: {}"+path);
1061             return false;
1062         }
1063 
1064         return true;
1065     }
1066 }
1067