View Javadoc

1   /**
2    * Project MI+
3    *
4    * Copyright (c) 2013, Syed Muhammad Humayun - smhumayun@gmail.com
5    *
6    * This project includes software developed by Syed Muhammad Humayun - smhumayun@gmail.com
7    * http://www.smhumayun.com
8    *
9    * Licensed under the Apache License, Version 2.0 (the
10   * "License"); you may not use this file except in compliance
11   * with the License. You may obtain a copy of the License at:
12   *
13   * http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing,
16   * software distributed under the License is distributed on
17   * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18   * KIND, either express or implied. See the License for the
19   * specific language governing permissions and limitations
20   * under the License.
21   */
22  package com.smhumayun.mi_plus;
23  
24  import com.smhumayun.mi_plus.impl.MIMethodResolverImpl;
25  import com.smhumayun.mi_plus.impl.MIObjectInMemoryCache;
26  import eu.infomas.annotation.AnnotationDetector;
27  
28  import java.lang.annotation.Annotation;
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.Modifier;
31  import java.lang.reflect.Proxy;
32  import java.util.LinkedHashMap;
33  import java.util.List;
34  import java.util.logging.Level;
35  import java.util.logging.Logger;
36  
37  /**
38   * Factory for creating objects that supports MI
39   *
40   * @author smhumayun
41   * @since 1.0
42   */
43  public final class MIFactory {
44  
45      /**
46       * Cache of container and composed objects
47       *  - Defaults to {@link com.smhumayun.mi_plus.impl.MIObjectInMemoryCache}
48       */
49      private MIObjectCache miObjectCache;
50  
51      /**
52       * Method resolver
53       *  - Defaults to {@link com.smhumayun.mi_plus.impl.MIMethodResolverImpl}
54       */
55      private MIMethodResolver miMethodResolver;
56  
57      /**
58       * Logger {@link java.util.logging.Logger}
59       */
60      private Logger logger = Logger.getLogger(MIFactory.class.getName());
61  
62      /**
63       * Lock Object for MI Container Cache
64       */
65      private final Object miContainerProxyCacheLockObj = new Object();
66  
67      /**
68       * Lock Object for Composed Objects Cache
69       */
70      private final Object composedObjectsCacheLockObj = new Object();
71  
72      /**
73       * Constructor which initializes this factory instance with a default {@link com.smhumayun.mi_plus.impl.MIObjectInMemoryCache} and a default
74       * {@link com.smhumayun.mi_plus.impl.MIMethodResolverImpl}
75       *
76       * @throws MIException {@link MIException}
77       */
78      @SuppressWarnings("UnusedDeclaration")
79      public MIFactory() throws MIException {
80          this(new MIObjectInMemoryCache(), new MIMethodResolverImpl(), null);
81      }
82  
83      /**
84       * Constructor which initializes this factory instance with a default {@link MIObjectInMemoryCache} and a default
85       * {@link MIMethodResolverImpl}; and scans given <code>packagesToScan</code> to find and process classes annotated
86       * with {@link MISupport} annotation
87       *
88       * @param packagesToScan list of packages to scan for {@link MISupport} annotation
89       * @throws MIException {@link MIException}
90       */
91      @SuppressWarnings("UnusedDeclaration")
92      public MIFactory(List<String> packagesToScan) throws MIException {
93          this(new MIObjectInMemoryCache(), new MIMethodResolverImpl(), packagesToScan);
94      }
95  
96      /**
97       * Constructor which initializes this factory instance with given <code>miObjectCache</code> and a default
98       * {@link MIMethodResolverImpl}
99       *
100      * @param miObjectCache object cache implementation
101      * @throws MIException {@link MIException}
102      */
103     @SuppressWarnings("UnusedDeclaration")
104     public MIFactory(MIObjectCache miObjectCache) throws MIException {
105         this(miObjectCache, new MIMethodResolverImpl(), null);
106     }
107 
108     /**
109      * Constructor which initializes this factory instance with given <code>miObjectCache</code> and a default
110      * {@link MIMethodResolverImpl}; and scans given <code>packagesToScan</code> to find and process classes annotated
111      * with {@link MISupport} annotation
112      *
113      * @param miObjectCache object cache implementation
114      * @param packagesToScan list of packages to scan for {@link MISupport} annotation
115      * @throws MIException {@link MIException}
116      */
117     @SuppressWarnings("UnusedDeclaration")
118     public MIFactory(MIObjectCache miObjectCache, List<String> packagesToScan) throws MIException {
119         this(miObjectCache, new MIMethodResolverImpl(), packagesToScan);
120     }
121 
122     /**
123      * Constructor which initializes this factory instance with given <code>miMethodResolver</code> and a default
124      * {@link MIObjectInMemoryCache}
125      *
126      * @param miMethodResolver method resolver implementation
127      * @throws MIException {@link MIException}
128      */
129     @SuppressWarnings("UnusedDeclaration")
130     public MIFactory(MIMethodResolver miMethodResolver) throws MIException {
131         this(new MIObjectInMemoryCache(), miMethodResolver, null);
132     }
133 
134     /**
135      * Constructor which initializes this factory instance with given <code>miMethodResolver</code> and a default
136      * {@link MIObjectInMemoryCache}; and scans given <code>packagesToScan</code> to find and process classes annotated
137      * with {@link MISupport} annotation
138      *
139      * @param miMethodResolver method resolver implementation
140      * @param packagesToScan list of packages to scan for {@link MISupport} annotation
141      * @throws MIException {@link MIException}
142      */
143     @SuppressWarnings("UnusedDeclaration")
144     public MIFactory(MIMethodResolver miMethodResolver, List<String> packagesToScan) throws MIException {
145         this(new MIObjectInMemoryCache(), miMethodResolver, packagesToScan);
146     }
147 
148     /**
149      * Constructor which initializes this factory instance with given <code>miObjectCache</code> and
150      * <code>miMethodResolver</code> objects
151      *
152      * @param miObjectCache object cache implementation
153      * @param miMethodResolver method resolver implementation
154      * @throws MIException {@link MIException}
155      */
156     @SuppressWarnings("UnusedDeclaration")
157     public MIFactory(MIObjectCache miObjectCache, MIMethodResolver miMethodResolver) throws MIException {
158         this(miObjectCache, miMethodResolver, null);
159     }
160 
161     /**
162      * Constructor which initializes this factory instance with given <code>miObjectCache</code> and
163      * , <code>miMethodResolver</code> objects; and scans given <code>packagesToScan</code>
164      * to find and process classes annotated with {@link MISupport} annotation
165      *
166      * @param miObjectCache object cache implementation
167      * @param miMethodResolver method resolver implementation
168      * @param packagesToScan list of packages to scan for {@link MISupport} annotation
169      * @throws MIException {@link MIException}
170      */
171     public MIFactory(MIObjectCache miObjectCache, MIMethodResolver miMethodResolver, List<String> packagesToScan)
172             throws MIException {
173         setMiObjectCache(miObjectCache);
174         setMiMethodResolver(miMethodResolver);
175         logger.info(MIFactory.class.getSimpleName() + " of type " + this.getClass().getName() + " created!");
176         scanPackagesAndProcessAnnotatedClasses(packagesToScan);
177     }
178 
179     /**
180      * This method will scan given <code>packagesToScan</code> to find and process classes annotated with
181      * {@link MISupport} annotation
182      *
183      * @param packagesToScan list of packages to scan for {@link MISupport} annotation
184      * @throws MIException {@link MIException}
185      */
186     private void scanPackagesAndProcessAnnotatedClasses (List<String> packagesToScan) throws MIException
187     {
188         try
189         {
190             if(packagesToScan != null && packagesToScan.size() > 0)
191             {
192                 AnnotationDetector annotationDetector = new AnnotationDetector(new AnnotationDetector.TypeReporter() {
193 
194                     public void reportTypeAnnotation(Class<? extends Annotation> aClass, String className) {
195                         try {
196                             newInstance(Class.forName(className));
197                         } catch (ClassNotFoundException e) {
198                             throw new MIException(e.toString(), e);
199                         }
200                     }
201 
202                     public Class<? extends Annotation>[] annotations() {
203                         //noinspection unchecked
204                         return new Class[]{MISupport.class};
205                     }
206 
207                 });
208 
209                 //noinspection ToArrayCallWithZeroLengthArrayArgument
210                 annotationDetector.detect(packagesToScan.toArray(new String[]{}));
211             }
212         }
213         catch (Throwable t)
214         {
215             if(t instanceof MIException)
216                 throw (MIException) t;
217             throw new MIException(t);
218         }
219     }
220 
221     /**
222      * Factory method to create new instances of MI Container classes that are annotated with
223      * {@link com.smhumayun.mi_plus.MISupport} annotation
224      *
225      * @param clazz class to instantiate - MI Container class annotated with {@link MISupport} annotation
226      * @return an object that functionally supports MI
227      * @throws MIException {@link MIException}
228      */
229     @SuppressWarnings("ConstantConditions")
230     public Object newInstance(Class clazz) throws MIException {
231         try
232         {
233             logger.info("clazz = " + clazz.getName());
234             //get annotation
235             MISupport MISupport = getAnnotation(clazz);
236             logger.fine("scope = " + MISupport.scope().name());
237             //if singleton container
238             if(MISupport.scope().equals(MIObjectScope.SINGLETON_CONTAINER_SINGLETON_COMPOSED))
239             {
240                 //acquire lock for MI Container Proxy cache
241                 synchronized (miContainerProxyCacheLockObj)
242                 {
243                     //get MI Container Proxy from cache
244                     Object miContainerProxy = getMiObjectCache().getCachedMiContainerProxy(clazz);
245                     //if MI Container Proxy does not exists against this clazz
246                     if(miContainerProxy == null)
247                     {
248                         logger.fine("MI Container Proxy object not available in cache");
249                         //create new MI Container Proxy object containing new set of composed objects
250                         MIContainerSupport miContainerSupport = createContainerSupport(MISupport, clazz, null);
251                         //create a MI Container Proxy object and associate MI Container Support object as the invocation handler
252                         miContainerProxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, miContainerSupport);
253                         logger.finer("MI Container Proxy object created");
254                         //cache MI Container Proxy
255                         getMiObjectCache().cacheMiContainerProxy(clazz, miContainerProxy);
256                         logger.finer("MI Container Proxy object saved in cache");
257                     }
258                     else
259                         logger.fine("MI Container Proxy object found in cache");
260                     return miContainerProxy;
261                 }
262             }
263             else //if prototype container
264             {
265                 MIContainerSupport miContainerSupport;
266                 //if singleton composed objects
267                 if(MISupport.scope().equals(MIObjectScope.PROTOTYPE_CONTAINER_SINGLETON_COMPOSED))
268                 {
269                     logger.finer("get Composed objects from cache");
270                     //acquire lock of composed objects cache
271                     synchronized (composedObjectsCacheLockObj)
272                     {
273                         LinkedHashMap<Class, Object> composedObjects = getMiObjectCache().getCachedComposedObjects(clazz);
274                         //create new container object containing composed objects - create composed objects if
275                         // - Singleton Composed Objects are not available in cache - once created cache them as well
276                         miContainerSupport = createContainerSupport(MISupport, clazz, composedObjects);
277                     }
278                 }
279                 else
280                     //create new container object containing composed objects and create composed objects
281                     miContainerSupport = createContainerSupport(MISupport, clazz, null);
282                 //create a proxy object and associate MI container object as the invocation handler
283                 Object miContainerProxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, miContainerSupport);
284                 logger.finer("Proxy of container object created");
285                 return miContainerProxy;
286             }
287         }
288         catch(Exception e) {
289             if(e instanceof MIException)
290                 throw (MIException) e;
291             logger.log(Level.SEVERE, e.toString(), e);
292             throw new MIException(e.toString(), e);
293         }
294     }
295 
296     /**
297      * This method is responsible to:
298      *  - validate MI Container class - the one which is annotated with {@link MISupport} annotation
299      *  - create a new {@link MIContainerSupport} object using
300      *  - if <code>composedObjects</code> are provided compose them inside newly created MI Container Support object
301      *  - else, create new set of composed objects
302      *  - cache newly created composed objects if {@link MIObjectScope#PROTOTYPE_CONTAINER_SINGLETON_COMPOSED}
303      *  - finally return the newly created and configured MI Container Support object to the caller
304      *
305      * @param miSupport {@link MISupport} annotation
306      * @param clazz MI Container class which has the {@link MISupport} annotation
307      * @param composedObjects objects to be composed inside newly created container
308      * @return newly created and configured {@link MIContainerSupport} object
309      * @throws MIException {@link MIException}
310      * @throws IllegalAccessException
311      * @throws InstantiationException
312      */
313     private MIContainerSupport createContainerSupport(MISupport miSupport, Class clazz, LinkedHashMap<Class, Object> composedObjects)
314             throws MIException, IllegalAccessException, InstantiationException {
315         //validate and extract annotation and parent classes
316         validateMIClass(clazz);
317         //create new MI Container Support object
318         MIContainerSupport miContainerSupport = new MIContainerSupport(clazz, getMiMethodResolver());
319         logger.finer("new MI Container Support object created");
320         //if composed objects are not provided, create, compose and cache them
321         if(composedObjects == null) {
322             //iterate through available inheritance classes
323             for(Class clazz1 : miSupport.parentClasses()) {
324                 //create new instances against each of the inherited classes and compose them inside
325                 //MI Container Support object
326                 miContainerSupport.compose(clazz1.newInstance());
327                 logger.finer("new Composed object created - " + clazz1.getName());
328             }
329             //check if composed objects needs to be cached
330             if(miSupport.scope().equals(MIObjectScope.PROTOTYPE_CONTAINER_SINGLETON_COMPOSED))
331             {
332                 //cache composed objects
333                 getMiObjectCache().cacheComposedObjects(clazz, miContainerSupport.getComposedObjects());
334                 logger.finer("newly created Composed object saved in cache");
335             }
336         }
337         else //if composed objects are provided
338             //compose them inside container
339             miContainerSupport.setComposedObjects(composedObjects);
340         return miContainerSupport;
341     }
342 
343     /**
344      * Return {@link MISupport} annotation if present on given <code>clazz</code> else throw following exception
345      *
346      * @param clazz class to extract annotation from
347      * @return extracted {@link MISupport} annotation
348      * @throws MIException if {@link MISupport} is not present on the <code>clazz</code>
349      */
350     private MISupport getAnnotation (Class clazz) throws MIException {
351         //check for MISupport annotation
352         if(clazz.isAnnotationPresent(MISupport.class))
353             //if found return
354             //noinspection unchecked
355             return (MISupport) clazz.getAnnotation(MISupport.class);
356         //else throw exception if annotation is missing
357         throw new MIException("@MISupport annotation not found on class " + clazz.getName());
358     }
359 
360     /**
361      * Validate given MI Container class
362      *
363      * @param clazz MI Container class to validate
364      * @throws MIException {@link MIException}
365      */
366     private void validateMIClass (Class clazz) throws MIException
367     {
368         logger.fine("validating class " + clazz.getName());
369         //check cache, if it is already validated
370         if(getMiObjectCache().isValidated(clazz))
371         {
372             logger.fine("class already validated : " + clazz.getName());
373             return;
374         }
375         @SuppressWarnings("unchecked")
376         MISupport MISupport = getAnnotation(clazz);
377         //get parent classes information from annotation
378         Class[] parentClasses = MISupport.parentClasses();
379         //check if one or more inheritance classes are available
380         if(parentClasses != null && parentClasses.length > 0) {
381             logger.fine("parentClasses.length = " + parentClasses.length);
382             //iterate through available inheritance classes
383             for(Class parentClass : parentClasses) {
384                 logger.fine("parentClass - " + parentClass.getName());
385                 //check if it its an interface
386                 if(parentClass.isInterface())
387                     //if it is, throw exception
388                     throw new MIException("Found interface " + parentClass.getName()
389                             + " - interfaces as parent not allowed, " +
390                             "because a class can not extend an interface, " +
391                             "it can only implement an interface");
392                 //check if its an abstract class
393                 if(Modifier.isAbstract(parentClass.getModifiers()))
394                     //if it is throw exception
395                     throw new MIException("Found abstract class " + parentClass.getName()
396                             + " - abstract classes as parent are not allowed, " +
397                             "because they can not be instantiated.");
398                 //find no-arg constructor
399                 boolean foundNoArgConstructor = false;
400                 Constructor<?>[] constructors = parentClass.getDeclaredConstructors();
401                 for (Constructor<?> constructor : constructors) {
402                     if(constructor.getParameterTypes().length == 0
403                             && constructor.getModifiers() == Modifier.PUBLIC) {
404                         foundNoArgConstructor = true;
405                         break;
406                     }
407                 }
408                 //if not found
409                 if(!foundNoArgConstructor)
410                 {
411                     displayClassInfo(parentClass);
412                     //throw exception
413                     throw new MIException("Class " + parentClass.getName()
414                             + " does not contains required no-arg constructor. Possible reason: " +
415                             "http://codeoftheday.blogspot.com/2013/07/no-arguments-default-constructor-and.html");
416                 }
417             }
418             //add validated class to cache
419             getMiObjectCache().validated(clazz);
420             return; // all is well
421         }
422         //throw exception if classes are missing
423         throw new MIException("@MISupport annotation on class " + clazz.getName()
424                 + " must contain one or more concrete parent classes");
425     }
426 
427     /**
428      * Method to log given class info
429      *
430      * @param clazz class whose info is to be logged
431      */
432     private void displayClassInfo (Class clazz) {
433         StringBuilder sb = new StringBuilder("\n").append(clazz.getName()).append(" [ ")
434                 .append(clazz.isMemberClass() ? "Member Class " : "")
435                 .append(clazz.isLocalClass() ? "Local Class " : "")
436                 .append(clazz.isAnonymousClass() ? "Anonymous Class " : "")
437                 .append(clazz.isSynthetic() ? "Synthetic Class " : "")
438                 .append("]")
439                 .append(getDeclaringClass(clazz))
440                 .append(clazz.getEnclosingClass() != null ? "\n\tEnclosed in class - " + clazz.getEnclosingClass().getSimpleName() : "")
441                 .append(clazz.getEnclosingConstructor() != null ? "\n\tEnclosed in constructor - " + clazz.getEnclosingConstructor().toGenericString() : "")
442                 .append(clazz.getEnclosingMethod() != null ? "\n\tEnclosed in method - " + clazz.getEnclosingMethod().toGenericString() : "")
443                 .append("\n\tConstructors:")
444                 ;
445         Constructor<?>[] constructors = clazz.getDeclaredConstructors();
446         for (Constructor<?> constructor : constructors) {
447             sb.append("\n\t\t").append(constructor.toGenericString());
448         }
449         logger.info(sb.toString());
450     }
451 
452     /**
453      * The sole purpose of this method is to swallow following exception:
454      *
455      * java.lang.IncompatibleClassChangeError: com.smhumayun.mi_plus.MIFactoryTest and
456      * com.smhumayun.mi_plus.MIFactoryTest$1Parent disagree on InnerClasses attribute
457      * at java.lang.Class.getDeclaringClass(Native Method)
458      *
459      * Need to investigate it further as to why this was thrown and how it can be prevented. The exception was
460      * thrown when we run impl case newInstance_missingNoArgConstructor(). It is quite confusing that the exception was
461      * thrown in a method that is meant for displaying class meta only. The actual class and method to blame is
462      * {@link Class#getDeclaringClass()}
463      *
464      * @param clazz class
465      * @return declaring class's simple name
466      */
467     private String getDeclaringClass (Class clazz)
468     {
469         try
470         {
471             return clazz.getDeclaringClass() != null ? "\n\tDeclared by class - " + clazz.getDeclaringClass().getSimpleName() : "";
472         }
473         catch (IncompatibleClassChangeError e)
474         {
475             /*SWALLOW*/
476             return "\n\tDeclared by class - [ " + e.toString() + "] ";
477         }
478     }
479 
480     /**
481      * Get method resolver
482      *
483      * @return method resolver
484      * @see {@link com.smhumayun.mi_plus.MIMethodResolver}
485      */
486     public MIMethodResolver getMiMethodResolver() {
487         return miMethodResolver;
488     }
489 
490     /**
491      * Set method resolver
492      *
493      * @param miMethodResolver method resolver
494      * @see {@link com.smhumayun.mi_plus.MIMethodResolver}
495      * @throws MIException {@link MIException}
496      */
497     public void setMiMethodResolver(MIMethodResolver miMethodResolver) throws MIException {
498         if(miMethodResolver == null)
499             throw new MIException("miMethodResolver is NULL");
500         this.miMethodResolver = miMethodResolver;
501     }
502 
503     /**
504      * Get {@link MIObjectCache}
505      *
506      * @return {@link MIObjectCache}
507      */
508     public MIObjectCache getMiObjectCache() {
509         return miObjectCache;
510     }
511 
512     /**
513      * Set {@link MIObjectCache}
514      *
515      * @param miObjectCache {@link MIObjectCache}
516      */
517     public void setMiObjectCache(MIObjectCache miObjectCache) throws MIException {
518         if(miObjectCache == null)
519             throw new MIException("miObjectCache is NULL");
520         this.miObjectCache = miObjectCache;
521     }
522 
523 }