package org.cytoscape.rest.internal.resource;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Singleton;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.cytoscape.model.CyNetwork;
import org.cytoscape.rest.internal.EdgeBundler;
import org.cytoscape.rest.internal.datamapper.MapperUtil;
import org.cytoscape.task.NetworkTaskFactory;
import org.cytoscape.view.layout.CyLayoutAlgorithm;
import org.cytoscape.view.layout.CyLayoutAlgorithmManager;
import org.cytoscape.view.model.CyNetworkView;
import org.cytoscape.view.vizmap.VisualStyle;
import org.cytoscape.work.TaskIterator;
import org.cytoscape.work.TaskMonitor;
import org.cytoscape.work.Tunable;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
*
* Algorithmic resources.
* Runs Cytoscape tasks, such as layouts or apply Style.
*
*/
@Singleton
@Path("/v1/apply")
public class AlgorithmicResource extends AbstractResource {
@Context
@NotNull
private TaskMonitor headlessTaskMonitor;
@Context
@NotNull
private CyLayoutAlgorithmManager layoutManager;
@Context
@NotNull
private NetworkTaskFactory fitContent;
@Context
@NotNull
private EdgeBundler edgeBundler;
/**
*
* @summary Apply layout to a network
*
* @param algorithmName
* Name of layout algorithm ("circular", "force-directed", etc.)
* @param networkId
* Target network SUID
* @param column (URL Query Parameter)
* Column name to be used by the layout algorithm
*
* @return Success message
*/
@GET
@Path("/layouts/{algorithmName}/{networkId}")
@Produces(MediaType.APPLICATION_JSON)
public Response applyLayout(
@PathParam("algorithmName") String algorithmName,
@PathParam("networkId") Long networkId,
@QueryParam("column") String column) {
final CyNetwork network = getCyNetwork(networkId);
final Collection<CyNetworkView> views =
this.networkViewManager.getNetworkViews(network);
if (views.isEmpty()) {
throw new NotFoundException(
"Could not find view for the network with SUID: "
+ networkId);
}
final CyNetworkView view = views.iterator().next();
final CyLayoutAlgorithm layout = this.layoutManager.getLayout(algorithmName);
if (layout == null) {
throw new NotFoundException("No such layout algorithm: " + algorithmName);
}
String columnForLayout = column;
if(columnForLayout == null) {
columnForLayout = "";
}
final TaskIterator itr = layout.createTaskIterator(view,
layout.getDefaultLayoutContext(),
CyLayoutAlgorithm.ALL_NODE_VIEWS, columnForLayout);
try {
itr.next().run(headlessTaskMonitor);
} catch (Exception e) {
throw getError("Could not apply layout.", e,
Response.Status.INTERNAL_SERVER_ERROR);
}
final Map<String, String> successMessage = new HashMap<String, String>();
successMessage.put("message", "Layout finished.");
return Response.status(Response.Status.OK)
.entity(successMessage)
.type(MediaType.APPLICATION_JSON).build();
}
/**
* The return value is an map of all layout details,
* and each of parameter entry includes:
*
* <ul>
* <li>name: Unique name (ID) of the parameter</li>
* <li>description: Description for the parameter</li>
* <li>type: Java data type of the parameter</li>
* <li>value: current value for the parameter field</li>
* </ul>
*
* @summary Get layout parameters for the algorithm
*
* @param algorithmName Name of the layout algorithm
*
* @return Editable layout parameters
*/
@GET
@Path("/layouts/{algorithmName}")
@Produces(MediaType.APPLICATION_JSON)
public Response getLayout(@PathParam("algorithmName") String algorithmName) {
final CyLayoutAlgorithm layout = this.layoutManager.getLayout(algorithmName);
if (layout == null) {
throw new NotFoundException("No such layout algorithm: " + algorithmName);
}
Map<String, Object> params;
try {
params = getLayoutDetails(layout);
} catch (Exception e) {
throw getError("Could not get layout parameters.", e,
Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.status(Response.Status.OK)
.entity(params)
.type(MediaType.APPLICATION_JSON).build();
}
/**
* @summary Returns layout parameter list
*
* @param algorithmName Name of layout algorithm
*
* @return All editable parameters for this algorithm.
*/
@GET
@Path("/layouts/{algorithmName}/parameters")
@Produces(MediaType.APPLICATION_JSON)
public Response getLayoutParameters(@PathParam("algorithmName") String algorithmName) {
final CyLayoutAlgorithm layout = this.layoutManager.getLayout(algorithmName);
if (layout == null) {
throw new NotFoundException("No such layout algorithm: " + algorithmName);
}
final List<Map<String, Object>> params;
try {
params = getLayoutParameterDetails(layout);
} catch (Exception e) {
throw getError("Could not get layout parameters.", e,
Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.status(Response.Status.OK)
.entity(params)
.type(MediaType.APPLICATION_JSON).build();
}
/**
* @summary Column data types compatible with this algorithm
*
* @param algorithmName Name of layout algorithm
*
* @return List of all compatible column data types
*/
@GET
@Path("/layouts/{algorithmName}/columntypes")
@Produces(MediaType.APPLICATION_JSON)
public Response getCompatibleColumnDataTypes(
@PathParam("algorithmName") String algorithmName) {
final CyLayoutAlgorithm layout = this.layoutManager.getLayout(algorithmName);
if (layout == null) {
throw new NotFoundException("No such layout algorithm: " + algorithmName);
}
final Map<String, Set<String>> result = getCompatibleTypes(layout);
return Response.status(Response.Status.OK)
.entity(result)
.type(MediaType.APPLICATION_JSON).build();
}
private Map<String, Set<String>> getCompatibleTypes(final CyLayoutAlgorithm layout) {
final Map<String, Set<String>> result = new HashMap<>();
Set<Class<?>> compatibleTypes = Collections.emptySet();
if (layout.getSupportedNodeAttributeTypes().isEmpty() == false) {
compatibleTypes = layout.getSupportedNodeAttributeTypes();
final Set<String> nodeSet = compatibleTypes.stream().map(type -> type.getSimpleName())
.collect(Collectors.toSet());
result.put("compatibleNodeColumnDataTypes", nodeSet);
}
if (layout.getSupportedEdgeAttributeTypes().isEmpty() == false) {
compatibleTypes = layout.getSupportedEdgeAttributeTypes();
final Set<String> edgeSet = compatibleTypes.stream().map(type -> type.getSimpleName())
.collect(Collectors.toSet());
result.put("compatibleEdgeColumnDataTypes", edgeSet);
}
return result;
}
/**
* The body of your request should contain an array of new parameters.
* The data should look like the following:
* <br/>
*
* [
* {
* "name": nodeHorizontalSpacing,
* "value": 40.0
* }, ...
* ]
*
* where:
* <ul>
* <li>name: Unique name (ID) of the parameter</li>
* <li>value: New value for the parameter field</li>
* </ul>
*
* @summary Update layout parameters for the algorithm
*
* @param algorithmName Name of the layout algorithm
*
* @return Response code 200 if success
*/
@PUT
@Path("/layouts/{algorithmName}/parameters")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateLayoutParameters(@PathParam("algorithmName") String algorithmName, final InputStream is) {
final ObjectMapper objMapper = new ObjectMapper();
final CyLayoutAlgorithm layout = this.layoutManager.getLayout(algorithmName);
final Object context = layout.getDefaultLayoutContext();
try {
final Map<String, Class<?>> params = getParameterTypes(layout);
// This should be an JSON array.
final JsonNode rootNode = objMapper.readValue(is, JsonNode.class);
for (final JsonNode entry : rootNode) {
final String parameterName = entry.get("name").asText();
final Class<?> type = params.get(parameterName);
if(type == null) {
throw new NotFoundException("No such parameter: " + parameterName);
}
final JsonNode val = entry.get("value");
final Object value = MapperUtil.getValue(val, params.get(parameterName));
context.getClass().getField(parameterName).set(context, value);
}
} catch (Exception e) {
throw getError(
"Could not parse the input JSON for updating view because: "
+ e.getMessage(), e,
Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.status(Response.Status.OK)
.type(MediaType.APPLICATION_JSON).build();
}
private final Map<String, Class<?>> getParameterTypes(final CyLayoutAlgorithm layout) {
final Object context = layout.getDefaultLayoutContext();
final List<Field> fields = Arrays.asList(context.getClass().getFields());
return fields.stream()
.filter(field -> field.getAnnotation(Tunable.class) != null)
.collect(Collectors.toMap(Field::getName, Field::getType));
}
private final Map<String, Object> getLayoutDetails(final CyLayoutAlgorithm layout) throws NoSuchFieldException, SecurityException {
final Map<String, Object> layoutInfo = new HashMap<String, Object>();
layoutInfo.put("name", layout.getName());
layoutInfo.put("longName", layout.toString());
layoutInfo.put("parameters", getLayoutParameterDetails(layout));
layoutInfo.put("compatibleColumnDataTypes", getCompatibleTypes(layout));
return layoutInfo;
}
private final List<Map<String, Object>> getLayoutParameterDetails(final CyLayoutAlgorithm layout) {
return Arrays.asList(
layout.getDefaultLayoutContext().getClass().getFields()
).stream()
.filter(field -> field.getAnnotation(Tunable.class) != null)
.map(field -> buildLayoutParamEntry(layout, field))
.collect(Collectors.toList());
}
private final Map<String, Object> buildLayoutParamEntry(final CyLayoutAlgorithm layout, final Field field) {
final Map<String, Object> entryMap = new HashMap<>();
final Tunable tunable = field.getAnnotation(Tunable.class);
entryMap.put("name", field.getName());
entryMap.put("description", tunable.description());
entryMap.put("type", field.getType().getSimpleName());
try {
entryMap.put("value", field.get(layout.getDefaultLayoutContext()));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return entryMap;
}
/**
*
* @summary Apply Visual Style to a network
*
* @param styleName
* Visual Style name (title)
* @param networkId
* Target network SUID
*
* @return Success message.
*/
@GET
@Path("/styles/{styleName}/{networkId}")
@Produces(MediaType.APPLICATION_JSON)
public Response applyStyle(@PathParam("styleName") String styleName,
@PathParam("networkId") Long networkId) {
final CyNetwork network = getCyNetwork(networkId);
final Set<VisualStyle> styles = vmm.getAllVisualStyles();
VisualStyle targetStyle = null;
for (final VisualStyle style : styles) {
final String name = style.getTitle();
if (name.equals(styleName)) {
targetStyle = style;
break;
}
}
if (targetStyle == null) {
throw new NotFoundException("Visual Style does not exist: "
+ styleName);
}
Collection<CyNetworkView> views = this.networkViewManager
.getNetworkViews(network);
if (views.isEmpty()) {
throw new NotFoundException(
"Network view does not exist for the network with SUID: "
+ networkId);
}
final CyNetworkView view = views.iterator().next();
vmm.setVisualStyle(targetStyle, view);
vmm.setCurrentVisualStyle(targetStyle);
targetStyle.apply(view);
return Response.status(Response.Status.OK)
.type(MediaType.APPLICATION_JSON)
.entity("{\"message\":\"Visual Style applied.\"}").build();
}
/**
*
* Fit an existing network view to current window.
*
* @summary Fit network to the window
*
* @param networkId
* Network SUID
* @return Success message
*/
@GET
@Path("/fit/{networkId}")
@Produces(MediaType.APPLICATION_JSON)
public Response fitContent(@PathParam("networkId") Long networkId) {
final CyNetwork network = getCyNetwork(networkId);
Collection<CyNetworkView> views = this.networkViewManager
.getNetworkViews(network);
if (views.isEmpty()) {
throw new NotFoundException(
"Network view does not exist for the network with SUID: "
+ networkId);
}
TaskIterator fit = fitContent.createTaskIterator(network);
try {
fit.next().run(headlessTaskMonitor);
} catch (Exception e) {
throw getError("Could not fit content.", e,
Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.status(Response.Status.OK)
.type(MediaType.APPLICATION_JSON)
.entity("{\"message\":\"Fit content success.\"}").build();
}
/**
* Apply edge bundling with default parameters. Currently optional
* parameters are not supported.
*
* @summary Apply Edge Bundling to a network
*
* @param networkId
* Target network SUID
* @return Success message
*
*/
@GET
@Path("/edgebundling/{networkId}")
@Produces(MediaType.APPLICATION_JSON)
public Response bundleEdge(@PathParam("networkId") Long networkId) {
final CyNetwork network = getCyNetwork(networkId);
Collection<CyNetworkView> views = this.networkViewManager
.getNetworkViews(network);
if (views.isEmpty()) {
throw new NotFoundException(
"Network view does not exist for the network with SUID: "
+ networkId);
}
final TaskIterator bundler = edgeBundler.getBundlerTF()
.createTaskIterator(network);
try {
bundler.next().run(headlessTaskMonitor);
} catch (Exception e) {
throw getError("Could not finish edge bundling.", e,
Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.status(Response.Status.OK)
.type(MediaType.APPLICATION_JSON)
.entity("{\"message\":\"Edge bundling success.\"}").build();
}
/**
* List of all available layout algorithm names.
*
* <h3>Important Note</h3>
* This <strong>does not include yFiles layout algorithms</strong>
* due to license issues.
*
* @summary Get list of available layout algorithm names
*
* @return List of layout algorithm names.
*/
@GET
@Path("/layouts")
@Produces(MediaType.APPLICATION_JSON)
public Collection<String> getLayoutNames() {
return layoutManager.getAllLayouts().stream()
.map(layout->layout.getName())
.collect(Collectors.toList());
}
/**
* Get list of all Visual Style names.
* Style names may not be unique.
*
* @summary Get list of all Visual Style names
*
* @return List of Visual Style names.
*/
@GET
@Path("/styles")
@Produces(MediaType.APPLICATION_JSON)
public Collection<String> getStyleNames() {
return vmm.getAllVisualStyles().stream()
.map(style->style.getTitle())
.collect(Collectors.toList());
}
}