/*
* Copyright 2016 Function1. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tools.gsf.config;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* This class caches the produced objects for the lifetime of this object.
*
* The object is located in subclasses by searching for methods annotated with the {@link ServiceProducer}
* annotation, whose return type is assignable from
* the class requested in the {@link #getObject(String,Class)} method. NOTE: It does not require an
* exact match of the return type, only that it is <em>assignable</em>. Subclasses should not declare
* ambiguous {@link ServiceProducer} methods (without specifying a unique <code>name</code>) in the
* annotation, or else users may be surprised by which object is returned.
*
* It is possible to differentiate between two objects that are of the same type (or supertype) by using
* the <code>name</code> attribute on the {@link ServiceProducer} annotation. If provided, the name
* of the producer takes precedence over unnamed producer methods.
*
* If an object created in this factory is flagged as cached (using the <code>cache</code> attribute of
* the {@link ServiceProducer} annotation, the object will be cached against the <code>name</code> (if provided,
* or else the simple name of the requested type will be used) for the lifetime of this factory.
*
* @author Tony Field
* @author Dolf Dijkstra
* @since 2016-08-06
*/
public abstract class AbstractDelegatingFactory<SCOPE> implements Factory {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDelegatingFactory.class);
private final Map<String, Object> objectCache = new HashMap<>();
private final SCOPE scope;
private final Factory delegate;
protected AbstractDelegatingFactory(SCOPE scope, Factory delegate) {
this.scope = scope;
this.delegate = delegate;
}
@Override
public final <T> T getObject(final String name, final Class<T> fieldType) {
T o;
try {
// try to locate it internally
o = locate(name, fieldType);
if (o != null) {
LOG.debug("Located object {} of type {} in scope {}", name, fieldType.getName(), this.getClass().getName());
} else {
// delegate to another factory
LOG.debug("Did NOT locate object {} of type {} in scope {}", name, fieldType.getName(), this.getClass().getName());
if (delegate != null) {
LOG.debug("Will attempt locating object {} of type {} in delegate {}", name, fieldType.getName(), delegate.getClass().getName());
o = delegate.getObject(name, fieldType);
if (o != null) {
LOG.debug("Located object {} of type {} in delegate {}", name, fieldType.getName(), delegate.getClass().getName());
}
} else {
// we can't build it and we can't delegate it.
LOG.debug("Cannot delegate lookup onto any other scope.");
}
}
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getTargetException());
}
return o;
}
/**
* Internal method to check for Services or create Services.
*
* @param <T> ics or cached object
* @param askedName name of asset to find
* @param fieldType current asset
* @return the found service, null if no T can be created.
* @throws InvocationTargetException exception from invocation
*/
@SuppressWarnings("unchecked")
private <T> T locate(final String askedName, final Class<T> fieldType) throws InvocationTargetException {
if (scope.getClass().isAssignableFrom(fieldType)) {
return (T) scope;
}
if (fieldType.isArray()) {
throw new IllegalArgumentException("Arrays are not supported");
}
final String name = StringUtils.isNotBlank(askedName) ? askedName : fieldType.getSimpleName();
if (StringUtils.isBlank(name)) {
return null; // should not be possible - fieldType cannot be anonymous.
}
Object o = locateInCache(fieldType, name);
if (o == null) {
o = namedAnnotationStrategy(name, fieldType);
}
if (o == null) {
o = unnamedAnnotationStrategy(name, fieldType);
}
return (T) o;
}
private <T> Object locateInCache(Class<T> c, String name) {
Object o = objectCache.get(name);
if (o != null) {
if (!c.isAssignableFrom(o.getClass())) {
throw new IllegalStateException("Name conflict: '" + name + "' is in cache and is of type '"
+ o.getClass() + "' but a '" + c.getName()
+ "' was asked for. Please check your factories for naming conflicts.");
} else {
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());
}
}
return o;
}
private static boolean shouldCache(Method m) {
boolean r = false;
if (m.isAnnotationPresent(ServiceProducer.class)) {
ServiceProducer ann = m.getAnnotation(ServiceProducer.class);
r = ann.cache();
}
return r;
}
/**
* Tries to create the object based on the {@link ServiceProducer}
* annotation where the names match.
*
* @param <T> object created by service producer
* @param name name
* @param c current asset
* @return created object
* @throws InvocationTargetException exception from invocation
*/
private <T> T namedAnnotationStrategy(String name, Class<T> c) throws InvocationTargetException {
for (Method m : this.getClass().getMethods()) {
if (m.isAnnotationPresent(ServiceProducer.class)) {
if (c.isAssignableFrom(m.getReturnType())) {
String n = m.getAnnotation(ServiceProducer.class).name();
if (name.equals(n)) {
return constructAndCacheObject(name, c, m);
}
}
}
}
return null;
}
/**
* Tries to create the object based on the {@link tools.gsf.config.ServiceProducer}
* annotation without a name.
*
* @param <T> object created based on service producer
* @param name name
* @param c current asset
* @return created object
* @throws InvocationTargetException exception from invocation
*/
private <T> T unnamedAnnotationStrategy(String name, Class<T> c) throws InvocationTargetException {
for (Method m : this.getClass().getMethods()) {
if (m.isAnnotationPresent(ServiceProducer.class)) {
if (c.isAssignableFrom(m.getReturnType())) {
String n = m.getAnnotation(ServiceProducer.class).name();
if (StringUtils.isBlank(n)) {
return constructAndCacheObject(name, c, m);
}
}
}
}
return null;
}
private <T> T constructAndCacheObject(String name, Class<T> c, Method m) throws InvocationTargetException {
switch (m.getParameterCount()) {
case 0: {
T result = ReflectionUtils.createFromMethod(name, c, this, m);
if (shouldCache(m)) {
objectCache.put(name, result);
}
return result;
}
case 1: {
Class type = m.getParameterTypes()[0];
if (type.isAssignableFrom(scope.getClass())) {
T result = ReflectionUtils.createFromMethod(name, c, this, m, scope);
if (shouldCache(m)) {
objectCache.put(name, result);
}
return result;
} else {
throw new UnsupportedOperationException("Cannot create object with parameter type " + type.getName() + " using method " + m.getName() + " in class " + m.getDeclaringClass().getName());
}
}
default: {
throw new UnsupportedOperationException("Cannot create object using method " + m.getName() + " in class " + m.getDeclaringClass().getName() + " - invalid number of parameters");
}
}
}
@Override
public String toString() {
String s = this.getClass().getSimpleName();
return "{" + s + (delegate == null ? "}" : "::delegate:" + delegate.getClass().getName() + "}");
}
}