View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.annotations;
20  
21  import java.lang.reflect.Field;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Modifier;
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.Locale;
27  
28  import javax.annotation.Resource;
29  import javax.naming.InitialContext;
30  import javax.naming.NameNotFoundException;
31  import javax.naming.NamingException;
32  
33  import org.eclipse.jetty.annotations.AnnotationIntrospector.AbstractIntrospectableAnnotationHandler;
34  import org.eclipse.jetty.plus.annotation.Injection;
35  import org.eclipse.jetty.plus.annotation.InjectionCollection;
36  import org.eclipse.jetty.util.log.Log;
37  import org.eclipse.jetty.util.log.Logger;
38  import org.eclipse.jetty.webapp.MetaData;
39  import org.eclipse.jetty.webapp.WebAppContext;
40  
41  public class ResourceAnnotationHandler extends AbstractIntrospectableAnnotationHandler
42  {
43      private static final Logger LOG = Log.getLogger(ResourceAnnotationHandler.class);
44      
45      protected static final List<Class<?>> ENV_ENTRY_TYPES = 
46              Arrays.asList(new Class[] {String.class, Character.class, Integer.class, Boolean.class, Double.class, Byte.class, Short.class, Long.class, Float.class});
47              
48  
49      protected WebAppContext _context;
50  
51  
52      public ResourceAnnotationHandler (WebAppContext wac)
53      {
54          super(true);
55          _context = wac;
56      }
57  
58  
59      /**
60       *  Class level Resource annotations declare a name in the
61       *  environment that will be looked up at runtime. They do
62       *  not specify an injection.
63       */
64      public void doHandle(Class<?> clazz)
65      {
66          if (supportsResourceInjection(clazz))
67          {
68              handleClass(clazz);
69  
70              Method[] methods = clazz.getDeclaredMethods();
71              for (int i=0; i<methods.length; i++)
72                  handleMethod(clazz, methods[i]);
73              Field[] fields = clazz.getDeclaredFields();
74              //For each field, get all of it's annotations
75              for (int i=0; i<fields.length; i++)
76                  handleField(clazz, fields[i]);
77          }
78      }
79  
80       public void handleClass (Class<?> clazz)
81       {
82           Resource resource = (Resource)clazz.getAnnotation(Resource.class);
83           if (resource != null)
84           {
85               String name = resource.name();
86               String mappedName = resource.mappedName();
87  
88               if (name==null || name.trim().equals(""))
89                   throw new IllegalStateException ("Class level Resource annotations must contain a name (Common Annotations Spec Section 2.3)");
90  
91               try
92               {
93                   if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context, name,mappedName))
94                       if (!org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context.getServer(), name,mappedName))
95                           throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
96               }
97               catch (NamingException e)
98               {
99                   LOG.warn(e);
100              }
101          }
102     }
103 
104     public void handleField(Class<?> clazz, Field field)
105     {
106         Resource resource = (Resource)field.getAnnotation(Resource.class);
107         if (resource != null)
108         {
109             //JavaEE Spec 5.2.3: Field cannot be static
110             if (Modifier.isStatic(field.getModifiers()))
111             {
112                 LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+field.getName()+": cannot be static");
113                 return;
114             }
115 
116             //JavaEE Spec 5.2.3: Field cannot be final
117             if (Modifier.isFinal(field.getModifiers()))
118             {
119                 LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+field.getName()+": cannot be final");
120                 return;
121             }
122 
123             //work out default name
124             String name = clazz.getCanonicalName()+"/"+field.getName();
125 
126             //allow @Resource name= to override the field name
127             name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
128             String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
129             //get the type of the Field
130             Class<?> type = field.getType();
131 
132             //Servlet Spec 3.0 p. 76
133             //If a descriptor has specified at least 1 injection target for this
134             //resource, then it overrides this annotation
135             MetaData metaData = _context.getMetaData();
136             if (metaData.getOriginDescriptor("resource-ref."+name+".injection") != null)
137             {
138                 //at least 1 injection was specified for this resource by a descriptor, so
139                 //it overrides this annotation
140                 return;
141             }
142 
143             //No injections for this resource in any descriptors, so we can add it
144             //Does the injection already exist?
145             InjectionCollection injections = (InjectionCollection)_context.getAttribute(InjectionCollection.INJECTION_COLLECTION);
146             if (injections == null)
147             {
148                 injections = new InjectionCollection();
149                 _context.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
150             }
151             Injection injection = injections.getInjection(name, clazz, field);
152             if (injection == null)
153             {
154                 //No injection has been specified, add it
155                 try
156                 {
157                     boolean bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context, name, mappedName);
158                     if (!bound)
159                         bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName);
160                     if (!bound)
161                         bound =  org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(null, name, mappedName);
162                     if (!bound)
163                     {
164                         //see if there is an env-entry value been bound
165                         try
166                         {
167                             InitialContext ic = new InitialContext();
168                             String nameInEnvironment = (mappedName!=null?mappedName:name);
169                             ic.lookup("java:comp/env/"+nameInEnvironment);
170                             bound = true;
171                         }
172                         catch (NameNotFoundException e)
173                         {
174                             bound = false;
175                         }
176                     }
177                     //Check there is a JNDI entry for this annotation
178                     if (bound)
179                     {
180                         LOG.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
181                         //   Make the Injection for it if the binding succeeded
182                         injection = new Injection();
183                         injection.setTarget(clazz, field, type);
184                         injection.setJndiName(name);
185                         injection.setMappingName(mappedName);
186                         injections.add(injection);
187 
188                         //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination
189                         metaData.setOrigin("resource-ref."+name+".injection",resource,clazz);
190                     }
191                     else if (!isEnvEntryType(type))
192                     {
193                         //if this is an env-entry type resource and there is no value bound for it, it isn't
194                         //an error, it just means that perhaps the code will use a default value instead
195                         // JavaEE Spec. sec 5.4.1.3
196 
197                         throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
198                     }
199                 }
200                 catch (NamingException e)
201                 {
202                     //if this is an env-entry type resource and there is no value bound for it, it isn't
203                     //an error, it just means that perhaps the code will use a default value instead
204                     // JavaEE Spec. sec 5.4.1.3
205                     if (!isEnvEntryType(type))
206                         throw new IllegalStateException(e);
207                 }
208             }
209         }
210     }
211 
212 
213     /**
214      * Process a Resource annotation on a Method.
215      * <p>
216      * This will generate a JNDI entry, and an Injection to be
217      * processed when an instance of the class is created.
218      * 
219      * @param clazz the class to process 
220      * @param method the method to process
221      */
222     public void handleMethod(Class<?> clazz, Method method)
223     {
224 
225         Resource resource = (Resource)method.getAnnotation(Resource.class);
226         if (resource != null)
227         {
228             /*
229              * Commons Annotations Spec 2.3
230              * " The Resource annotation is used to declare a reference to a resource.
231              *   It can be specified on a class, methods or on fields. When the
232              *   annotation is applied on a field or method, the container will
233              *   inject an instance of the requested resource into the application
234              *   when the application is initialized... Even though this annotation
235              *   is not marked Inherited, if used all superclasses MUST be examined
236              *   to discover all uses of this annotation. All such annotation instances
237              *   specify resources that are needed by the application. Note that this
238              *   annotation may appear on private fields and methods of the superclasses.
239              *   Injection of the declared resources needs to happen in these cases as
240              *   well, even if a method with such an annotation is overridden by a subclass."
241              *
242              *  Which IMHO, put more succinctly means "If you find a @Resource on any method
243              *  or field, inject it!".
244              */
245             //JavaEE Spec 5.2.3: Method cannot be static
246             if (Modifier.isStatic(method.getModifiers()))
247             {
248                 LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": cannot be static");
249                 return;
250             }
251 
252             // Check it is a valid javabean: must be void return type, the name must start with "set" and it must have
253             // only 1 parameter
254             if (!method.getName().startsWith("set"))
255             {
256                 LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": invalid java bean, does not start with 'set'");
257                 return;
258             }
259 
260             if (method.getParameterTypes().length != 1)
261             {
262                 LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": invalid java bean, not single argument to method");
263                 return;
264             }
265 
266             if (Void.TYPE != method.getReturnType())
267             {
268                 LOG.warn("Skipping Resource annotation on "+clazz.getName()+"."+method.getName()+": invalid java bean, not void");
269                 return;
270             }
271 
272 
273             //default name is the javabean property name
274             String name = method.getName().substring(3);
275             name = name.substring(0,1).toLowerCase(Locale.ENGLISH)+name.substring(1);
276             name = clazz.getCanonicalName()+"/"+name;
277 
278             name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name);
279             String mappedName = (resource.mappedName()!=null && !resource.mappedName().trim().equals("")?resource.mappedName():null);
280             Class<?> paramType = method.getParameterTypes()[0];
281 
282             Class<?> resourceType = resource.type();
283 
284             //Servlet Spec 3.0 p. 76
285             //If a descriptor has specified at least 1 injection target for this
286             //resource, then it overrides this annotation
287             MetaData metaData = _context.getMetaData();
288             if (metaData.getOriginDescriptor("resource-ref."+name+".injection") != null)
289             {
290                 //at least 1 injection was specified for this resource by a descriptor, so
291                 //it overrides this annotation
292                 return;
293             }
294 
295             //check if an injection has already been setup for this target by web.xml
296             InjectionCollection injections = (InjectionCollection)_context.getAttribute(InjectionCollection.INJECTION_COLLECTION);
297             if (injections == null)
298             {
299                 injections = new InjectionCollection();
300                 _context.setAttribute(InjectionCollection.INJECTION_COLLECTION, injections);
301             }
302             Injection injection = injections.getInjection(name, clazz, method, paramType);
303             if (injection == null)
304             {
305                 try
306                 {
307                     //try binding name to environment
308                     //try the webapp's environment first
309                     boolean bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context, name, mappedName);
310 
311                     //try the server's environment
312                     if (!bound)
313                         bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(_context.getServer(), name, mappedName);
314 
315                     //try the jvm's environment
316                     if (!bound)
317                         bound = org.eclipse.jetty.plus.jndi.NamingEntryUtil.bindToENC(null, name, mappedName);
318 
319                     //TODO if it is an env-entry from web.xml it can be injected, in which case there will be no
320                     //NamingEntry, just a value bound in java:comp/env
321                     if (!bound)
322                     {
323                         try
324                         {
325                             InitialContext ic = new InitialContext();
326                             String nameInEnvironment = (mappedName!=null?mappedName:name);
327                             ic.lookup("java:comp/env/"+nameInEnvironment);
328                             bound = true;
329                         }
330                         catch (NameNotFoundException e)
331                         {
332                             bound = false;
333                         }
334                     }
335 
336                     if (bound)
337                     {
338                         LOG.debug("Bound "+(mappedName==null?name:mappedName) + " as "+ name);
339                         //   Make the Injection for it
340                         injection = new Injection();
341                         injection.setTarget(clazz, method,paramType,resourceType);
342                         injection.setJndiName(name);
343                         injection.setMappingName(mappedName);
344                         injections.add(injection);
345                         //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination
346                         metaData.setOrigin("resource-ref."+name+".injection",resource,clazz);
347                     }
348                     else if (!isEnvEntryType(paramType))
349                     {
350 
351                         //if this is an env-entry type resource and there is no value bound for it, it isn't
352                         //an error, it just means that perhaps the code will use a default value instead
353                         // JavaEE Spec. sec 5.4.1.3
354                         throw new IllegalStateException("No resource at "+(mappedName==null?name:mappedName));
355                     }
356                 }
357                 catch (NamingException e)
358                 {
359                     //if this is an env-entry type resource and there is no value bound for it, it isn't
360                     //an error, it just means that perhaps the code will use a default value instead
361                     // JavaEE Spec. sec 5.4.1.3
362                     if (!isEnvEntryType(paramType))
363                         throw new IllegalStateException(e);
364                 }
365             }
366 
367         }
368     }
369     
370     /**
371      * Check if the given Class is one that the specification allows to have a Resource annotation.
372      * 
373      * @param c the class
374      * @return true if Resource annotation permitted, false otherwise
375      */
376     public boolean supportsResourceInjection (Class<?> c)
377     {
378         if (javax.servlet.Servlet.class.isAssignableFrom(c) ||
379                 javax.servlet.Filter.class.isAssignableFrom(c) || 
380                 javax.servlet.ServletContextListener.class.isAssignableFrom(c) ||
381                 javax.servlet.ServletContextAttributeListener.class.isAssignableFrom(c) ||
382                 javax.servlet.ServletRequestListener.class.isAssignableFrom(c) ||
383                 javax.servlet.ServletRequestAttributeListener.class.isAssignableFrom(c) ||
384                 javax.servlet.http.HttpSessionListener.class.isAssignableFrom(c) ||
385                 javax.servlet.http.HttpSessionAttributeListener.class.isAssignableFrom(c) ||
386                 javax.servlet.http.HttpSessionIdListener.class.isAssignableFrom(c) ||
387                 javax.servlet.AsyncListener.class.isAssignableFrom(c) ||
388                 javax.servlet.http.HttpUpgradeHandler.class.isAssignableFrom(c))
389             return true;
390         
391         return false;
392     }
393     
394     
395     /**
396      * Check if the class is one of the basic java types permitted as 
397      * env-entries.
398      * @param clazz the class to check
399      * @return true if class is permitted by the spec to be an env-entry value
400      */
401     public boolean isEnvEntryType (Class<?> clazz)
402     {
403         return ENV_ENTRY_TYPES.contains(clazz);
404     }
405 }