View Javadoc

1   // ========================================================================
2   // Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.annotations;
15  
16  import java.lang.reflect.Field;
17  import java.lang.reflect.Method;
18  import java.lang.reflect.Modifier;
19  import java.util.List;
20  
21  import javax.annotation.PostConstruct;
22  import javax.annotation.PreDestroy;
23  import javax.annotation.Resource;
24  import javax.annotation.Resources;
25  import javax.annotation.security.RunAs;
26  import javax.naming.InitialContext;
27  import javax.naming.NameNotFoundException;
28  import javax.naming.NamingException;
29  
30  import org.eclipse.jetty.plus.annotation.Injection;
31  import org.eclipse.jetty.plus.annotation.InjectionCollection;
32  import org.eclipse.jetty.plus.annotation.LifeCycleCallbackCollection;
33  import org.eclipse.jetty.plus.annotation.PostConstructCallback;
34  import org.eclipse.jetty.plus.annotation.PreDestroyCallback;
35  import org.eclipse.jetty.plus.annotation.RunAsCollection;
36  import org.eclipse.jetty.servlet.ServletHandler;
37  import org.eclipse.jetty.util.IntrospectionUtil;
38  import org.eclipse.jetty.util.LazyList;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.webapp.WebAppContext;
41  
42  
43  
44  /**
45   * AnnotationProcessor
46   *
47   * Act on the annotations discovered in the webapp.
48   */
49  public class AnnotationProcessor
50  {
51      AnnotationFinder _finder;
52      ClassLoader _loader;
53      RunAsCollection _runAs;
54      InjectionCollection _injections;
55      LifeCycleCallbackCollection _callbacks;
56      List _servlets;
57      List _filters;
58      List _listeners;
59      List _servletMappings;
60      List _filterMappings;
61      WebAppContext _webApp;
62      
63      private static Class[] __envEntryTypes = 
64          new Class[] {String.class, Character.class, Integer.class, Boolean.class, Double.class, Byte.class, Short.class, Long.class, Float.class};
65     
66      public AnnotationProcessor(WebAppContext webApp, AnnotationFinder finder)
67      {
68          if (webApp == null)
69              throw new IllegalStateException("No WebAppContext");
70          
71          _webApp=webApp;
72          _finder=finder;
73          ServletHandler servletHandler = _webApp.getServletHandler();
74          _filters = LazyList.array2List(servletHandler.getFilters());
75          _filterMappings = LazyList.array2List(servletHandler.getFilterMappings());
76          _servlets = LazyList.array2List(servletHandler.getServlets());
77          _servletMappings = LazyList.array2List(servletHandler.getServletMappings());
78          _listeners = LazyList.array2List(_webApp.getEventListeners());
79          
80          _runAs = (RunAsCollection)_webApp.getAttribute(RunAsCollection.RUNAS_COLLECTION);
81          _injections = (InjectionCollection)_webApp.getAttribute(InjectionCollection.INJECTION_COLLECTION);
82          _callbacks = (LifeCycleCallbackCollection)_webApp.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
83          
84          if (_runAs == null || _injections == null || _callbacks == null)
85              throw new IllegalStateException("RunAs, Injections or LifeCycleCallbacks is null");
86      }
87      
88      
89      public void process ()
90      throws Exception
91      { 
92          processServlets();
93          processFilters();
94          processListeners();
95          processRunAsAnnotations();
96          processLifeCycleCallbackAnnotations();
97          processResourcesAnnotations();
98          processResourceAnnotations();
99      }
100     
101     public void processServlets ()
102     throws Exception
103     {
104     }
105     
106     public void processFilters ()
107     throws Exception
108     {   
109     }
110     
111 
112     
113     public void processListeners ()
114     throws Exception
115     {
116     }
117     
118     
119     public List getServlets ()
120     {
121         return _servlets;
122     }
123     
124     public List getServletMappings ()
125     {
126         return _servletMappings;
127     }
128     
129     public List getFilters ()
130     {
131         return _filters;
132     }
133     
134     public List getFilterMappings ()
135     {
136         return _filterMappings;
137     }
138     
139     public List getListeners()
140     {
141         return _listeners;
142     }
143    
144     
145     public void processRunAsAnnotations ()
146     throws Exception
147     {
148         for (Class clazz:_finder.getClassesForAnnotation(RunAs.class))
149         {
150             if (!javax.servlet.Servlet.class.isAssignableFrom(clazz))
151             {
152                 Log.debug("Ignoring runAs notation on on-servlet class "+clazz.getName());
153                 continue;
154             }
155             RunAs runAs = (RunAs)clazz.getAnnotation(RunAs.class);
156             if (runAs != null)
157             {
158                 String role = runAs.value();
159                 if (role != null)
160                 {
161                     org.eclipse.jetty.plus.annotation.RunAs ra = new org.eclipse.jetty.plus.annotation.RunAs();
162                     ra.setTargetClass(clazz);
163                     ra.setRoleName(role);
164                     _runAs.add(ra);
165                 }
166             }
167         } 
168     }
169     
170     
171     public void processLifeCycleCallbackAnnotations()
172     throws Exception
173     {
174         processPostConstructAnnotations();
175         processPreDestroyAnnotations();
176     }
177 
178     private void processPostConstructAnnotations ()
179     throws Exception
180     {
181         //      TODO: check that the same class does not have more than one
182         for (Method m:_finder.getMethodsForAnnotation(PostConstruct.class))
183         {
184             if (!isServletType(m.getDeclaringClass()))
185             {
186                 Log.debug("Ignoring "+m.getName()+" as non-servlet type");
187                 continue;
188             }
189             if (m.getParameterTypes().length != 0)
190                 throw new IllegalStateException(m+" has parameters");
191             if (m.getReturnType() != Void.TYPE)
192                 throw new IllegalStateException(m+" is not void");
193             if (m.getExceptionTypes().length != 0)
194                 throw new IllegalStateException(m+" throws checked exceptions");
195             if (Modifier.isStatic(m.getModifiers()))
196                 throw new IllegalStateException(m+" is static");
197 
198             PostConstructCallback callback = new PostConstructCallback();
199             callback.setTargetClass(m.getDeclaringClass());
200             callback.setTarget(m);
201             _callbacks.add(callback);
202         }
203     }
204 
205     public void processPreDestroyAnnotations ()
206     throws Exception
207     {
208         //TODO: check that the same class does not have more than one
209 
210         for (Method m: _finder.getMethodsForAnnotation(PreDestroy.class))
211         {
212             if (!isServletType(m.getDeclaringClass()))
213             {
214                 Log.debug("Ignoring "+m.getName()+" as non-servlet type");
215                 continue;
216             }
217             if (m.getParameterTypes().length != 0)
218                 throw new IllegalStateException(m+" has parameters");
219             if (m.getReturnType() != Void.TYPE)
220                 throw new IllegalStateException(m+" is not void");
221             if (m.getExceptionTypes().length != 0)
222                 throw new IllegalStateException(m+" throws checked exceptions");
223             if (Modifier.isStatic(m.getModifiers()))
224                 throw new IllegalStateException(m+" is static");
225            
226             PreDestroyCallback callback = new PreDestroyCallback(); 
227             callback.setTargetClass(m.getDeclaringClass());
228             callback.setTarget(m);
229             _callbacks.add(callback);
230         }
231     }
232     
233     
234     /**
235      * Process @Resources annotation on classes
236      */
237     public void processResourcesAnnotations ()
238     throws Exception
239     {
240         List<Class<?>> classes = _finder.getClassesForAnnotation(Resources.class);
241         for (Class<?> clazz:classes)
242         {
243             if (!isServletType(clazz))
244             {
245                 Log.debug("Ignoring @Resources annotation on on-servlet type class "+clazz.getName());
246                 continue;
247             }
248             //Handle Resources annotation - add namespace entries
249             Resources resources = (Resources)clazz.getAnnotation(Resources.class);
250             if (resources == null)
251                 continue;
252 
253             Resource[] resArray = resources.value();
254             if (resArray==null||resArray.length==0)
255                 continue;
256 
257             for (int j=0;j<resArray.length;j++)
258             {
259                 String name = resArray[j].name();
260                 String mappedName = resArray[j].mappedName();
261                 Resource.AuthenticationType auth = resArray[j].authenticationType();
262                 Class type = resArray[j].type();
263                 boolean shareable = resArray[j].shareable();
264 
265                 if (name==null || name.trim().equals(""))
266                     throw new IllegalStateException ("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");
267                 try
268                 {
269                     //TODO don't ignore the shareable, auth etc etc
270 
271                        if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp, name, mappedName))
272                            if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp.getServer(), name, mappedName))
273                                throw new IllegalStateException("No resource bound at "+(mappedName==null?name:mappedName));
274                 }
275                 catch (NamingException e)
276                 {
277                     throw new IllegalStateException(e);
278                 }
279             }
280         }
281     }
282     
283     
284     public void processResourceAnnotations ()
285     throws Exception
286     {
287         processClassResourceAnnotations();
288         processMethodResourceAnnotations();
289         processFieldResourceAnnotations();
290     }
291     
292     /**
293      *  Class level Resource annotations declare a name in the
294      *  environment that will be looked up at runtime. They do
295      *  not specify an injection.
296      */
297     public void processClassResourceAnnotations ()
298     throws Exception
299     {
300         List<Class<?>> classes = _finder.getClassesForAnnotation(Resource.class);
301         for (Class<?> clazz:classes)
302         {
303             if (!isServletType(clazz))
304             {
305                 Log.debug("Ignoring @Resource annotation on on-servlet type class "+clazz.getName());
306                 continue;
307             }
308             //Handle Resource annotation - add namespace entries
309             Resource resource = (Resource)clazz.getAnnotation(Resource.class);
310             if (resource != null)
311             {
312                String name = resource.name();
313                String mappedName = resource.mappedName();
314                Resource.AuthenticationType auth = resource.authenticationType();
315                Class type = resource.type();
316                boolean shareable = resource.shareable();
317                
318                if (name==null || name.trim().equals(""))
319                    throw new IllegalStateException ("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");
320                
321                try
322                {
323                    //TODO don't ignore the shareable, auth etc etc
324                    if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp, name,mappedName))
325                        if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp.getServer(), name,mappedName))
326                            throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
327                }
328                catch (NamingException e)
329                {
330                    throw new IllegalStateException(e);
331                }
332             }
333         }
334     }
335     
336     /**
337      * Process a Resource annotation on the Methods.
338      * 
339      * This will generate a JNDI entry, and an Injection to be
340      * processed when an instance of the class is created.
341      * @param injections
342      */
343     public void processMethodResourceAnnotations ()
344     throws Exception
345     {
346         //Get all methods that have a Resource annotation
347         List<Method> methods = _finder.getMethodsForAnnotation(javax.annotation.Resource.class);
348 
349         for (Method m: methods)
350         {
351             if (!isServletType(m.getDeclaringClass()))
352             {
353                 Log.debug("Ignoring @Resource annotation on on-servlet type method "+m.getName());
354                 continue;
355             }
356             /*
357              * Commons Annotations Spec 2.3
358              * " The Resource annotation is used to declare a reference to a resource.
359              *   It can be specified on a class, methods or on fields. When the 
360              *   annotation is applied on a field or method, the container will 
361              *   inject an instance of the requested resource into the application 
362              *   when the application is initialized... Even though this annotation 
363              *   is not marked Inherited, if used all superclasses MUST be examined 
364              *   to discover all uses of this annotation. All such annotation instances 
365              *   specify resources that are needed by the application. Note that this 
366              *   annotation may appear on private fields and methods of the superclasses. 
367              *   Injection of the declared resources needs to happen in these cases as 
368              *   well, even if a method with such an annotation is overridden by a subclass."
369              *  
370              *  Which IMHO, put more succinctly means "If you find a @Resource on any method
371              *  or field, inject it!".
372              */
373             Resource resource = (Resource)m.getAnnotation(Resource.class);
374             if (resource == null)
375                 continue;
376 
377             //JavaEE Spec 5.2.3: Method cannot be static
378             if (Modifier.isStatic(m.getModifiers()))
379                 throw new IllegalStateException(m+" cannot be static");
380 
381 
382             // Check it is a valid javabean 
383             if (!IntrospectionUtil.isJavaBeanCompliantSetter(m))
384                 throw new IllegalStateException(m+" is not a java bean compliant setter method");
385 
386             //default name is the javabean property name
387             String name = m.getName().substring(3);
388             name = name.substring(0,1).toLowerCase()+name.substring(1);
389             name = m.getDeclaringClass().getCanonicalName()+"/"+name;
390             //allow default name to be overridden
391             name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
392             //get the mappedName if there is one
393             String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
394 
395             Class type = m.getParameterTypes()[0];
396 
397             //get other parts that can be specified in @Resource
398             Resource.AuthenticationType auth = resource.authenticationType();
399             boolean shareable = resource.shareable();
400 
401             //if @Resource specifies a type, check it is compatible with setter param
402             if ((resource.type() != null) 
403                     && 
404                     !resource.type().equals(Object.class)
405                     &&
406                     (!IntrospectionUtil.isTypeCompatible(type, resource.type(), false)))
407                 throw new IllegalStateException("@Resource incompatible type="+resource.type()+ " with method param="+type+ " for "+m);
408 
409             //check if an injection has already been setup for this target by web.xml
410             Injection webXmlInjection = _injections.getInjection(m.getDeclaringClass(), m);
411             if (webXmlInjection == null)
412             {
413                 try
414                 {
415                     //try binding name to environment
416                     //try the webapp's environment first
417                     boolean bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp, name, mappedName);
418                     
419                     //try the server's environment
420                     if (!bound)
421                         bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp.getServer(), name, mappedName);
422                     
423                     //try the jvm's environment
424                     if (!bound)
425                         bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(null, name, mappedName);
426                     
427                     //TODO if it is an env-entry from web.xml it can be injected, in which case there will be no
428                     //NamingEntry, just a value bound in java:comp/env
429                     if (!bound)
430                     {
431                         try
432                         {
433                             InitialContext ic = new InitialContext();
434                             String nameInEnvironment = (mappedName!=null?mappedName:name);
435                             ic.lookup("java:comp/env/"+nameInEnvironment);                               
436                             bound = true;
437                         }
438                         catch (NameNotFoundException e)
439                         {
440                             bound = false;
441                         }
442                     }
443                     
444                     if (bound)
445                     {
446                         Log.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
447                         //   Make the Injection for it
448                         Injection injection = new Injection();
449                         injection.setTargetClass(m.getDeclaringClass());
450                         injection.setJndiName(name);
451                         injection.setMappingName(mappedName);
452                         injection.setTarget(m);
453                         _injections.add(injection);
454                     } 
455                     else if (!isEnvEntryType(type))
456                     {
457 
458                         //if this is an env-entry type resource and there is no value bound for it, it isn't
459                         //an error, it just means that perhaps the code will use a default value instead
460                         // JavaEE Spec. sec 5.4.1.3   
461                         throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
462                     }
463                 }
464                 catch (NamingException e)
465                 {  
466                     //if this is an env-entry type resource and there is no value bound for it, it isn't
467                     //an error, it just means that perhaps the code will use a default value instead
468                     // JavaEE Spec. sec 5.4.1.3
469                     if (!isEnvEntryType(type))
470                         throw new IllegalStateException(e);
471                 }
472             }
473             else
474             {
475                 //if an injection is already set up for this name, then the types must be compatible
476                 //JavaEE spec sec 5.2.4
477 
478                 Object value = webXmlInjection.lookupInjectedValue();
479                 if (!IntrospectionUtil.isTypeCompatible(type, value.getClass(), false))
480                     throw new IllegalStateException("Type of field="+type+" is not compatible with Resource type="+value.getClass());
481             }
482         }
483     }
484 
485     /**
486      * Process @Resource annotation for a Field. These will both set up a
487      * JNDI entry and generate an Injection. Or they can be the equivalent
488      * of env-entries with default values
489      * 
490      * @param injections
491      */
492     public void processFieldResourceAnnotations ()
493     throws Exception
494     {
495         //Get all fields that have a Resource annotation
496         List<Field> fields = _finder.getFieldsForAnnotation(Resource.class);
497         for (Field f: fields)
498         {
499             if (!isServletType(f.getDeclaringClass()))
500             {
501                 Log.debug("Ignoring @Resource annotation on on-servlet type field "+f.getName());
502                 continue;
503             }
504             Resource resource = (Resource)f.getAnnotation(Resource.class);
505             if (resource == null)
506                 continue;
507 
508             //JavaEE Spec 5.2.3: Field cannot be static
509             if (Modifier.isStatic(f.getModifiers()))
510                 throw new IllegalStateException(f+" cannot be static");
511 
512             //JavaEE Spec 5.2.3: Field cannot be final
513             if (Modifier.isFinal(f.getModifiers()))
514                 throw new IllegalStateException(f+" cannot be final");
515 
516             //work out default name
517             String name = f.getDeclaringClass().getCanonicalName()+"/"+f.getName();
518             //allow @Resource name= to override the field name
519             name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
520 
521             //get the type of the Field
522             Class type = f.getType();
523             //if @Resource specifies a type, check it is compatible with field type
524             if ((resource.type() != null)
525                     && 
526                     !resource.type().equals(Object.class)
527                     &&
528                     (!IntrospectionUtil.isTypeCompatible(type, resource.type(), false)))
529                 throw new IllegalStateException("@Resource incompatible type="+resource.type()+ " with field type ="+f.getType());
530 
531             //get the mappedName if there is one
532             String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
533             //get other parts that can be specified in @Resource
534             Resource.AuthenticationType auth = resource.authenticationType();
535             boolean shareable = resource.shareable();
536             //check if an injection has already been setup for this target by web.xml
537             Injection webXmlInjection = _injections.getInjection(f.getDeclaringClass(), f);
538             if (webXmlInjection == null)
539             {
540                 try
541                 {
542                     boolean bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp, name, mappedName);
543                     if (!bound)
544                         bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_webApp.getServer(), name, mappedName);
545                     if (!bound)
546                         bound =  org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(null, name, mappedName); 
547                     if (!bound)
548                     {
549                         //see if there is an env-entry value been bound from web.xml
550                         try
551                         {
552                             InitialContext ic = new InitialContext();
553                             String nameInEnvironment = (mappedName!=null?mappedName:name);
554                             ic.lookup("java:comp/env/"+nameInEnvironment);                               
555                             bound = true;
556                         }
557                         catch (NameNotFoundException e)
558                         {
559                             bound = false;
560                         }
561                     }
562                     //Check there is a JNDI entry for this annotation 
563                     if (bound)
564                     { 
565                         Log.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
566                         //   Make the Injection for it if the binding succeeded
567                         Injection injection = new Injection();
568                         injection.setTargetClass(f.getDeclaringClass());
569                         injection.setJndiName(name);
570                         injection.setMappingName(mappedName);
571                         injection.setTarget(f);
572                         _injections.add(injection); 
573                     }  
574                     else if (!isEnvEntryType(type))
575                     {
576                         //if this is an env-entry type resource and there is no value bound for it, it isn't
577                         //an error, it just means that perhaps the code will use a default value instead
578                         // JavaEE Spec. sec 5.4.1.3
579 
580                         throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
581                     }
582                 }
583                 catch (NamingException e)
584                 {
585                     //if this is an env-entry type resource and there is no value bound for it, it isn't
586                     //an error, it just means that perhaps the code will use a default value instead
587                     // JavaEE Spec. sec 5.4.1.3
588                     if (!isEnvEntryType(type))
589                         throw new IllegalStateException(e);
590                 }
591             }
592             else
593             {
594                 //if an injection is already set up for this name, then the types must be compatible
595                 //JavaEE spec sec 5.2.4
596                 Object value = webXmlInjection.lookupInjectedValue();
597                 if (!IntrospectionUtil.isTypeCompatible(type, value.getClass(), false))
598                     throw new IllegalStateException("Type of field="+type+" is not compatible with Resource type="+value.getClass());
599             }
600         }
601     }
602     
603 
604     /**
605      * Check if the presented method belongs to a class that is one
606      * of the classes with which a servlet container should be concerned.
607      * @param m
608      * @return
609      */
610     private boolean isServletType (Class c)
611     {    
612         boolean isServlet = false;
613         if (javax.servlet.Servlet.class.isAssignableFrom(c) ||
614                 javax.servlet.Filter.class.isAssignableFrom(c) || 
615                 javax.servlet.ServletContextListener.class.isAssignableFrom(c) ||
616                 javax.servlet.ServletContextAttributeListener.class.isAssignableFrom(c) ||
617                 javax.servlet.ServletRequestListener.class.isAssignableFrom(c) ||
618                 javax.servlet.ServletRequestAttributeListener.class.isAssignableFrom(c) ||
619                 javax.servlet.http.HttpSessionListener.class.isAssignableFrom(c) ||
620                 javax.servlet.http.HttpSessionAttributeListener.class.isAssignableFrom(c))
621 
622                 isServlet=true;
623         
624         return isServlet;  
625     }
626     
627    
628    
629     private static boolean isEnvEntryType (Class type)
630     {
631         boolean result = false;
632         for (int i=0;i<__envEntryTypes.length && !result;i++)
633         {
634             result = (type.equals(__envEntryTypes[i]));
635         }
636         return result;
637     }
638     
639     protected static String normalizePattern(String p)
640     {
641         if (p!=null && p.length()>0 && !p.startsWith("/") && !p.startsWith("*"))
642             return "/"+p;
643         return p;
644     }
645 }