/**
* Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.sesame.graph;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Provider;
import org.apache.commons.lang.ClassUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.opengamma.core.link.Link;
import com.opengamma.id.ExternalId;
import com.opengamma.id.ExternalIdBundle;
import com.opengamma.id.UniqueId;
import com.opengamma.sesame.config.EngineUtils;
import com.opengamma.sesame.config.FunctionArguments;
import com.opengamma.sesame.config.FunctionModelConfig;
import com.opengamma.sesame.engine.ComponentMap;
import com.opengamma.sesame.function.Parameter;
import com.opengamma.sesame.graph.convert.ArgumentConverter;
import com.opengamma.util.ArgumentChecker;
/**
* A node in the function model that can build a function and its dependencies.
* TODO this class has static and instance methods both called create(). that's nasty
*/
public abstract class FunctionModelNode {
/** A set of ineligible types. */
private static final Set<Class<?>> INELIGIBLE_TYPES =
Sets.<Class<?>>newHashSet(UniqueId.class, ExternalId.class, ExternalIdBundle.class);
/** The expected type of the object created by this node, not null. */
private final Class<?> _type;
/** The parameter this node satisfies, null if it's the root node. */
private final Parameter _parameter;
/**
* Creates an instance.
*
* @param type the expected type of the object created by this node, not null
* @param parameter the parameter this node satisfies, null if it's the root node
*/
FunctionModelNode(Class<?> type, Parameter parameter) {
_type = ArgumentChecker.notNull(type, "type");
_parameter = parameter;
}
/**
* Creates a node for building an object of the specified type.
* The node and its dependencies are built using the provided config.
*
* @param type the type of the object built by the node
* @param config configuration used to build the node and any dependent nodes
* @param availableComponents the types of components that the engine can provide to satisfy node dependencies
* @param nodeDecorator inserts proxies between functions to provide engine services, e.g. caching, tracing
* @return a node that can build an object
*/
public static FunctionModelNode create(Class<?> type,
FunctionModelConfig config,
Set<Class<?>> availableComponents,
NodeDecorator nodeDecorator) {
// TODO maybe use DefaultArgumentConverter here, that would allow more compact (but less type safe) config
return create(type, config, availableComponents, nodeDecorator, ArgumentConverter.NO_OP);
}
/**
* Creates a node for building an object of the specified type.
* The node and its dependencies are built using the provided config.
*
* @param type the type of the object built by the node
* @param config configuration used to build the node and any dependent nodes
* @param availableComponents the types of components that the engine can provide to satisfy node dependencies
* @param nodeDecorator inserts proxies between functions to provide engine services, e.g. caching, tracing
* @return a node that can build an object
*/
public static FunctionModelNode create(Class<?> type,
FunctionModelConfig config,
Set<Class<?>> availableComponents,
NodeDecorator nodeDecorator,
ArgumentConverter argumentConverter) {
return createNode(type, config, availableComponents, nodeDecorator, Lists.<Parameter>newArrayList(), null, argumentConverter);
}
@Nullable
private static FunctionModelNode createNode(Class<?> type,
FunctionModelConfig config,
Set<Class<?>> availableComponents,
NodeDecorator nodeDecorator,
List<Parameter> path,
@Nullable Parameter parameter,
ArgumentConverter argumentConverter) {
if (!isEligibleForBuilding(type)) {
return null;
}
Class<?> implType;
try {
implType = getImplementationType(type, config, path, parameter);
} catch (NoImplementationException e) {
return new NoImplementationNode(type, e, parameter);
} catch (InvalidImplementationException e) {
return new CannotBuildNode(type, e, parameter);
}
Constructor<?> constructor;
try {
constructor = getConstructor(implType, path);
} catch (NoSuitableConstructorException e) {
return new CannotBuildNode(type, e, parameter);
}
List<Parameter> constructorParameters = EngineUtils.getParameters(constructor);
List<FunctionModelNode> constructorArguments = Lists.newArrayListWithCapacity(constructorParameters.size());
for (Parameter constructorParameter : constructorParameters) {
// this isn't terribly efficient but unlikely to be a problem. could use a stack but that's nasty and mutable
List<Parameter> newPath = Lists.newArrayList(path);
newPath.add(constructorParameter);
FunctionModelNode argNode =
createArgumentNode(type, config, availableComponents, nodeDecorator, implType, constructorParameter, newPath, argumentConverter);
constructorArguments.add(argNode);
}
FunctionModelNode node;
if (type.isInterface()) {
node = new InterfaceNode(type, implType, constructorArguments, parameter);
} else {
node = new ClassNode(type, implType, constructorArguments, parameter);
}
return nodeDecorator.decorateNode(node);
}
/**
* Returns the implementation that should be used for creating instances of a type.
* <p>
* The result can be:
* <ul>
* <li>An implementation of the specified interface</li>
* <li>A {@link Provider} that provides the implementation</li>
* <li>The input type itself, if it is a concrete class</li>
* </ul>
* @param type the type to find the implementation type for, not null
* @param parameter the constructor parameter the implementation must satisfy
* @return the implementation type that should be used, not null
* @throws NoImplementationException if there is no implementation available
* @throws InvalidImplementationException if the implementation can't be built
*/
private static Class<?> getImplementationType(Class<?> type,
FunctionModelConfig config,
List<Parameter> path,
@Nullable Parameter parameter) {
Class<?> implType = config.getFunctionImplementation(parameter, type);
if (implType == null) {
if (type.isInterface()) {
throw new NoImplementationException(type, path, "No implementation or provider found: " + type.getSimpleName());
}
implType = type;
}
if (!type.isAssignableFrom(implType) && !Provider.class.isAssignableFrom(implType)) {
throw new InvalidImplementationException(path, "Function not an implementation of parent: " + implType.getSimpleName());
}
if (!isValidImplementationType(implType)) {
throw new InvalidImplementationException(path, "Function implementation is invalid: " + type.getSimpleName());
}
return implType;
}
/**
* Returns a constructor for the engine to build instances of type.
* <p>
* Only public constructors are considered.
* If there is only one constructor it is used. If there are multiple constructors
* then one, and only one, must be annotated with {@link Inject}.
*
* @param type the type to find the constructor for, not null
* @return the constructor the engine should use for building instances, not null
* @throws NoSuitableConstructorException if there isn't a valid constructor
*/
private static Constructor<?> getConstructor(Class<?> type, List<Parameter> path) {
try {
return EngineUtils.getConstructor(type);
} catch (IllegalArgumentException ex) {
// TODO better error message here. this can mean an object is missing from config
throw new NoSuitableConstructorException(path, "No suitable constructor: " + type.getName());
}
}
/**
* <p>Creates a node in the function model representing a single constructor argument.
* If the object has dependencies this method is called recursively and descends down the dependency tree
* creating child nodes until all dependencies are satisfied. If a dependency can't be satisfied an error node is
* created.</p>
*
* <p>There are 3 ways a dependency can be satisfied. They are tried in order:
* <ol>
* <li>A component provided by the configuration. The component is matched to the type of the parameter</li>
* <li>A value provided by the user in the configuration. This is specified in {@link FunctionArguments} by
* the implementation class of the function and the parameter name</li>
* <li>A function built by recursively calling
* {@link #createNode(Class, FunctionModelConfig, Set, NodeDecorator, List, Parameter, ArgumentConverter)}</li>
* </ol>
* </p>
* @param type The type of the parent object into which the argument will be injected
* @param config The configuration for building the model
* @param parameter The constructor parameter this node must satisfy.
* @param path The chain of dependencies from the root of the tree of functions to this parameter. In the event of a
* failure this allows the exact location in the graph to be identified
* @return A node in the function graph, not null
*/
private static FunctionModelNode createArgumentNode(Class<?> type,
FunctionModelConfig config,
Set<Class<?>> availableComponents,
NodeDecorator nodeDecorator,
Class<?> implType,
Parameter parameter,
List<Parameter> path,
ArgumentConverter argumentConverter) {
try {
if (availableComponents.contains(parameter.getType())) {
// the parameter can be satisfied by an existing component, no need to build it or look up a user argument
return nodeDecorator.decorateNode(new ComponentNode(parameter));
}
// has the user explicitly provided an argument?
Object argument = getConstructorArgument(config, implType, path, parameter, argumentConverter);
// can we use this argument directly?
if (canInjectArgument(argument, parameter.getType())) {
// TODO should this be decorated?
return new ArgumentNode(parameter.getType(), argument, parameter);
}
// the parameter is a configuration object so the engine won't try to create it. it should be
// injected using a config link
boolean isConfig = EngineUtils.isConfig(parameter.getType());
if (!isConfig) {
// if the user has specified function config as the argument we use it to build the subtree
// otherwise we use the existing config to build it
FunctionModelConfig subtreeConfig =
argument != null ?
((FunctionModelConfig) argument).mergedWith(config) :
config;
FunctionModelNode createdNode =
createNode(parameter.getType(),
subtreeConfig,
availableComponents,
nodeDecorator,
path,
parameter,
argumentConverter);
if (createdNode != null) {
return createdNode;
}
}
if (parameter.isNullable()) {
return nodeDecorator.decorateNode(new ArgumentNode(parameter.getType(), null, parameter));
}
if (isConfig) {
throw new MissingConfigException(path, "No configuration link available for non-nullable parameter " + parameter.getFullName());
} else {
throw new MissingArgumentException(path, "No value available for non-nullable parameter " + parameter.getFullName());
}
} catch (MissingArgumentException e) {
return new MissingArgumentNode(type, e, parameter);
} catch (MissingConfigException e) {
return new MissingConfigNode(type, e, parameter);
} catch (ArgumentConversionException e) {
return new ArgumentConversionErrorNode(type, e, parameter, e.getArgument(), e.getMessage());
} catch (IncompatibleTypeException e) {
return new IncompatibleArgumentTypeNode(type, e, parameter);
}
}
/**
* Checks whether the argument can be used to satisfy a parameter of the specified type.
* Returns false if the argument is null or the argument type is {@link FunctionModelConfig}
* but the parameter type isn't.
*
* @param argument the argument
* @param parameterType the type of the parameter
* @return true if the argument can be used to satisfy the parameter
*/
private static boolean canInjectArgument(@Nullable Object argument, Class<?> parameterType) {
if (argument == null) {
return false;
}
if (argument instanceof FunctionModelConfig) {
return parameterType.isAssignableFrom(FunctionModelConfig.class);
}
return true;
}
/**
* Checks if the type is eligible for building.
*
* @param type the type, not null
* @return true if valid
*/
private static boolean isEligibleForBuilding(Class<?> type) {
if (INELIGIBLE_TYPES.contains(type)) {
return false;
}
if (type.isPrimitive()) {
return false;
}
Package pkg = type.getPackage();
if (pkg != null) {
String packageName = pkg.getName();
if (packageName.startsWith("java") || packageName.startsWith("javax") || packageName.startsWith("org.threeten")) {
return false;
}
}
return true;
}
/**
* Checks if the type is invalid and cannot be constructed.
*
* @param type the type, not null
* @return true if valid
*/
private static boolean isValidImplementationType(Class<?> type) {
if (type.isInterface() || type.isAnnotation() || type.isPrimitive() || type.isArray() || type.isEnum() || type.isSynthetic() ||
Modifier.isAbstract(type.getModifiers()) || type.isAnonymousClass() || type.isLocalClass()) {
return false;
}
if (type.isMemberClass() && !Modifier.isStatic(type.getModifiers())) { // inner class (vs nested class)
return false;
}
return true;
}
/**
* Checks the type of the constructor argument matches the expected type and returns it.
* Handles {@link Link}s and {@link Provider}s by checking the parameter type is a
* {@link Link} or {@link Provider} or that its type is compatible with the linked / provided object.
*/
private static Object getConstructorArgument(FunctionModelConfig functionModelConfig,
Class<?> objectType,
List<Parameter> path,
Parameter parameter,
ArgumentConverter argumentConverter) {
FunctionArguments args = functionModelConfig.getFunctionArguments(objectType);
Object arg = args.getArgument(parameter.getName());
if (arg == null || arg instanceof Provider || arg instanceof FunctionModelConfig) {
return arg;
// this takes into account boxing of primitives which Class.isAssignableFrom() doesn't
} else if (ClassUtils.isAssignable(arg.getClass(), parameter.getType(), true)) {
return arg;
} else if (arg instanceof Link) {
if (ClassUtils.isAssignable(((Link<?>) arg).getTargetType(), parameter.getType(), true)) {
return arg;
} else {
throw new IncompatibleTypeException(path, "Link argument (" + arg + ") doesn't resolve to the " +
"required type for " + parameter.getFullName());
}
} else if (arg instanceof String && argumentConverter.isConvertible(parameter.getParameterType())) {
try {
return argumentConverter.convertFromString(parameter.getParameterType(), (String) arg);
} catch (Exception e) {
throw new ArgumentConversionException((String) arg, path, "Unable to convert value '" + arg + "' to type " +
parameter.getParameterType().getName(), e);
}
} else {
throw new IncompatibleTypeException(path, "Argument (" + arg + ": " + arg.getClass().getSimpleName() + ") isn't of the " +
"required type for " + parameter.getFullName());
}
}
//-------------------------------------------------------------------------
/**
* Gets the expected type of the object created by this node.
*
* @return the expected type, not null
*/
public Class<?> getType() {
return _type;
}
/**
* Gets the parameter that this node satisfies.
*
* @return the parameter, not null
*/
public Parameter getParameter() {
return _parameter;
}
/**
* Gets the concrete, non-proxy, node.
* <p>
* This is used to access the concrete node that has been proxied.
* Most nodes simply return {@code this}.
*
* @return the parameter, not null
*/
public FunctionModelNode getConcreteNode() {
return this;
}
//-------------------------------------------------------------------------
/**
* Creates the object represented by this node.
* <p>
* Implementations should override {@link #doCreate}, not this method.
*
* @param componentMap the map of infrastructure components, not null
* @param dependencies the dependencies of this node, not null
* @param functionIdProvider provides unique IDs for function instances
* @return the object represented by this node, may be null
*/
public Object create(ComponentMap componentMap, List<Object> dependencies, FunctionIdProvider functionIdProvider) {
Object object = doCreate(componentMap, dependencies, functionIdProvider);
if (object instanceof Provider) {
// TODO some slightly more robust checking of compatibility of types
// TODO what's the logic I actually need here?
return Provider.class.isAssignableFrom(_type) ? object : ((Provider<?>) object).get();
} else if (object instanceof Link) {
return Link.class.isAssignableFrom(_type) ? object : ((Link<?>) object).resolve();
} else {
return object;
}
}
/**
* Returns the object represented by this node, creating if necessary.
* <p>
* If this node's object is a {@link Provider} this method should return it,
* not the results of calling {@link Provider#get()}. This class will use the
* expected type to decide whether to call {@code get()} or
* to inject the provider instance directly.
*
* @param componentMap the map of infrastructure components, not null
* @param dependencies the dependencies of this node, not null
* @param idProvider provides unique IDs for function instances
* @return the object represented by this node, may be null
*/
protected abstract Object doCreate(ComponentMap componentMap, List<Object> dependencies, FunctionIdProvider idProvider);
//-------------------------------------------------------------------------
/**
* Gets the dependencies of this node.
*
* @return the dependencies, not null
*/
public List<FunctionModelNode> getDependencies() {
return Collections.emptyList();
}
/**
* Gets the complete set of exceptions in the tree of this node.
*
* @return the list of exceptions, not null
*/
public List<InvalidGraphException> getExceptions() {
return Collections.emptyList();
}
/**
* Checks if this node represents a valid object that can be constructed.
* <p>
* A true result implies that this node and all nodes below it in the dependency tree are valid.
*
* @return true if this node and all its dependencies are valid
*/
public boolean isValid() {
return true;
}
/**
* Checks if this node is an error node.
*
* @return true if this node represents an object that is the source of an error
*/
public boolean isError() {
return false;
}
//-------------------------------------------------------------------------
/**
* Pretty prints this node and its descendants.
*
* @param showProxies true to include proxy nodes
* @return the node structure, not null
*/
public String prettyPrint(boolean showProxies) {
return prettyPrint(new StringBuilder(), "", "", showProxies).toString();
}
/**
* Pretty prints this node and its descendants without including proxy nodes inserted to provide engine services.
*
* @return the node structure, not null
*/
public String prettyPrint() {
return prettyPrint(false);
}
/**
* Provides the name of the parameter being satisfied ready for pretty printing.
*
* @return a description of the node, not null
*/
protected abstract String prettyPrintLine();
/**
* Performs the pretty print.
*
* @param builder the builder to add to, not null
* @param indent the current indent, not null
* @param childIndent the child indent, not null
* @param showProxies true to include proxy nodes
* @return the node structure, not null
*/
private StringBuilder prettyPrint(StringBuilder builder, String indent, String childIndent, boolean showProxies) {
FunctionModelNode realNode = (showProxies ? this : getConcreteNode());
// prefix the line with an indicator if the node is an error node for easier debugging
String errorPrefix = isError() ? "->" : " ";
// prefix the line with the parameter name
String paramPrefix = (realNode.getParameter() != null ? realNode.getParameter().getName() + ": " : "");
builder.append('\n').append(errorPrefix).append(indent).append(paramPrefix).append(realNode.prettyPrintLine());
for (Iterator<FunctionModelNode> it = realNode.getDependencies().iterator(); it.hasNext();) {
FunctionModelNode child = it.next();
String newIndent;
String newChildIndent;
boolean isFinalChild = !it.hasNext();
if (!isFinalChild) {
newIndent = childIndent + " |--"; // Unicode boxes: \u251c\u2500\u2500
newChildIndent = childIndent + " | "; // Unicode boxes: \u2502
} else {
newIndent = childIndent + " `--"; // Unicode boxes: \u2514\u2500\u2500
newChildIndent = childIndent + " ";
}
child.prettyPrint(builder, newIndent, newChildIndent, showProxies);
}
return builder;
}
//-------------------------------------------------------------------------
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final FunctionModelNode other = (FunctionModelNode) obj;
return Objects.equals(this._type, other._type);
}
@Override
public int hashCode() {
return Objects.hash(_type);
}
@Override
public String toString() {
String paramPrefix = (getParameter() != null ? getParameter().getName() + ": " : "");
return paramPrefix + prettyPrintLine();
}
}