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 COM.FutureTense.Interfaces.ICS;
19  
20  import javax.servlet.ServletContext;
21  
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.InvocationTargetException;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.Set;
30  
31  
32  /**
33   * Default factory producer class that creates factory classes for the scopes specified.
34   * <p>
35   * Factory classes will be looked up in a configuration file called {@link #CONFIG_FILE}.
36   * The format of the file is as follows: blank lines are allowed (and ignored); comments are
37   * allowed (and ignored). The comment indicator is (#).
38   * <p>
39   * Each functional line of the configuration file has two parts, the factory class name, and the
40   * scope class name. For example:
41   * <code>scope.class.name : factory.class.name</code>
42   * <p>
43   * Each factory class name must have a two-argument constructor. The first argument is the scope class,
44   * the very same class that is passed into the {@link #getFactory(Object)} method. The second
45   * argument is the optional delegate factory that the specified factory will defer to if it is not
46   * able to locate the specified object. (If no parent is specified, no delegation will occur).
47   * <p>
48   * Only one factory class per scope may be defined. If more than one is found, a configuration
49   * error will be thrown.
50   * <p>
51   * By default, DefaultFactoryProducer knows how to get access to the ServletContext from within the
52   * ICS object, and so a factory corresponding to the ServletContext scope will be used
53   * as the delegate for the ICS scoped factory. Users may override either of these two and the
54   * relationship will be preserved.
55   * <p>
56   * Conversely, this factory producer does not know the relationship between any custom scope
57   * and the two it knows about, and so it is unable to automatically wire delegation between
58   * factories involving custom scopes. To do this, it is necessary to extend this class (or
59   * replace it).
60   * <p>
61   * Also, this factory producer does not know how to cache factories with custom scopes, so
62   * new versions will be created whenever one is requested. For the known scopes, caching
63   * is built-in.
64   *
65   * @author Tony Field
66   * @since 2016-08-05
67   */
68  public class DefaultFactoryProducer implements FactoryProducer {
69  	
70  	private static final Logger LOG = LoggerFactory.getLogger(DefaultFactoryProducer.class);
71  
72      /**
73       * The configuration file(s) that this class will read while looking for
74       * information about which factories to use
75       */
76      protected static final String CONFIG_FILE = "META-INF/gsf-factory";
77  
78      private final Map<Class, Constructor<Factory>> factoryConstructors;
79  
80      /**
81       * Creates a new factory producer. This constructor pre-reads the configuration
82       * files and validates that constructors exist for the scope specified.
83       * <p>
84       * If no factory is configured for ICS or ServletContext, a default is provided.
85       */
86      public DefaultFactoryProducer() {
87  
88          ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
89  
90          // read the configuration file
91          Map<Class, Class> conf = lookupConfiguration(classLoader);
92  
93          // add defaults for known scopes
94          if (!conf.containsKey(ICS.class)) {
95              conf.put(ICS.class, IcsBackedFactory.class);
96          }
97          if (!conf.containsKey(ServletContext.class)) {
98              conf.put(ServletContext.class, ServletContextBackedFactory.class);
99          }
100         
101         // get the constructors
102         factoryConstructors = getConstructorMap(conf);
103         
104         if (LOG.isDebugEnabled()) {
105         	LOG.debug("DefaultFactoryProducer constructor is done... factoryConstructors = " + factoryConstructors);
106         }
107 
108     }
109 
110     @Override
111     public Factory getFactory(Object scope) {
112         if (scope instanceof ICS) {
113             return getFactory((ICS) scope);
114         } else if (scope instanceof ServletContext) {
115             return getFactory((ServletContext) scope);
116         } else if (scope == null) {
117             throw new IllegalArgumentException("Null scope not allowed");
118         } else if (factoryConstructors.containsKey(scope.getClass())) {
119             return createFactory(scope);
120         } else {
121             throw new IllegalArgumentException("Unsupported scope: " + scope.getClass().getName());
122         }
123     }
124 
125     /**
126      * Create a custom-scoped factory. Note that there is no caching support for a custom scope (because
127      * this class does not know enough about the scope to know how to cache against it).
128      *
129      * Subclasses can override this method and create their own internal cache.
130      * @param customScope custom scope
131      * @return the factory
132      */
133     protected Factory createFactory(Object customScope) {
134         Constructor<Factory> con = factoryConstructors.get(customScope.getClass());
135         try {
136             return con.newInstance(customScope, null /* delegate is not knowable */);
137         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
138             throw new IllegalStateException("Could not instantiate factory: " + e, e);
139         }
140     }
141 
142     /**
143      * The ics object pool key for the ics-backed factory.
144      */
145     protected static final String ICS_CONTEXT_BACKED_FACTORY = "gsf-ics-backed-factory";
146 
147     /**
148      * Get the ics-backed factory. Once created, the factory is cached on the ICS object
149      * pool using {@link #ICS_CONTEXT_BACKED_FACTORY} as the key.
150      *
151      * @param ics ics context
152      * @return the factory, never null
153      */
154     protected Factory getFactory(ICS ics) {
155         Object o = ics.GetObj(ICS_CONTEXT_BACKED_FACTORY);
156         if (o == null) {
157             o = createFactory(ics);
158             ics.SetObj(ICS_CONTEXT_BACKED_FACTORY, o);
159         }
160         return (Factory) o;
161     }
162 
163     /**
164      * Create a new instance of the ics-backed factory.
165      * <p>
166      * This implementation uses the constructor configured in the {@link #CONFIG_FILE}. It also
167      * uses ICS to locate the ServletContext, and then gets the servletContext-backed factory
168      * which it will use as a delegate for the ics-backed factory.
169      *
170      * @param ics the ics context
171      * @return the factory
172      */
173     protected Factory createFactory(ICS ics) {
174         ServletContext servletContext = ics.getIServlet().getServlet().getServletContext();
175         Factory delegate = getFactory(servletContext);
176 
177         Constructor<Factory> con = factoryConstructors.get(ICS.class);
178         try {
179             return con.newInstance(ics, delegate);
180         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
181             throw new IllegalStateException("Could not instantiate factory: " + e, e);
182         }
183     }
184 
185     /**
186      * The servlet context attribute key for the servletContext-backed factory.
187      */
188     protected static final String SERVLET_CONTEXT_BACKED_FACTORY = "gsf-servlet-context-backed-factory";
189 
190     /**
191      * Get the servletContext-backed factory. Once created, the factory is cached on the servlet context
192      * pool using {@link #SERVLET_CONTEXT_BACKED_FACTORY} as the key.
193      *
194      * @param servletContext the servlet context
195      * @return the factory, never null
196      */
197     protected Factory getFactory(ServletContext servletContext) {
198         Object o = servletContext.getAttribute(SERVLET_CONTEXT_BACKED_FACTORY);
199         if (o == null) {
200             o = createFactory(servletContext);
201             servletContext.setAttribute(SERVLET_CONTEXT_BACKED_FACTORY, o);
202         }
203         return (Factory) o;
204     }
205 
206     /**
207      * Create a new instance of the servletContext-backed factory.
208      * <p>
209      * This implementation uses the constructor configured in the {@link #CONFIG_FILE}.
210      *
211      * @param servletContext the servlet context
212      * @return the factory, never null
213      */
214     protected Factory createFactory(ServletContext servletContext) {
215         Constructor<Factory> con = factoryConstructors.get(ServletContext.class);
216         try {
217             return con.newInstance(servletContext, null);
218         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
219             throw new IllegalStateException("Could not instantiate factory: " + e, e);
220         }
221     }
222 
223     /**
224      * Look up the configuration for the FactoryProducer by reading the {@link #CONFIG_FILE}s found
225      * in the specified ClassLoader's classpath. The configuration file format is described above.
226      *
227      * @param classLoader the classloader to do the looking
228      * @return map containing the classes from the config file
229      * @throws IllegalStateException if the config file format is invalid, if more than one
230      *                               configuration setting is found for a given scope, or if the config
231      *                               file points to classes that are not found.
232      */
233     protected static Map<Class, Class> lookupConfiguration(ClassLoader classLoader) {
234         Map<Class, Class> config = new HashMap<>();
235         Set<String> configLines = ReflectionUtils.readConfigurationResource(classLoader, CONFIG_FILE);
236         for (String line : configLines) {
237             String[] lineInfo = line.split(":");
238             if (lineInfo.length != 2) {
239                 throw new IllegalStateException("Invalid configuration file format in " + CONFIG_FILE + ": " + line);
240             }
241             String scopeClassName = lineInfo[0];
242             String factoryClassName = lineInfo[1];
243             try {
244                 Class scopeClass = classLoader.loadClass(scopeClassName);
245                 if (config.containsKey(scopeClass)) {
246                     throw new IllegalStateException("More than one matching configuration setting found for " +
247                             scopeClass.getName() + " in " + CONFIG_FILE);
248                 }
249                 Class factoryClass = classLoader.loadClass(factoryClassName);
250                 config.put(scopeClass, factoryClass);
251             } catch (ClassNotFoundException e) {
252                 throw new IllegalStateException("Could not find class listed in configuration file: " +
253                         CONFIG_FILE + ": " + e, e);
254             }
255         }
256         return config;
257     }
258 
259     /**
260      * Converts the map of classes to a map containing factory constructors as the value.
261      * <p>
262      * This class will look for a constructor with the signature described above, and will
263      * fail if one is not found.
264      *
265      * @param config the mapping between scope and factory constructors to be used to create
266      *               the factories
267      * @return a map containing factory constructors for each scope. Never null
268      * @throws IllegalStateException if no constructor can be found with the appropriate signature,
269      *                               or if the class specified does not implement the Factory interface.
270      */
271     protected static Map<Class, Constructor<Factory>> getConstructorMap(Map<Class, Class> config) {
272         Map<Class, Constructor<Factory>> constructorMap = new HashMap<>(config.size());
273         for (Class scopeClass : config.keySet()) {
274             Class configuredFactoryClass = config.get(scopeClass);
275             if (Factory.class.isAssignableFrom(configuredFactoryClass)) {
276                 @SuppressWarnings("unchecked")
277                 Class<Factory> factoryClass = (Class<Factory>) configuredFactoryClass;
278                 try {
279                     Constructor<Factory> constructor = factoryClass.getConstructor(scopeClass, Factory.class);
280                     constructorMap.put(scopeClass, constructor);
281                 } catch (NoSuchMethodException e) {
282                     throw new IllegalStateException("Could not locate constructor with argument " + scopeClass.getName() + " for class " + factoryClass.getName());
283                 }
284             } else {
285                 throw new IllegalStateException("Invalid configuration - class " + configuredFactoryClass.getName() + " does not implement " + Factory.class);
286             }
287         }
288         return constructorMap;
289     }
290 }