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 }