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 }