/**
* 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.opengamma.DataDuplicationException;
import com.opengamma.core.config.impl.ConfigItem;
import com.opengamma.id.UniqueId;
import com.opengamma.master.config.ConfigDocument;
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.FunctionModelConfig;
import com.opengamma.sesame.config.ViewColumn;
import com.opengamma.sesame.config.ViewOutput;
import com.opengamma.sesame.function.AvailableImplementations;
import com.opengamma.sesame.function.AvailableOutputs;
import com.opengamma.sesame.function.FunctionMetadata;
import com.opengamma.sesame.graph.FunctionModel;
import com.opengamma.sesame.graph.NodeDecorator;
import com.opengamma.sesame.graph.convert.ArgumentConverter;
import com.opengamma.sesame.graph.convert.DefaultArgumentConverter;
import com.opengamma.util.ArgumentChecker;
/**
* REST resource providing endpoints for the column configuration webapp.
*/
@SuppressWarnings("unchecked")
public class ColumnConfigResource {
private static final Logger s_logger = LoggerFactory.getLogger(ColumnConfigResource.class);
private final AvailableOutputs _availableOutputs;
private final Set<Class<?>> _availableComponents;
private final ConfigJsonBuilder _jsonBuilder;
private final FunctionModelConfig _defaultConfig;
private final Gson _gson = new Gson();
private final FunctionModelConfig _defaultImpls;
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 availableComponents the types of the components available in the engine
* @param defaultConfig the default configuration used as the starting point when new configuration is created
* @param configMaster for looking up configuration
* @param argumentConverter converts arguments to and from strings
*/
public ColumnConfigResource(AvailableOutputs availableOutputs,
AvailableImplementations availableImplementations,
Set<Class<?>> availableComponents,
FunctionModelConfig defaultConfig,
ArgumentConverter argumentConverter,
ConfigMaster configMaster) {
_defaultConfig = ArgumentChecker.notNull(defaultConfig, "defaultConfig");
_argumentConverter = ArgumentChecker.notNull(argumentConverter, "argumentConverter");
_configMaster = ArgumentChecker.notNull(configMaster, "configMaster");
_availableOutputs = ArgumentChecker.notNull(availableOutputs, "availableOutputs");
_availableComponents = ArgumentChecker.notNull(availableComponents, "availableComponents");
_jsonBuilder = new ConfigJsonBuilder(availableOutputs, availableImplementations, configMaster, new DefaultArgumentConverter());
_defaultImpls = new FunctionModelConfig(availableImplementations.getDefaultImplementations());
}
/**
* Returns a map of containing the details of all {@link ViewColumn} instances in the config database.
* The structure is:
* <pre>
* {columns: [{name: 'column name', id: 'columnUniqueId'}, ...]}
* </pre>
*
* @return a map of containing the details of all {@link ViewColumn} instances in the config database
*/
public Map<String, Object> getColumnsPageModel() {
ConfigSearchRequest<ViewColumn> searchRequest = new ConfigSearchRequest<>();
searchRequest.setType(ViewColumn.class);
ConfigSearchResult<ViewColumn> searchResult = _configMaster.search(searchRequest);
List<Map<String, Object>> columns = new ArrayList<>();
for (ConfigItem<ViewColumn> configItem : searchResult.getValues()) {
String columnName = configItem.getName();
UniqueId columnId = configItem.getUniqueId();
columns.add(ImmutableMap.<String, Object>of("name", columnName, "id", columnId.getObjectId().toString()));
}
// don't really need this but strictly speaking a naked array isn't valid JSON
return ImmutableMap.<String, Object>of("columns", columns);
}
/**
* Returns a map of containing the details of a single {@link ViewColumn} instance from the config database.
* The structure is:
* <pre>
* {
* name: 'column name',
* id: 'columnUniqueId',
* inputTypes: [{name: 'input type name', type: 'input type class'}, ...]
* }
* </pre>
*
* @return a map of containing the details of all {@link ViewColumn} instances in the config database
*/
public Map<String, Object> getColumnPageModel(UniqueId columnId) {
ViewColumn column = loadColumn(columnId);
List<Map<String, Object>> inputTypes = new ArrayList<>(column.getOutputs().size());
for (Class<?> inputType : column.getOutputs().keySet()) {
inputTypes.add(ImmutableMap.<String, Object>of("name", ConfigJsonBuilder.getName(inputType), "type", inputType.getName()));
}
return ImmutableMap.of("inputTypes", inputTypes, "name", column.getName(), "id", columnId.toString());
}
/**
* Returns a map containing the model for the column configuration page.
* The structure is:
* <pre>
*
* </pre>
*
* @param columnId the ID of the column
* @param configJsonStr the column config from the client
* @return the model JSON for the column configuration page
*/
public Map<String, Object> getConfigPageModel(UniqueId columnId, String configJsonStr) {
ArgumentChecker.notEmpty(configJsonStr, "configJsonStr");
ArgumentChecker.notNull(columnId, "columnId");
ViewColumn column = loadColumn(columnId);
Class<?> inputType;
FunctionModelConfig config;
OutputName outputName;
FunctionModel model;
Map<String, Object> configJson;
try {
configJson = _gson.fromJson(configJsonStr, Map.class);
} catch (JsonSyntaxException e) {
throw new IllegalArgumentException(e);
}
String inputTypeName = (String) configJson.get("inputType");
if (!StringUtils.isEmpty(inputTypeName)) {
inputType = loadInputType(inputTypeName);
String outputNameStr = (String) configJson.get("outputName");
FunctionMetadata outputFunction;
if (StringUtils.isEmpty(outputNameStr)) {
// use the existing output name, possibly null
outputName = column.getOutputName(inputType);
} else {
// use the output name from the client
outputName = OutputName.of(outputNameStr);
}
if (outputName != null) {
outputFunction = _availableOutputs.getOutputFunction(outputName, inputType);
config = _jsonBuilder.getConfigFromJson(configJson);
model = FunctionModel.forFunction(outputFunction, config, _availableComponents, NodeDecorator.IDENTITY, _argumentConverter);
} else {
config = FunctionModelConfig.EMPTY;
model = null;
}
} else {
inputType = null;
outputName = null;
config = null;
model = null;
}
return _jsonBuilder.getConfigPageModel(column.getName(), config, inputType, outputName, model);
}
/**
* Saves configuration for a single output function to the config master.
* The expected format of the JSON is
*
* <pre>
* {
* impls: {interface1: impl1, interface2: impl2, ... },
* args: {
* impl1: {
* propertyName1: arg1,
* propertyName2: arg2,
* ...
* },
* impl2: {
* propertyName3: arg3,
* ...
* },
* ...
* }
* }
* </pre>
*
* @param columnId unique ID of the column which owns the output
* @param inputTypeName the fully qualified name of the input type to the function
* @param body the body of the request containing the configuration as JSON
* @return unique ID of the updated column
*/
public UniqueId saveConfig(UniqueId columnId, String inputTypeName, String body) {
Map<String, Object> inputJson;
try {
inputJson = _gson.fromJson(body, Map.class);
} catch (JsonSyntaxException e) {
throw new IllegalArgumentException(e);
}
FunctionModelConfig config = _jsonBuilder.getConfigFromJson(inputJson);
String outputName = (String) inputJson.get("outputName");
if (outputName == null) {
throw new IllegalArgumentException("No output name specified");
}
ConfigItem<ViewColumn> configItem = loadColumnConfigItem(columnId);
ViewColumn column = configItem.getValue();
Class<?> inputType = loadInputType(inputTypeName);
Map<Class<?>, ViewOutput> outputs = column.getOutputs();
ViewOutput output = new ViewOutput(OutputName.of(outputName), config);
Map<Class<?>, ViewOutput> newOutputs = new HashMap<>();
newOutputs.putAll(outputs);
newOutputs.put(inputType, output);
ViewColumn newColumn = column.toBuilder().outputs(newOutputs).build();
ConfigDocument document = new ConfigDocument(ConfigItem.of(newColumn, newColumn.getName()));
document.setUniqueId(configItem.getUniqueId());
ConfigDocument newDocument = _configMaster.update(document);
UniqueId newId = newDocument.getUniqueId();
s_logger.debug("Saved column with ID {}, {}", newId, newColumn);
return newId;
}
/**
* Gets the default configuration as a map, corresponds to {@link FunctionModelConfig}.
* This is built from system default configuration combined with the default implementations that can be inferred
* where there is only one known implementation of an interface. The format is
*
* <pre>
* {
* impls: {interface1: impl1, interface2: impl2, ... },
* args: {
* impl1: {
* propertyName1: arg1,
* propertyName2: arg2,
* ...
* },
* impl2: {
* propertyName3: arg3,
* ...
* },
* ...
* }
* }
* </pre>
*
* @return the default configuration as a map
*/
public Map<String, Object> getDefaultConfig() {
Map<Class<?>, Class<?>> impls = Maps.newHashMap(_defaultImpls.getImplementations());
impls.putAll(_defaultConfig.getImplementations());
return _jsonBuilder.getJsonFromConfig(new FunctionModelConfig(impls, _defaultConfig.getArguments()));
}
/**
* Gets the configuration for a column and input type as a map, corresponds to {@link FunctionModelConfig}.
* If there is no configuration for the column and type the {@link #getDefaultConfig() default configuration}
* is returned. The existing configuration is merged with the current defaults with the existing configuration
* taking priority. The format is
*
* <pre>
* {
* impls: {interface1: impl1, interface2: impl2, ... },
* args: {
* impl1: {
* propertyName1: arg1,
* propertyName2: arg2,
* ...
* },
* impl2: {
* propertyName3: arg3,
* ...
* },
* ...
* }
* }
* </pre>
*
* @param columnId ID of the column
* @param inputTypeName the fully qualified name of the input type
* @return the configuration as a map
*/
public Map<String, Object> getConfig(UniqueId columnId, String inputTypeName) {
ViewColumn column = loadColumn(columnId);
Class<?> inputType = loadInputType(inputTypeName);
FunctionModelConfig config = column.getFunctionConfig(inputType);
if (config != null) {
// compose the implementations from the defaults into the existing config
Map<Class<?>, Class<?>> impls = Maps.newHashMap(_defaultImpls.getImplementations());
impls.putAll(_defaultConfig.getImplementations());
impls.putAll(config.getImplementations());
FunctionModelConfig defaultConfig = new FunctionModelConfig(impls, _defaultConfig.getArguments());
return _jsonBuilder.getJsonFromConfig(config.mergedWith(defaultConfig));
} else {
return getDefaultConfig();
}
}
/**
* Adds a new column.
*
* @param name the column name, not empty
* @return the
*/
public UniqueId addColumn(String name) {
ArgumentChecker.notEmpty(name, "name");
ConfigSearchRequest<ViewColumn> searchRequest = new ConfigSearchRequest<>();
searchRequest.setType(ViewColumn.class);
searchRequest.setName(name);
ConfigSearchResult<ViewColumn> searchResult = _configMaster.search(searchRequest);
if (!searchResult.getValues().isEmpty()) {
throw new DataDuplicationException("A column already exists with the name '" + name + "'");
}
ViewColumn column = new ViewColumn(name, null, Collections.<Class<?>, ViewOutput>emptyMap());
ConfigDocument document = _configMaster.add(new ConfigDocument(ConfigItem.of(column, name)));
return document.getUniqueId();
}
/**
* Deletes a column.
*
* @param columnId the ID of the column
*/
public void deleteColumn(UniqueId columnId) {
_configMaster.remove(columnId);
}
/**
* Deletes the configuration for an output.
*
* @param columnId the column containing the configuration
* @param inputTypeName the input type whose configuration should be deleted
*/
public void deleteConfig(UniqueId columnId, String inputTypeName) {
Class<?> inputType = loadInputType(inputTypeName);
ConfigItem<ViewColumn> configItem = loadColumnConfigItem(columnId);
ViewColumn column = configItem.getValue();
Map<Class<?>, ViewOutput> outputs = new HashMap<>(column.getOutputs());
ViewOutput removed = outputs.remove(inputType);
if (removed != null) {
ViewColumn updatedColumn = column.toBuilder().outputs(outputs).build();
ConfigItem<ViewColumn> updatedItem = ConfigItem.of(updatedColumn, updatedColumn.getName());
updatedItem.setUniqueId(configItem.getUniqueId());
_configMaster.update(new ConfigDocument(updatedItem));
}
}
private Class<?> loadInputType(String inputTypeName) {
try {
return Class.forName(inputTypeName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
private ViewColumn loadColumn(UniqueId columnId) {
Object value = _configMaster.get(columnId).getValue().getValue();
if (!(value instanceof ViewColumn)) {
throw new IllegalArgumentException("ID " + columnId + " refers to an instance of " + value.getClass().getName());
}
return (ViewColumn) value;
}
private ConfigItem<ViewColumn> loadColumnConfigItem(UniqueId columnId) {
ConfigItem<?> configItem = _configMaster.get(columnId).getValue();
Object value = configItem.getValue();
if (!(value instanceof ViewColumn)) {
throw new IllegalArgumentException("ID " + columnId + " refers to an instance of " + value.getClass().getName());
}
return (ConfigItem<ViewColumn>) configItem;
}
}