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