View Javadoc
1   /*
2    * Copyright 2016 Function1. All Rights Reserved.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package tools.gsf.config;
17  
18  import org.apache.commons.lang3.StringUtils;
19  import org.slf4j.Logger;
20  import org.slf4j.LoggerFactory;
21  
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.util.HashMap;
25  import java.util.Map;
26  
27  /**
28   * This class caches the produced objects for the lifetime of this object.
29   *
30   * The object is located in subclasses by searching for methods annotated with the {@link ServiceProducer}
31   * annotation, whose return type is assignable from
32   * the class requested in the {@link #getObject(String,Class)} method. NOTE: It does not require an
33   * exact match of the return type, only that it is <em>assignable</em>. Subclasses should not declare
34   * ambiguous {@link ServiceProducer} methods (without specifying a unique <code>name</code>) in the
35   * annotation, or else users may be surprised by which object is returned.
36   *
37   * It is possible to differentiate between two objects that are of the same type (or supertype) by using
38   * the <code>name</code> attribute on the {@link ServiceProducer} annotation. If provided, the name
39   * of the producer takes precedence over unnamed producer methods.
40   *
41   * If an object created in this factory is flagged as cached (using the <code>cache</code> attribute of
42   * the {@link ServiceProducer} annotation, the object will be cached against the <code>name</code> (if provided,
43   * or else the simple name of the requested type will be used) for the lifetime of this factory.
44   *
45   * @author Tony Field
46   * @author Dolf Dijkstra
47   * @since 2016-08-06
48   */
49  public abstract class AbstractDelegatingFactory<SCOPE> implements Factory {
50  
51      private static final Logger LOG = LoggerFactory.getLogger(AbstractDelegatingFactory.class);
52  
53      private final Map<String, Object> objectCache = new HashMap<>();
54  
55      private final SCOPE scope;
56      private final Factory delegate;
57  
58      protected AbstractDelegatingFactory(SCOPE scope, Factory delegate) {
59          this.scope = scope;
60          this.delegate = delegate;
61      }
62  
63      @Override
64      public final <T> T getObject(final String name, final Class<T> fieldType) {
65  
66          T o;
67          try {
68              // try to locate it internally
69              o = locate(name, fieldType);
70              if (o != null) {
71                  LOG.debug("Located object {} of type {} in scope {}", name, fieldType.getName(), this.getClass().getName());
72              } else {
73                  // delegate to another factory
74              	LOG.debug("Did NOT locate object {} of type {} in scope {}", name, fieldType.getName(), this.getClass().getName());
75                  if (delegate != null) {
76                  	LOG.debug("Will attempt locating object {} of type {} in delegate {}", name, fieldType.getName(), delegate.getClass().getName());
77                      o = delegate.getObject(name, fieldType);
78                      if (o != null) {
79                          LOG.debug("Located object {} of type {} in delegate {}", name, fieldType.getName(), delegate.getClass().getName());
80                      }
81                  } else {
82  	                // we can't build it and we can't delegate it.
83                  	LOG.debug("Cannot delegate lookup onto any other scope.");
84                  }
85              }
86          } catch (InvocationTargetException e) {
87              throw new RuntimeException(e.getTargetException());
88          }
89          return o;
90      }
91  
92      /**
93       * Internal method to check for Services or create Services.
94       *
95       * @param <T>       ics or cached object
96       * @param askedName name of asset to find
97       * @param fieldType current asset
98       * @return the found service, null if no T can be created.
99       * @throws InvocationTargetException exception from invocation
100      */
101     @SuppressWarnings("unchecked")
102     private <T> T locate(final String askedName, final Class<T> fieldType) throws InvocationTargetException {
103         if (scope.getClass().isAssignableFrom(fieldType)) {
104             return (T) scope;
105         }
106         if (fieldType.isArray()) {
107             throw new IllegalArgumentException("Arrays are not supported");
108         }
109         final String name = StringUtils.isNotBlank(askedName) ? askedName : fieldType.getSimpleName();
110         if (StringUtils.isBlank(name)) {
111             return null; // should not be possible - fieldType cannot be anonymous.
112         }
113 
114         Object o = locateInCache(fieldType, name);
115         if (o == null) {
116             o = namedAnnotationStrategy(name, fieldType);
117         }
118         if (o == null) {
119             o = unnamedAnnotationStrategy(name, fieldType);
120         }
121         return (T) o;
122     }
123 
124     private <T> Object locateInCache(Class<T> c, String name) {
125         Object o = objectCache.get(name);
126         if (o != null) {
127             if (!c.isAssignableFrom(o.getClass())) {
128                 throw new IllegalStateException("Name conflict: '" + name + "' is in cache and is of type  '"
129                         + o.getClass() + "' but a '" + c.getName()
130                         + "' was asked for. Please check your factories for naming conflicts.");
131             } else {
132                 LOG.debug("Object named {} was found in cache in factory {}. An object of type {} was requested, which is assignable from the returned object, whose type is {}.", name, this.getClass().getName(), c.getName(), o.getClass().getName());
133             }
134         }
135         return o;
136     }
137 
138     private static boolean shouldCache(Method m) {
139         boolean r = false;
140         if (m.isAnnotationPresent(ServiceProducer.class)) {
141             ServiceProducer ann = m.getAnnotation(ServiceProducer.class);
142             r = ann.cache();
143         }
144         return r;
145     }
146 
147     /**
148      * Tries to create the object based on the {@link ServiceProducer}
149      * annotation where the names match.
150      *
151      * @param <T>  object created by service producer
152      * @param name name
153      * @param c    current asset
154      * @return created object
155      * @throws InvocationTargetException exception from invocation
156      */
157     private <T> T namedAnnotationStrategy(String name, Class<T> c) throws InvocationTargetException {
158 
159         for (Method m : this.getClass().getMethods()) {
160             if (m.isAnnotationPresent(ServiceProducer.class)) {
161                 if (c.isAssignableFrom(m.getReturnType())) {
162                     String n = m.getAnnotation(ServiceProducer.class).name();
163                     if (name.equals(n)) {
164                         return constructAndCacheObject(name, c, m);
165                     }
166                 }
167             }
168         }
169         return null;
170     }
171 
172     /**
173      * Tries to create the object based on the {@link tools.gsf.config.ServiceProducer}
174      * annotation without a name.
175      *
176      * @param <T>  object created based on service producer
177      * @param name name
178      * @param c    current asset
179      * @return created object
180      * @throws InvocationTargetException exception from invocation
181      */
182     private <T> T unnamedAnnotationStrategy(String name, Class<T> c) throws InvocationTargetException {
183         for (Method m : this.getClass().getMethods()) {
184             if (m.isAnnotationPresent(ServiceProducer.class)) {
185                 if (c.isAssignableFrom(m.getReturnType())) {
186                     String n = m.getAnnotation(ServiceProducer.class).name();
187                     if (StringUtils.isBlank(n)) {
188                         return constructAndCacheObject(name, c, m);
189                     }
190                 }
191             }
192         }
193         return null;
194     }
195 
196     private <T> T constructAndCacheObject(String name, Class<T> c, Method m) throws InvocationTargetException {
197         switch (m.getParameterCount()) {
198             case 0: {
199                 T result = ReflectionUtils.createFromMethod(name, c, this, m);
200                 if (shouldCache(m)) {
201                     objectCache.put(name, result);
202                 }
203                 return result;
204             }
205             case 1: {
206                 Class type = m.getParameterTypes()[0];
207                 if (type.isAssignableFrom(scope.getClass())) {
208                     T result = ReflectionUtils.createFromMethod(name, c, this, m, scope);
209                     if (shouldCache(m)) {
210                         objectCache.put(name, result);
211                     }
212                     return result;
213                 } else {
214                     throw new UnsupportedOperationException("Cannot create object with parameter type " + type.getName() + " using method " + m.getName() + " in class " + m.getDeclaringClass().getName());
215                 }
216             }
217             default: {
218                 throw new UnsupportedOperationException("Cannot create object using method " + m.getName() + " in class " + m.getDeclaringClass().getName() + " - invalid number of parameters");
219             }
220         }
221     }
222 
223     @Override
224     public String toString() {
225         String s = this.getClass().getSimpleName();
226         return "{" + s + (delegate == null ? "}" : "::delegate:" + delegate.getClass().getName() + "}");
227     }
228 }