/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.sesame.web.functionconfig;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import com.google.common.collect.ImmutableMap;
import com.opengamma.core.config.impl.ConfigItem;
import com.opengamma.core.link.ConfigLink;
import com.opengamma.master.config.ConfigMaster;
import com.opengamma.master.config.ConfigSearchRequest;
import com.opengamma.master.config.ConfigSearchResult;
import com.opengamma.sesame.OutputName;
import com.opengamma.sesame.config.EngineUtils;
import com.opengamma.sesame.config.FunctionArguments;
import com.opengamma.sesame.config.FunctionModelConfig;
import com.opengamma.sesame.function.AvailableImplementations;
import com.opengamma.sesame.function.AvailableOutputs;
import com.opengamma.sesame.function.Parameter;
import com.opengamma.sesame.function.ParameterType;
import com.opengamma.sesame.graph.ArgumentConversionErrorNode;
import com.opengamma.sesame.graph.ArgumentNode;
import com.opengamma.sesame.graph.CannotBuildNode;
import com.opengamma.sesame.graph.ClassNode;
import com.opengamma.sesame.graph.FunctionModel;
import com.opengamma.sesame.graph.FunctionModelNode;
import com.opengamma.sesame.graph.InterfaceNode;
import com.opengamma.sesame.graph.MissingArgumentNode;
import com.opengamma.sesame.graph.MissingConfigNode;
import com.opengamma.sesame.graph.NoImplementationNode;
import com.opengamma.sesame.graph.convert.ArgumentConverter;
import com.opengamma.util.ArgumentChecker;
/**
* Builds maps representing the JSON used in the function configuration web app.
*/
public class ConfigJsonBuilder {
private static final String FUNC = "func";
private static final String IMPL = "impl";
private static final String IMPLS = "impls";
private static final String ARGS = "args";
private static final String NAME = "name";
private static final String VALUE = "value";
private static final String ERROR = "error";
private static final String TYPE = "type";
private static final String COL_NAME = "colName";
private static final String INPUT_TYPES = "inputTypes";
private static final String INPUT_TYPE = "inputType";
private static final String OUTPUT_NAMES = "outputNames";
private static final String OUTPUT_NAME = "outputName";
private static final String FUNCTIONS = "functions";
private static final String CONFIGS = "configs";
private final AvailableOutputs _availableOutputs;
private final AvailableImplementations _availableImplementations;
private final ConfigMaster _configMaster;
private final ArgumentConverter _argumentConverter;
/**
* @param availableOutputs the functions known to the engine that can calculate output values
* @param availableImplementations the function implementations known to the engine
* @param configMaster for looking up configuration
* @param argumentConverter converts arguments to and from strings
*/
ConfigJsonBuilder(AvailableOutputs availableOutputs,
AvailableImplementations availableImplementations,
ConfigMaster configMaster,
ArgumentConverter argumentConverter) {
_argumentConverter = ArgumentChecker.notNull(argumentConverter, "argumentConverter");
_configMaster = ArgumentChecker.notNull(configMaster, "configMaster");
_availableOutputs = ArgumentChecker.notNull(availableOutputs, "availableOutputs");
_availableImplementations = ArgumentChecker.notNull(availableImplementations, "availableImplementations");
}
/**
* Builds a configuration object from JSON produced by the client.
* The expected format of the JSON is:
*
* <pre>
* {
* impls: {interface1: impl1, interface2: impl2, ... },
* args: {
* impl1: {
* propertyName1: arg1,
* propertyName2: arg2,
* ...
* },
* impl2: {
* propertyName3: arg3,
* ...
* },
* ...
* }
* }
* </pre>
*
* @param json JSON representing function configuration
* @return the configuration as an object
* @throws IllegalArgumentException if the JSON doesn't define valid configuration
*/
@SuppressWarnings("unchecked")
public FunctionModelConfig getConfigFromJson(Map<String, Object> json) {
Map<String, String> implsJson = (Map<String, String>) json.get(IMPLS);
Map<Class<?>, Class<?>> impls = new HashMap<>();
for (Map.Entry<String, String> entry : implsJson.entrySet()) {
String fnType = entry.getKey();
String implType = entry.getValue();
if (!StringUtils.isEmpty(fnType) && !StringUtils.isEmpty(implType)) {
try {
impls.put(Class.forName(fnType), Class.forName(implType));
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
}
Map<String, Map<String, String>> argsJson = (Map<String, Map<String, String>>) json.get(ARGS);
Map<Class<?>, FunctionArguments> args = new HashMap<>();
for (Map.Entry<String, Map<String, String>> entry : argsJson.entrySet()) {
String fnTypeName = entry.getKey();
Class<?> fnType;
try {
fnType = Class.forName(fnTypeName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
Map<String, String> fnArgStrs = entry.getValue();
Map<String, Object> fnArgs = new HashMap<>();
for (Map.Entry<String, String> fnArgEntry : fnArgStrs.entrySet()) {
String paramName = fnArgEntry.getKey();
String paramValueStr = fnArgEntry.getValue();
if (StringUtils.isEmpty(paramValueStr)) {
continue;
}
Parameter parameter = Parameter.named(paramName, fnType);
if (EngineUtils.isConfig(parameter.getType())) {
fnArgs.put(paramName, ConfigLink.resolvable(paramValueStr, parameter.getType()));
} else if (_argumentConverter.isConvertible(parameter.getParameterType())) {
fnArgs.put(paramName, _argumentConverter.convertFromString(parameter.getParameterType(), paramValueStr));
} else {
throw new IllegalArgumentException("Cannot convert from string to parameter type " + parameter.getParameterType());
}
}
FunctionArguments simpleFunctionArguments = new FunctionArguments(fnArgs);
args.put(fnType, simpleFunctionArguments);
}
return new FunctionModelConfig(impls, args);
}
/**
* Converts a configuration instance to a map representing some JSON.
* The format of the JSON is:
*
* <pre>
* {
* impls: {interface1: impl1, interface2: impl2, ... },
* args: {
* impl1: {
* propertyName1: arg1,
* propertyName2: arg2,
* ...
* },
* impl2: {
* propertyName3: arg3,
* ...
* },
* ...
* }
* }
* </pre>
*
* @param config some configuration
* @return the configuration as JSON
*/
public Map<String, Object> getJsonFromConfig(FunctionModelConfig config) {
Map<String, Object> jsonMap = new HashMap<>();
Map<String, Object> implsMap = new HashMap<>();
for (Map.Entry<Class<?>, Class<?>> entry : config.getImplementations().entrySet()) {
implsMap.put(entry.getKey().getName(), entry.getValue().getName());
}
jsonMap.put(IMPLS, implsMap);
Map<String, Object> argsMap = new HashMap<>();
for (Map.Entry<Class<?>, FunctionArguments> entry : config.getArguments().entrySet()) {
Map<String, String> fnArgsMap = new HashMap<>();
Class<?> functionType = entry.getKey();
FunctionArguments fnArgs = entry.getValue();
for (Map.Entry<String, Object> argEntry : fnArgs.getArguments().entrySet()) {
String parameterName = argEntry.getKey();
Object argument = argEntry.getValue();
String argumentStr;
Parameter parameter = Parameter.named(parameterName, functionType);
ParameterType parameterType = parameter.getParameterType();
if (_argumentConverter.isConvertible(parameterType)) {
argumentStr = _argumentConverter.convertToString(parameterType, argument);
} else {
argumentStr = argument.toString();
}
fnArgsMap.put(parameterName, argumentStr);
}
String typeName = functionType.getName();
argsMap.put(typeName, fnArgsMap);
}
jsonMap.put(ARGS, argsMap);
return jsonMap;
}
/**
* Returns JSON containing the model for the function configuration page.
* This contains the configuration for a single output associated with a column.
*
* @param columnName the name of the column containing the output
* @param config the configuration
* @param inputType the input type for the top level function
* @param outputName the name of the output calculated by the function
* @param model the function model of a function that can calculate the named output for the specified input
* type, built using the configuration
* @return the page model for displaying and editing the configuration
*/
public Map<String, Object> getConfigPageModel(String columnName,
FunctionModelConfig config,
@Nullable Class<?> inputType,
@Nullable OutputName outputName,
@Nullable FunctionModel model) {
ArgumentChecker.notEmpty(columnName, "columnName");
List<Map<String, Object>> inputTypeList = new ArrayList<>();
// TODO if we're editing an existing config this should either be empty or only contain the selected type
// the user shouldn't be able to change the input type for an existing config, that's part of the key
// TODO if we're adding a new config the types shouldn't include the types for which the column already has config
for (Class<?> type : _availableOutputs.getInputTypes()) {
inputTypeList.add(typeMap(type));
}
Collections.sort(inputTypeList, TypeMapComparator.INSTANCE);
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put(COL_NAME, columnName);
jsonMap.put(INPUT_TYPES, inputTypeList);
if (inputType != null) {
Set<OutputName> availableOutputs = _availableOutputs.getAvailableOutputs(inputType);
List<String> outputNames = new ArrayList<>(availableOutputs.size());
for (OutputName availableOutput : availableOutputs) {
outputNames.add(availableOutput.getName());
}
Collections.sort(outputNames);
// TODO output names needs to be filtered so it only includes names for which no config exists
// need the existing ViewColumn
jsonMap.put(INPUT_TYPE, typeMap(inputType));
jsonMap.put(OUTPUT_NAMES, outputNames);
if (outputName != null && availableOutputs.contains(outputName)) {
jsonMap.put(OUTPUT_NAME, outputName.getName());
}
}
List<Map<String, Object>> functions = getFunctions(config, model);
if (!functions.isEmpty()) {
jsonMap.put(FUNCTIONS, functions);
}
return jsonMap;
}
private List<Map<String, Object>> getFunctions(FunctionModelConfig config, FunctionModel model) {
List<Map<String, Object>> functions = new ArrayList<>();
LinkedHashSet<FunctionModelNode> nodes = flattenModel(model);
for (FunctionModelNode node : nodes) {
if (node instanceof InterfaceNode) {
Class<?> functionType = node.getType();
Class<?> selectedImpl = ((InterfaceNode) node).getImplementationType();
Map<String, Object> map = new HashMap<>();
map.put(FUNC, typeMap(functionType));
map.put(IMPL, typeMap(selectedImpl));
map.put(IMPLS, getImplementations(functionType));
map.put(ARGS, getArguments(config, node.getDependencies()));
functions.add(map);
} else if (node instanceof ClassNode && hasArguments(node)) {
Map<String, Object> map = new HashMap<>();
map.put(IMPL, typeMap(node.getType()));
map.put(ARGS, getArguments(config, node.getDependencies()));
functions.add(map);
} else if (node instanceof NoImplementationNode) {
NoImplementationNode noImplementationNode = (NoImplementationNode) node;
Class<?> functionType = noImplementationNode.getException().getInterfaceType();
Map<String, Object> map = new HashMap<>();
map.put(FUNC, typeMap(functionType));
map.put(IMPLS, getImplementations(functionType));
functions.add(map);
}
}
return functions;
}
private List<Map<String, Object>> getImplementations(Class<?> functionType) {
List<Class<?>> impls = new ArrayList<>(_availableImplementations.getImplementationTypes(functionType));
List<Map<String, Object>> implsList = new ArrayList<>(impls.size());
for (Class<?> impl : impls) {
implsList.add(typeMap(impl));
}
Collections.sort(implsList, TypeMapComparator.INSTANCE);
return implsList;
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> getArguments(FunctionModelConfig config, List<FunctionModelNode> dependencies) {
List<Map<String, Object>> args = new ArrayList<>();
for (FunctionModelNode node : dependencies) {
Parameter parameter = node.getParameter();
String paramName = parameter.getName();
String value;
String errorMessage;
List<String> configNames = new ArrayList<>();
Map<String, Object> map = new HashMap<>();
map.put(NAME, paramName);
map.put(TYPE, parameter.getParameterType().getName());
if (EngineUtils.isConfig(parameter.getType())) {
Class<?> parameterType = parameter.getType();
ConfigSearchRequest<?> searchRequest = new ConfigSearchRequest<>();
searchRequest.setType(parameterType);
ConfigSearchResult<?> searchResult = _configMaster.search(searchRequest);
List<? extends ConfigItem<?>> configItems = searchResult.getValues();
for (ConfigItem<?> configItem : configItems) {
configNames.add(configItem.getName());
}
map.put(CONFIGS, configNames);
}
if (node instanceof ArgumentNode) {
Object argument = config.getFunctionArguments(parameter.getDeclaringClass()).getArgument(paramName);
if (argument == null) {
value = null;
} else if (argument instanceof ConfigLink<?>) {
value = "TODO - need to expose config link name PLAT-6469";
} else if (_argumentConverter.isConvertible(parameter.getParameterType())) {
value = _argumentConverter.convertToString(parameter.getParameterType(), argument);
} else {
value = argument.toString();
}
errorMessage = null;
} else if (node instanceof MissingConfigNode) {
value = null;
if (!configNames.isEmpty()) {
errorMessage = "Configuration required";
} else {
errorMessage = "No configuration available";
}
} else if (node instanceof MissingArgumentNode) {
value = null;
errorMessage = "Value required";
} else if (node instanceof CannotBuildNode) {
value = null;
errorMessage = "Unable to create value";
} else if (node instanceof ArgumentConversionErrorNode) {
ArgumentConversionErrorNode conversionErrorNode = (ArgumentConversionErrorNode) node;
value = conversionErrorNode.getValue();
errorMessage = conversionErrorNode.getErrorMessage();
} else {
continue;
}
if (value != null) {
map.put(VALUE, value);
}
if (errorMessage != null) {
map.put(ERROR, errorMessage);
}
args.add(map);
}
return args;
}
/**
* Returns <code>{name: "type name", type: "fully qualified class name"}</code>
*
* @param type the type
* @return a map containing the type's name and fully qualified class name
*/
private static Map<String, Object> typeMap(Class<?> type) {
return ImmutableMap.<String, Object>of(NAME, getName(type), TYPE, type.getName());
}
/**
* Returns the name for an input type.
* Currently uses the class simple name, but in future could use a value from an annotations. See SSM-224.
*
* @param inputType a type that is the input to a calculation in the engine
* @return the name used for the type in the user interface
*/
static String getName(Class<?> inputType) {
// TODO use annotation if available
return inputType.getSimpleName();
}
private static boolean hasArguments(FunctionModelNode node) {
for (FunctionModelNode childNode : node.getDependencies()) {
if (childNode instanceof ArgumentNode ||
childNode instanceof MissingArgumentNode ||
childNode instanceof MissingConfigNode) {
return true;
}
}
return false;
}
private static LinkedHashSet<FunctionModelNode> flattenModel(@Nullable FunctionModel model) {
if (model == null) {
return new LinkedHashSet<>();
}
LinkedHashSet<FunctionModelNode> nodes = new LinkedHashSet<>();
flattenNode(model.getRoot(), nodes);
return nodes;
}
private static void flattenNode(FunctionModelNode node, Set<FunctionModelNode> accumulator) {
accumulator.add(node);
for (FunctionModelNode childNode : node.getDependencies()) {
flattenNode(childNode, accumulator);
}
}
private static class TypeMapComparator implements Comparator<Map<String, Object>> {
private static final Comparator<Map<String, Object>> INSTANCE = new TypeMapComparator();
@Override
public int compare(Map<String, Object> o1, Map<String, Object> o2) {
String name1 = (String) o1.get(NAME);
String name2 = (String) o2.get(NAME);
return name1.compareTo(name2);
}
}
}