View Javadoc

1   //========================================================================
2   //$Id: AnnotationCollection.java 3680 2008-09-21 10:37:13Z janb $
3   //Copyright 2006 Mort Bay Consulting Pty. Ltd.
4   //------------------------------------------------------------------------
5   //Licensed under the Apache License, Version 2.0 (the "License");
6   //you may not use this file except in compliance with the License.
7   //You may obtain a copy of the License at 
8   //http://www.apache.org/licenses/LICENSE-2.0
9   //Unless required by applicable law or agreed to in writing, software
10  //distributed under the License is distributed on an "AS IS" BASIS,
11  //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  //See the License for the specific language governing permissions and
13  //limitations under the License.
14  //========================================================================
15  
16  package org.mortbay.jetty.annotations;
17  
18  
19  import java.lang.reflect.Field;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.util.ArrayList;
23  import java.util.List;
24  
25  import javax.annotation.Resource;
26  import javax.annotation.PostConstruct;
27  import javax.annotation.PreDestroy;
28  import javax.annotation.Resources;
29  import javax.annotation.security.RunAs;
30  import javax.naming.InitialContext;
31  import javax.naming.NameNotFoundException;
32  import javax.naming.NamingException;
33  import javax.servlet.Servlet;
34  import javax.transaction.UserTransaction;
35  
36  import org.mortbay.jetty.plus.annotation.Injection;
37  import org.mortbay.jetty.plus.annotation.InjectionCollection;
38  import org.mortbay.jetty.plus.annotation.LifeCycleCallbackCollection;
39  import org.mortbay.jetty.plus.annotation.PostConstructCallback;
40  import org.mortbay.jetty.plus.annotation.PreDestroyCallback;
41  import org.mortbay.jetty.plus.annotation.RunAsCollection;
42  import org.mortbay.jetty.plus.naming.EnvEntry;
43  import org.mortbay.jetty.plus.naming.Transaction;
44  import org.mortbay.jetty.servlet.Holder;
45  import org.mortbay.jetty.servlet.ServletHolder;
46  import org.mortbay.jetty.webapp.WebAppContext;
47  import org.mortbay.log.Log;
48  import org.mortbay.util.IntrospectionUtil;
49  import org.mortbay.util.Loader;
50  
51  
52  
53  /**
54   * AnnotationCollection
55   * 
56   * An AnnotationCollection represents all of the annotated classes, methods and fields in the
57   * inheritance hierarchy for a class. NOTE that methods and fields in this collection are NOT
58   * just the ones that are inherited by the class, but represent ALL annotations that must be
59   * processed for a single instance of a given class.
60   * 
61   * The class to which this collection pertains is obtained by calling
62   * getTargetClass().
63   * 
64   * Using the list of annotated classes, methods and fields, the collection will generate
65   * the appropriate JNDI entries and the appropriate Injection and LifeCycleCallback objects
66   * to be later applied to instances of the getTargetClass().
67   */
68  public class AnnotationCollection
69  {
70      private WebAppContext _webApp; //the webapp
71      private Class _targetClass; //the most derived class to which this collection pertains
72      private List _methods = new ArrayList(); //list of methods relating to the _targetClass which have annotations
73      private List _fields = new ArrayList(); //list of fields relating to the _targetClass which have annotations
74      private List _classes = new ArrayList();//list of classes in the inheritance hierarchy that have annotations
75      private static Class[] __envEntryTypes = 
76          new Class[] {String.class, Character.class, Integer.class, Boolean.class, Double.class, Byte.class, Short.class, Long.class, Float.class};
77     
78      
79      public void setWebAppContext(WebAppContext webApp)
80      {
81          _webApp=webApp;
82      }
83    
84      public WebAppContext getWebAppContext()
85      {
86          return _webApp;
87      }
88      
89      /**
90       * Get the class which is the subject of these annotations
91       * @return the clazz
92       */
93      public Class getTargetClass()
94      {
95          return _targetClass;
96      }
97      
98      /** 
99       * Set the class to which this collection pertains
100      * @param clazz the clazz to set
101      */
102     public void setTargetClass(Class clazz)
103     {
104         _targetClass=clazz;
105     }
106     
107     
108     public void addClass (Class clazz)
109     {
110         if (clazz.getDeclaredAnnotations().length==0)
111             return;
112         _classes.add(clazz);
113     }
114     
115     public void addMethod (Method method)
116     {
117         if (method.getDeclaredAnnotations().length==0)
118             return;
119        _methods.add(method);
120     }
121     
122     public void addField(Field field)
123     {
124         if (field.getDeclaredAnnotations().length==0)
125             return;
126         _fields.add(field);
127     }
128     
129     public List getClasses()
130     {
131         return _classes;
132     }
133     public List getMethods ()
134     {
135         return _methods;
136     }
137     
138     
139     public List getFields()
140     {
141         return _fields;
142     }
143     
144     
145     
146     public void processRunAsAnnotations (RunAsCollection runAsCollection)
147     {
148         for (int i=0; i<_classes.size();i++)
149         {
150             Class clazz = (Class)_classes.get(i);
151 
152             //if this implements javax.servlet.Servlet check for run-as
153             if (Servlet.class.isAssignableFrom(clazz))
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.mortbay.jetty.plus.annotation.RunAs ra = new org.mortbay.jetty.plus.annotation.RunAs();
162                         ra.setTargetClass(clazz);
163                         ra.setRoleName(role);
164                         runAsCollection.add(ra);
165                     }
166                 }
167             }
168         } 
169     }
170     
171     
172     
173     /**
174      * Process @Resource annotations at the class, method and field level.
175      * @return
176      */
177     public InjectionCollection processResourceAnnotations(InjectionCollection injections)
178     {      
179         processClassResourceAnnotations();
180         processMethodResourceAnnotations(injections);
181         processFieldResourceAnnotations(injections);
182         
183         return injections;
184     }
185   
186   
187     /**
188      * Process @PostConstruct and @PreDestroy annotations.
189      * @return
190      */
191     public LifeCycleCallbackCollection processLifeCycleCallbackAnnotations(LifeCycleCallbackCollection callbacks)
192     {
193         processPostConstructAnnotations(callbacks);
194         processPreDestroyAnnotations(callbacks);
195         return callbacks;
196     }
197     
198     
199     
200     
201     /**
202      * Process @Resources annotation on classes
203      */
204     public void processResourcesAnnotations ()
205     {        
206         for (int i=0; i<_classes.size();i++)
207         {
208             Class clazz = (Class)_classes.get(i);
209             Resources resources = (Resources)clazz.getAnnotation(Resources.class);
210             if (resources != null)
211             {
212                 Resource[] resArray = resources.value();
213                 if (resArray==null||resArray.length==0)
214                     continue;
215 
216                 for (int j=0;j<resArray.length;j++)
217                 {
218 
219                     String name = resArray[j].name();
220                     String mappedName = resArray[j].mappedName();
221                     Resource.AuthenticationType auth = resArray[j].authenticationType();
222                     Class type = resArray[j].type();
223                     boolean shareable = resArray[j].shareable();
224 
225                     if (name==null || name.trim().equals(""))
226                         throw new IllegalStateException ("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");
227 
228                     try
229                     {
230                         //TODO don't ignore the shareable, auth etc etc
231 
232                            if (!org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp, name, mappedName))
233                                if (!org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp.getServer(), name, mappedName))
234                                    throw new IllegalStateException("No resource bound at "+(mappedName==null?name:mappedName));
235                     }
236                     catch (NamingException e)
237                     {
238                         throw new IllegalStateException(e);
239                     }
240                 }
241             }
242         } 
243     }
244     
245     
246     /**
247      *  Class level Resource annotations declare a name in the
248      *  environment that will be looked up at runtime. They do
249      *  not specify an injection.
250      */
251     private void processClassResourceAnnotations ()
252     {
253         for (int i=0; i<_classes.size();i++)
254         {
255             Class clazz = (Class)_classes.get(i);
256             Resource resource = (Resource)clazz.getAnnotation(Resource.class);
257             if (resource != null)
258             {
259                String name = resource.name();
260                String mappedName = resource.mappedName();
261                Resource.AuthenticationType auth = resource.authenticationType();
262                Class type = resource.type();
263                boolean shareable = resource.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                
268                try
269                {
270                    //TODO don't ignore the shareable, auth etc etc
271                    if (!org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp, name,mappedName))
272                        if (!org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp.getServer(), name,mappedName))
273                            throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
274                }
275                catch (NamingException e)
276                {
277                    throw new IllegalStateException(e);
278                }
279             }
280         }
281     }
282     
283     /**
284      * Process a Resource annotation on the Methods.
285      * 
286      * This will generate a JNDI entry, and an Injection to be
287      * processed when an instance of the class is created.
288      * @param injections
289      */
290     private void processMethodResourceAnnotations(InjectionCollection webXmlInjections)
291     {
292         //Get the method level Resource annotations        
293         for (int i=0;i<_methods.size();i++)
294         {
295             Method m = (Method)_methods.get(i);
296             Resource resource = (Resource)m.getAnnotation(Resource.class);
297             if (resource != null)
298             {
299                 //JavaEE Spec 5.2.3: Method cannot be static
300                 if (Modifier.isStatic(m.getModifiers()))
301                     throw new IllegalStateException(m+" cannot be static");
302                 
303                 
304                 // Check it is a valid javabean 
305                 if (!IntrospectionUtil.isJavaBeanCompliantSetter(m))
306                     throw new IllegalStateException(m+" is not a java bean compliant setter method");
307      
308                 //allow default name to be overridden
309                 String name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): defaultResourceNameFromMethod(m));
310                 //get the mappedName if there is one
311                 String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);       
312                 Class type = m.getParameterTypes()[0];
313                 //get other parts that can be specified in @Resource
314                 Resource.AuthenticationType auth = resource.authenticationType();
315                 boolean shareable = resource.shareable();
316 
317                 //if @Resource specifies a type, check it is compatible with setter param
318                 if ((resource.type() != null) 
319                         && 
320                         !resource.type().equals(Object.class)
321                         &&
322                         (!IntrospectionUtil.isTypeCompatible(type, resource.type(), false)))
323                     throw new IllegalStateException("@Resource incompatible type="+resource.type()+ " with method param="+type+ " for "+m);
324                
325                 //check if an injection has already been setup for this target by web.xml
326                 Injection webXmlInjection = webXmlInjections.getInjection(getTargetClass(), m);
327                 
328                 if (webXmlInjection == null)
329                 {
330                     try
331                     {
332                         //try binding name to environment
333                         //try the webapp's environment first
334                         boolean bound = org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp, name, mappedName);
335                         
336                         //try the server's environment
337                         if (!bound)
338                             bound = org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp.getServer(), name, mappedName);
339                         
340                         //try the jvm's environment
341                         if (!bound)
342                             bound = org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(null, name, mappedName);
343                         
344                         //TODO if it is an env-entry from web.xml it can be injected, in which case there will be no
345                         //NamingEntry, just a value bound in java:comp/env
346                         if (!bound)
347                         {
348                             try
349                             {
350                                 InitialContext ic = new InitialContext();
351                                 String nameInEnvironment = (mappedName!=null?mappedName:name);
352                                 ic.lookup("java:comp/env/"+nameInEnvironment);                               
353                                 bound = true;
354                             }
355                             catch (NameNotFoundException e)
356                             {
357                                 bound = false;
358                             }
359                         }
360                         
361                         if (bound)
362                         {
363                             Log.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
364                             //   Make the Injection for it
365                             Injection injection = new Injection();
366                             injection.setTargetClass(getTargetClass());
367                             injection.setJndiName(name);
368                             injection.setMappingName(mappedName);
369                             injection.setTarget(m);
370                             webXmlInjections.add(injection);
371                         }
372                         else if (!isEnvEntryType(type))
373                         {
374 
375                             //if this is an env-entry type resource and there is no value bound for it, it isn't
376                             //an error, it just means that perhaps the code will use a default value instead
377                             // JavaEE Spec. sec 5.4.1.3   
378                             throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
379                         }
380 
381                     }
382                     catch (NamingException e)
383                     {  
384                       
385                         throw new IllegalStateException(e);
386                     }
387                 }
388                 else
389                 {
390                     //if an injection is already set up for this name, then the types must be compatible
391                     //JavaEE spec sec 5.2.4
392                     try
393                     {
394                          Object value = webXmlInjection.lookupInjectedValue();
395                          if (!IntrospectionUtil.isTypeCompatible(type, value.getClass(), false))
396                              throw new IllegalStateException("Type of field="+type+" is not compatible with Resource type="+value.getClass());
397                     }
398                     catch (NamingException e)
399                     {
400                         throw new IllegalStateException(e);
401                     }
402                 }
403             }
404         }
405     }
406     
407     
408     /**
409      * Process @Resource annotation for a Field. These will both set up a
410      * JNDI entry and generate an Injection. Or they can be the equivalent
411      * of env-entries with default values
412      * 
413      * @param injections
414      */
415     private void processFieldResourceAnnotations (InjectionCollection webXmlInjections)
416     {
417         for (int i=0;i<_fields.size();i++)
418         {
419             Field f = (Field)_fields.get(i);
420             Resource resource = (Resource)f.getAnnotation(Resource.class);
421             if (resource != null)
422             {
423                 //JavaEE Spec 5.2.3: Field cannot be static
424                 if (Modifier.isStatic(f.getModifiers()))
425                     throw new IllegalStateException(f+" cannot be static");
426                 
427                 //JavaEE Spec 5.2.3: Field cannot be final
428                 if (Modifier.isFinal(f.getModifiers()))
429                     throw new IllegalStateException(f+" cannot be final");
430                 
431                 //work out default name
432                 String name = f.getDeclaringClass().getCanonicalName()+"/"+f.getName();
433                 //allow @Resource name= to override the field name
434                 name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
435                 
436                 //get the type of the Field
437                 Class type = f.getType();
438                 //if @Resource specifies a type, check it is compatible with field type
439                 if ((resource.type() != null)
440                         && 
441                         !resource.type().equals(Object.class)
442                         &&
443                         (!IntrospectionUtil.isTypeCompatible(type, resource.type(), false)))
444                     throw new IllegalStateException("@Resource incompatible type="+resource.type()+ " with field type ="+f.getType());
445                 
446                 //get the mappedName if there is one
447                 String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
448                 //get other parts that can be specified in @Resource
449                 Resource.AuthenticationType auth = resource.authenticationType();
450                 boolean shareable = resource.shareable();
451             
452                 //check if an injection has already been setup for this target by web.xml
453                 Injection webXmlInjection = webXmlInjections.getInjection(getTargetClass(), f);
454                 if (webXmlInjection == null)
455                 {
456                     try
457                     {
458                         boolean bound = org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp, name, mappedName);
459                         if (!bound)
460                             bound = org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(_webApp.getServer(), name, mappedName);
461                         if (!bound)
462                             bound =  org.mortbay.jetty.plus.naming.NamingEntryUtil.bindToENC(null, name, mappedName); 
463                         if (!bound)
464                         {
465                             //see if there is an env-entry value been bound from web.xml
466                             try
467                             {
468                                 InitialContext ic = new InitialContext();
469                                 String nameInEnvironment = (mappedName!=null?mappedName:name);
470                                 ic.lookup("java:comp/env/"+nameInEnvironment);                               
471                                 bound = true;
472                             }
473                             catch (NameNotFoundException e)
474                             {
475                                 bound = false;
476                             }
477                         }
478                         //Check there is a JNDI entry for this annotation 
479                         if (bound)
480                         { 
481                             Log.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
482                             //   Make the Injection for it if the binding succeeded
483                             Injection injection = new Injection();
484                             injection.setTargetClass(getTargetClass());
485                             injection.setJndiName(name);
486                             injection.setMappingName(mappedName);
487                             injection.setTarget(f);
488                             webXmlInjections.add(injection); 
489                         }
490                         else if (!isEnvEntryType(type))
491                         {
492                             //if this is an env-entry type resource and there is no value bound for it, it isn't
493                             //an error, it just means that perhaps the code will use a default value instead
494                             // JavaEE Spec. sec 5.4.1.3
495 
496                             throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
497                         }
498                     }
499                     catch (NamingException e)
500                     {
501                         throw new IllegalStateException(e);
502                     }
503                 }
504                 else
505                 {
506                     //if an injection is already set up for this name, then the types must be compatible
507                     //JavaEE spec sec 5.2.4
508                     try
509                     {
510                          Object value = webXmlInjection.lookupInjectedValue();
511                          if (!IntrospectionUtil.isTypeCompatible(type, value.getClass(), false))
512                              throw new IllegalStateException("Type of field="+type+" is not compatible with Resource type="+value.getClass());
513                     }
514                     catch (NamingException e)
515                     {
516                         throw new IllegalStateException(e);
517                     }
518                 }
519             }
520         }  
521     }
522     
523     
524     /**
525      * Find @PostConstruct annotations.
526      * 
527      * The spec says (Common Annotations Sec 2.5) that only ONE method
528      * may be adorned with the PostConstruct annotation, however this does
529      * not clarify how this works with inheritance.
530      * 
531      * TODO work out what to do with inherited PostConstruct annotations
532      * 
533      * @param callbacks
534      */
535     private void processPostConstructAnnotations (LifeCycleCallbackCollection callbacks)
536     {
537         //      TODO: check that the same class does not have more than one
538         for (int i=0; i<_methods.size(); i++)
539         {
540             Method m = (Method)_methods.get(i);
541             if (m.isAnnotationPresent(PostConstruct.class))
542             {
543                 if (m.getParameterTypes().length != 0)
544                     throw new IllegalStateException(m+" has parameters");
545                 if (m.getReturnType() != Void.TYPE)
546                     throw new IllegalStateException(m+" is not void");
547                 if (m.getExceptionTypes().length != 0)
548                     throw new IllegalStateException(m+" throws checked exceptions");
549                 if (Modifier.isStatic(m.getModifiers()))
550                     throw new IllegalStateException(m+" is static");
551                 
552                 
553                 PostConstructCallback callback = new PostConstructCallback();
554                 callback.setTargetClass(getTargetClass());
555                 callback.setTarget(m);
556                 callbacks.add(callback);
557             }
558         }
559     }
560     
561     /**
562      * Find @PreDestroy annotations.
563      * 
564      * The spec says (Common Annotations Sec 2.5) that only ONE method
565      * may be adorned with the PreDestroy annotation, however this does
566      * not clarify how this works with inheritance.
567      * 
568      * TODO work out what to do with inherited PreDestroy annotations
569      * @param callbacks
570      */
571     private void processPreDestroyAnnotations (LifeCycleCallbackCollection callbacks)
572     {
573         //TODO: check that the same class does not have more than one
574         
575         for (int i=0; i<_methods.size(); i++)
576         {
577             Method m = (Method)_methods.get(i);
578             if (m.isAnnotationPresent(PreDestroy.class))
579             {
580                 if (m.getParameterTypes().length != 0)
581                     throw new IllegalStateException(m+" has parameters");
582                 if (m.getReturnType() != Void.TYPE)
583                     throw new IllegalStateException(m+" is not void");
584                 if (m.getExceptionTypes().length != 0)
585                     throw new IllegalStateException(m+" throws checked exceptions");
586                 if (Modifier.isStatic(m.getModifiers()))
587                     throw new IllegalStateException(m+" is static");
588                 
589                 PreDestroyCallback callback = new PreDestroyCallback(); 
590                 callback.setTargetClass(getTargetClass());
591                 callback.setTarget(m);
592                 callbacks.add(callback);
593             }
594         }
595     }
596     
597  
598     private static boolean isEnvEntryType (Class type)
599     {
600         boolean result = false;
601         for (int i=0;i<__envEntryTypes.length && !result;i++)
602         {
603             result = (type.equals(__envEntryTypes[i]));
604         }
605         return result;
606     }
607     
608     private static Class getNamingEntryType (Class type)
609     {
610         if (type==null)
611             return null;
612         
613         if (isEnvEntryType(type))
614             return EnvEntry.class;
615         
616         if (type.getName().equals("javax.transaction.UserTransaction"))
617                 return Transaction.class;
618         else
619             return org.mortbay.jetty.plus.naming.Resource.class;
620     }
621     
622     private String defaultResourceNameFromMethod (Method m)
623     {
624         String name = m.getName().substring(3);
625         name = name.substring(0,1).toLowerCase()+name.substring(1);
626         return m.getDeclaringClass().getCanonicalName()+"/"+name;
627     }
628 
629 }