View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.config.annotation;
20  
21  import java.io.DataInputStream;
22  import java.io.IOException;
23  import java.lang.annotation.Annotation;
24  import java.net.JarURLConnection;
25  import java.net.URL;
26  import java.net.URLConnection;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.Enumeration;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.jar.JarEntry;
37  import java.util.jar.JarFile;
38  import java.util.logging.Level;
39  import java.util.logging.Logger;
40  
41  import javax.faces.FacesException;
42  import javax.faces.bean.ManagedBean;
43  import javax.faces.component.FacesComponent;
44  import javax.faces.component.behavior.FacesBehavior;
45  import javax.faces.context.ExternalContext;
46  import javax.faces.convert.FacesConverter;
47  import javax.faces.event.NamedEvent;
48  import javax.faces.render.FacesBehaviorRenderer;
49  import javax.faces.render.FacesRenderer;
50  import javax.faces.validator.FacesValidator;
51  
52  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
53  import org.apache.myfaces.shared.util.ClassUtils;
54  import org.apache.myfaces.spi.AnnotationProvider;
55  import org.apache.myfaces.spi.AnnotationProviderFactory;
56  import org.apache.myfaces.view.facelets.util.Classpath;
57  
58  /**
59   * 
60   * @since 2.0.2
61   * @author Leonardo Uribe
62   */
63  public class DefaultAnnotationProvider extends AnnotationProvider
64  {
65      private static final Logger log = Logger.getLogger(DefaultAnnotationProvider.class.getName());
66      
67      /**
68       * Servlet context init parameter which defines which packages to scan
69       * for beans, separated by commas.
70       */
71      @JSFWebConfigParam(since="2.0")
72      public static final String SCAN_PACKAGES = "org.apache.myfaces.annotation.SCAN_PACKAGES";
73  
74      /**
75       * <p>Prefix path used to locate web application classes for this
76       * web application.</p>
77       */
78      private static final String WEB_CLASSES_PREFIX = "/WEB-INF/classes/";
79      
80      /**
81       * <p>Prefix path used to locate web application libraries for this
82       * web application.</p>
83       */
84      private static final String WEB_LIB_PREFIX = "/WEB-INF/lib/";
85      
86      private static final String META_INF_PREFIX = "META-INF/";
87  
88      private static final String FACES_CONFIG_SUFFIX = ".faces-config.xml";
89      
90      private static final String STANDARD_FACES_CONFIG_RESOURCE = "META-INF/standard-faces-config.xml";
91  
92      /**
93       * <p>Resource path used to acquire implicit resources buried
94       * inside application JARs.</p>
95       */
96      private static final String FACES_CONFIG_IMPLICIT = "META-INF/faces-config.xml";
97      
98      private final _ClassByteCodeAnnotationFilter _filter;
99  
100     /**
101      * This set contains the annotation names that this AnnotationConfigurator is able to scan
102      * in the format that is read from .class file.
103      */
104     private static Set<String> byteCodeAnnotationsNames;
105 
106     static
107     {
108         Set<String> bcan = new HashSet<String>(10, 1f);
109         bcan.add("Ljavax/faces/component/FacesComponent;");
110         bcan.add("Ljavax/faces/component/behavior/FacesBehavior;");
111         bcan.add("Ljavax/faces/convert/FacesConverter;");
112         bcan.add("Ljavax/faces/validator/FacesValidator;");
113         bcan.add("Ljavax/faces/render/FacesRenderer;");
114         bcan.add("Ljavax/faces/bean/ManagedBean;");
115         bcan.add("Ljavax/faces/event/NamedEvent;");
116         //bcan.add("Ljavax/faces/event/ListenerFor;");
117         //bcan.add("Ljavax/faces/event/ListenersFor;");
118         bcan.add("Ljavax/faces/render/FacesBehaviorRenderer;");
119 
120         byteCodeAnnotationsNames = Collections.unmodifiableSet(bcan);
121     }
122     
123     private static Set<Class<? extends Annotation>> JSF_ANNOTATION_CLASSES;
124     
125     static
126     {
127         Set<Class<? extends Annotation>> bcan = new HashSet<Class<? extends Annotation>>(10, 1f);
128         bcan.add(FacesComponent.class);
129         bcan.add(FacesBehavior.class);
130         bcan.add(FacesConverter.class);
131         bcan.add(FacesValidator.class);
132         bcan.add(FacesRenderer.class);
133         bcan.add(ManagedBean.class);
134         bcan.add(NamedEvent.class);
135         bcan.add(FacesBehaviorRenderer.class);
136         JSF_ANNOTATION_CLASSES = Collections.unmodifiableSet(bcan);
137     }
138     
139     public DefaultAnnotationProvider()
140     {
141         super();
142         _filter = new _ClassByteCodeAnnotationFilter();
143     }
144     
145     @Override
146     public Map<Class<? extends Annotation>, Set<Class<?>>> getAnnotatedClasses(ExternalContext ctx)
147     {
148         Map<Class<? extends Annotation>,Set<Class<?>>> map = new HashMap<Class<? extends Annotation>, Set<Class<?>>>();
149         Collection<Class<?>> classes = null;
150 
151         //1. Scan for annotations on /WEB-INF/classes
152         try
153         {
154             classes = getAnnotatedWebInfClasses(ctx);
155         }
156         catch (IOException e)
157         {
158             throw new FacesException(e);
159         }
160 
161         for (Class<?> clazz : classes)
162         {
163             processClass(map, clazz);
164         }
165         
166         //2. Scan for annotations on classpath
167         try
168         {
169             AnnotationProvider provider = AnnotationProviderFactory.getAnnotationProviderFactory(ctx).getAnnotationProvider(ctx);
170             classes = getAnnotatedMetaInfClasses(ctx, provider.getBaseUrls());
171         }
172         catch (IOException e)
173         {
174             throw new FacesException(e);
175         }
176         
177         for (Class<?> clazz : classes)
178         {
179             processClass(map, clazz);
180         }
181         
182         //3. Scan on myfaces-impl for annotations available on myfaces-impl.
183         //Also scan jar including META-INF/standard-faces-config.xml
184         //(myfaces-impl jar file)
185         URL url = getClassLoader().getResource(STANDARD_FACES_CONFIG_RESOURCE);
186         if (url == null)
187         {
188             url = getClass().getClassLoader().getResource(STANDARD_FACES_CONFIG_RESOURCE);
189         }
190         classes = getAnnotatedMyfacesImplClasses(ctx, url);
191         for (Class<?> clazz : classes)
192         {
193             processClass(map, clazz);
194         }
195         
196         return map;
197     }
198     
199     @Override
200     public Set<URL> getBaseUrls() throws IOException
201     {
202         Set<URL> urlSet = new HashSet<URL>();
203         
204         //This usually happens when maven-jetty-plugin is used
205         //Scan jars looking for paths including META-INF/faces-config.xml
206         Enumeration<URL> resources = getClassLoader().getResources(FACES_CONFIG_IMPLICIT);
207         while (resources.hasMoreElements())
208         {
209             urlSet.add(resources.nextElement());
210         }
211 
212         //Scan files inside META-INF ending with .faces-config.xml
213         URL[] urls = Classpath.search(getClassLoader(), META_INF_PREFIX, FACES_CONFIG_SUFFIX);
214         for (int i = 0; i < urls.length; i++)
215         {
216             urlSet.add(urls[i]);
217         }
218         
219         return urlSet;
220     }
221 
222     protected Collection<Class<?>> getAnnotatedMetaInfClasses(ExternalContext ctx, Set<URL> urls)
223     {
224         if (urls != null && !urls.isEmpty())
225         {
226             List<Class<?>> list = new ArrayList<Class<?>>();
227             for (URL url : urls)
228             {
229                 try
230                 {
231                     JarFile jarFile = getJarFile(url);
232                     if (jarFile != null)
233                     {
234                         archiveClasses(ctx, jarFile, list);
235                     }
236                 }
237                 catch(IOException e)
238                 {
239                     log.log(Level.SEVERE, "cannot scan jar file for annotations:"+url, e);
240                 }
241             }
242             return list;
243         }
244         return Collections.emptyList();
245     }
246 
247     protected Collection<Class<?>> getAnnotatedMyfacesImplClasses(ExternalContext ctx, URL url)
248     {
249         return Collections.emptyList();
250         /*
251         try
252         {
253             List<Class<?>> list = new ArrayList<Class<?>>();
254             JarFile jarFile = getJarFile(url);
255             if (jarFile == null)
256             {
257                 return list;
258             }
259             else
260             {
261                 return archiveClasses(ctx, jarFile, list);
262             }
263         }
264         catch(IOException e)
265         {
266             throw new FacesException("cannot scan jar file for annotations:"+url, e);
267         }*/
268     }
269 
270     protected Collection<Class<?>> getAnnotatedWebInfClasses(ExternalContext ctx) throws IOException
271     {
272         String scanPackages = ctx.getInitParameter(SCAN_PACKAGES);
273         if (scanPackages != null)
274         {
275             try
276             {
277                 return packageClasses(ctx, scanPackages);
278             }
279             catch (ClassNotFoundException e)
280             {
281                 throw new FacesException(e);
282             }
283             catch (IOException e)
284             {
285                 throw new FacesException(e);
286             }
287         }
288         else
289         {
290             return webClasses(ctx);
291         }
292     }
293     
294     /**
295      * <p>Return a list of the classes defined within the given packages
296      * If there are no such classes, a zero-length list will be returned.</p>
297      *
298      * @param scanPackages the package configuration
299      *
300      * @exception ClassNotFoundException if a located class cannot be loaded
301      * @exception IOException if an input/output error occurs
302      */
303     private List<Class<?>> packageClasses(final ExternalContext externalContext,
304             final String scanPackages) throws ClassNotFoundException, IOException
305     {
306 
307         List<Class<?>> list = new ArrayList<Class<?>>();
308 
309         String[] scanPackageTokens = scanPackages.split(",");
310         for (String scanPackageToken : scanPackageTokens)
311         {
312             if (scanPackageToken.toLowerCase().endsWith(".jar"))
313             {
314                 URL jarResource = externalContext.getResource(WEB_LIB_PREFIX
315                         + scanPackageToken);
316                 String jarURLString = "jar:" + jarResource.toString() + "!/";
317                 URL url = new URL(jarURLString);
318                 JarFile jarFile = ((JarURLConnection) url.openConnection())
319                         .getJarFile();
320 
321                 archiveClasses(externalContext, jarFile, list);
322             }
323             else
324             {
325                 List<Class> list2 = new ArrayList<Class>();
326                 _PackageInfo.getInstance().getClasses(list2, scanPackageToken);
327                 for (Class c : list2)
328                 {
329                     list.add(c);                    
330                 }
331             }
332         }
333         return list;
334     }    
335     
336     /**
337      * <p>Return a list of classes to examine from the specified JAR archive.
338      * If this archive has no classes in it, a zero-length list is returned.</p>
339      *
340      * @param context <code>ExternalContext</code> instance for
341      *  this application
342      * @param jar <code>JarFile</code> for the archive to be scanned
343      *
344      * @exception ClassNotFoundException if a located class cannot be loaded
345      */
346     private List<Class<?>> archiveClasses(ExternalContext context, JarFile jar, List<Class<?>> list)
347     {
348 
349         // Accumulate and return a list of classes in this JAR file
350         ClassLoader loader = ClassUtils.getContextClassLoader();
351         if (loader == null)
352         {
353             loader = this.getClass().getClassLoader();
354         }
355         Enumeration<JarEntry> entries = jar.entries();
356         while (entries.hasMoreElements())
357         {
358             JarEntry entry = entries.nextElement();
359             if (entry.isDirectory())
360             {
361                 continue; // This is a directory
362             }
363             String name = entry.getName();
364             if (name.startsWith("META-INF/"))
365             {
366                 continue; // Attribute files
367             }
368             if (!name.endsWith(".class"))
369             {
370                 continue; // This is not a class
371             }
372 
373             DataInputStream in = null;
374             boolean couldContainAnnotation = false;
375             try
376             {
377                 in = new DataInputStream(jar.getInputStream(entry));
378                 couldContainAnnotation = _filter
379                         .couldContainAnnotationsOnClassDef(in,
380                                 byteCodeAnnotationsNames);
381             }
382             catch (IOException e)
383             {
384                 // Include this class - we can't scan this class using
385                 // the filter, but it could be valid, so we need to
386                 // load it using the classLoader. Anyway, log a debug
387                 // message.
388                 couldContainAnnotation = true;
389                 if (log.isLoggable(Level.FINE))
390                 {
391                     log.fine("IOException when filtering class " + name
392                             + " for annotations");
393                 }
394             }
395             finally
396             {
397                 if (in != null)
398                     try
399                     {
400                         in.close();
401                     }
402                     catch (IOException e)
403                     {
404                         // No Op
405                     }
406             }
407 
408             if (couldContainAnnotation)
409             {
410                 name = name.substring(0, name.length() - 6); // Trim ".class"
411                 Class<?> clazz = null;
412                 try
413                 {
414                     clazz = loader.loadClass(name.replace('/', '.'));
415                 }
416                 catch (NoClassDefFoundError e)
417                 {
418                     ; // Skip this class - we cannot analyze classes we cannot load
419                 }
420                 catch (Exception e)
421                 {
422                     ; // Skip this class - we cannot analyze classes we cannot load
423                 }
424                 if (clazz != null)
425                 {
426                     list.add(clazz);
427                 }
428             }
429         }
430         return list;
431 
432     }
433     
434     /**
435      * <p>Return a list of the classes defined under the
436      * <code>/WEB-INF/classes</code> directory of this web
437      * application.  If there are no such classes, a zero-length list
438      * will be returned.</p>
439      *
440      * @param externalContext <code>ExternalContext</code> instance for
441      *  this application
442      *
443      * @exception ClassNotFoundException if a located class cannot be loaded
444      */
445     private List<Class<?>> webClasses(ExternalContext externalContext)
446     {
447         List<Class<?>> list = new ArrayList<Class<?>>();
448         webClasses(externalContext, WEB_CLASSES_PREFIX, list);
449         return list;
450     }
451 
452     /**
453      * <p>Add classes found in the specified directory to the specified
454      * list, recursively calling this method when a directory is encountered.</p>
455      *
456      * @param externalContext <code>ExternalContext</code> instance for
457      *  this application
458      * @param prefix Prefix specifying the "directory path" to be searched
459      * @param list List to be appended to
460      *
461      * @exception ClassNotFoundException if a located class cannot be loaded
462      */
463     private void webClasses(ExternalContext externalContext, String prefix,
464             List<Class<?>> list)
465     {
466 
467         ClassLoader loader = getClassLoader();
468 
469         Set<String> paths = externalContext.getResourcePaths(prefix);
470         if(paths == null)
471         {
472             return; //need this in case there is no WEB-INF/classes directory
473         }
474         if (log.isLoggable(Level.FINEST))
475         {
476             log.finest("webClasses(" + prefix + ") - Received " + paths.size()
477                     + " paths to check");
478         }
479 
480         String path = null;
481 
482         if (paths.isEmpty())
483         {
484             if (log.isLoggable(Level.WARNING))
485             {
486                 log
487                         .warning("AnnotationConfigurator does not found classes "
488                                 + "for annotations in "
489                                 + prefix
490                                 + " ."
491                                 + " This could happen because maven jetty plugin is used"
492                                 + " (goal jetty:run). Try configure "
493                                 + SCAN_PACKAGES + " init parameter "
494                                 + "or use jetty:run-exploded instead.");
495             }
496         }
497         else
498         {
499             for (Object pathObject : paths)
500             {
501                 path = (String) pathObject;
502                 if (path.endsWith("/"))
503                 {
504                     webClasses(externalContext, path, list);
505                 }
506                 else if (path.endsWith(".class"))
507                 {
508                     DataInputStream in = null;
509                     boolean couldContainAnnotation = false;
510                     try
511                     {
512                         in = new DataInputStream(externalContext
513                                 .getResourceAsStream(path));
514                         couldContainAnnotation = _filter
515                                 .couldContainAnnotationsOnClassDef(in,
516                                         byteCodeAnnotationsNames);
517                     }
518                     catch (IOException e)
519                     {
520                         // Include this class - we can't scan this class using
521                         // the filter, but it could be valid, so we need to
522                         // load it using the classLoader. Anyway, log a debug
523                         // message.
524                         couldContainAnnotation = true;
525                         if (log.isLoggable(Level.FINE))
526                         {
527                             log.fine("IOException when filtering class " + path
528                                     + " for annotations");
529                         }
530                     }
531                     finally
532                     {
533                         if (in != null)
534                             try
535                             {
536                                 in.close();
537                             }
538                             catch (IOException e)
539                             {
540                                 // No Op
541                             }
542                     }
543 
544                     if (couldContainAnnotation)
545                     {
546                         //Load it and add it to list for later processing
547                         path = path.substring(WEB_CLASSES_PREFIX.length()); // Strip prefix
548                         path = path.substring(0, path.length() - 6); // Strip suffix
549                         path = path.replace('/', '.'); // Convert to FQCN
550 
551                         Class<?> clazz = null;
552                         try
553                         {
554                             clazz = loader.loadClass(path);
555                         }
556                         catch (NoClassDefFoundError e)
557                         {
558                             ; // Skip this class - we cannot analyze classes we cannot load
559                         }
560                         catch (Exception e)
561                         {
562                             ; // Skip this class - we cannot analyze classes we cannot load
563                         }
564                         if (clazz != null)
565                         {
566                             list.add(clazz);
567                         }
568                     }
569                 }
570             }
571         }
572     }
573     
574     private JarFile getJarFile(URL url) throws IOException
575     {
576         URLConnection conn = url.openConnection();
577         conn.setUseCaches(false);
578         conn.setDefaultUseCaches(false);
579 
580         JarFile jarFile;
581         if (conn instanceof JarURLConnection)
582         {
583             jarFile = ((JarURLConnection) conn).getJarFile();
584         }
585         else
586         {
587             jarFile = _getAlternativeJarFile(url);
588         }
589         return jarFile;
590     }
591     
592 
593     /**
594      * taken from org.apache.myfaces.view.facelets.util.Classpath
595      * 
596      * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile
597      * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full
598      * solution, since an unpacked WAR or EAR will not have JAR "files" as such.
599      */
600     private static JarFile _getAlternativeJarFile(URL url) throws IOException
601     {
602         String urlFile = url.getFile();
603 
604         // Trim off any suffix - which is prefixed by "!/" on Weblogic
605         int separatorIndex = urlFile.indexOf("!/");
606 
607         // OK, didn't find that. Try the less safe "!", used on OC4J
608         if (separatorIndex == -1)
609         {
610             separatorIndex = urlFile.indexOf('!');
611         }
612 
613         if (separatorIndex != -1)
614         {
615             String jarFileUrl = urlFile.substring(0, separatorIndex);
616             // And trim off any "file:" prefix.
617             if (jarFileUrl.startsWith("file:"))
618             {
619                 jarFileUrl = jarFileUrl.substring("file:".length());
620             }
621 
622             return new JarFile(jarFileUrl);
623         }
624 
625         return null;
626     }
627         
628     private ClassLoader getClassLoader()
629     {
630         ClassLoader loader = ClassUtils.getContextClassLoader();
631         if (loader == null)
632         {
633             loader = this.getClass().getClassLoader();
634         }
635         return loader;
636     }
637     
638     private void processClass(Map<Class<? extends Annotation>,Set<Class<?>>> map, Class<?> clazz)
639     {
640         Annotation[] annotations = clazz.getAnnotations();
641         for (Annotation anno : annotations)
642         {
643             Class<? extends Annotation> annotationClass = anno.annotationType();
644             if (JSF_ANNOTATION_CLASSES.contains(annotationClass))
645             {
646                 Set<Class<?>> set = map.get(annotationClass);
647                 if (set == null)
648                 {
649                     set = new HashSet<Class<?>>();
650                     set.add(clazz);
651                     map.put(annotationClass, set);
652                 }
653                 else
654                 {
655                     set.add(clazz);
656                 }
657 
658             }
659         }
660     }
661 }