001package org.galaxyproject.gxformat2;
002
003import static org.galaxyproject.dockstore_galaxy_interface.language.GalaxyWorkflowPlugin.LOG;
004
005import com.fasterxml.jackson.databind.ObjectMapper;
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.LinkedHashMap;
010import java.util.List;
011import java.util.Map;
012import java.util.Optional;
013import java.util.Set;
014import java.util.stream.Collectors;
015
016/**
017 * General notes: There's no guarantee that a normalized step has a label, step definition ID or
018 * state
019 */
020public class Cytoscape {
021  public static final ObjectMapper objectMapper = new ObjectMapper();
022  public static final String MAIN_TS_PREFIX = "toolshed.g2.bx.psu.edu/repos/";
023  public static final String START_ID = "UniqueBeginKey";
024  public static final String END_ID = "UniqueEndKey";
025
026  public static Map<String, Object> getElements(final String path) throws Exception {
027    final Map<String, Object> object = (Map<String, Object>) IoUtils.readYamlFromPath(path);
028    return getElements(object);
029  }
030
031  private static class IdAndLabel {
032    private String id;
033    private String label;
034
035    IdAndLabel(String id, String label) {
036      this.setId(id);
037      this.setLabel(label);
038    }
039
040    public String getId() {
041      return id;
042    }
043
044    public void setId(String id) {
045      this.id = id;
046    }
047
048    public String getLabel() {
049      return label;
050    }
051
052    public void setLabel(String label) {
053      this.label = label;
054    }
055  }
056
057  public static Map<String, Object> getElements(final Map<String, Object> object) {
058    final String wfClass = (String) object.get("class");
059    WorkflowAdapter adapter;
060    if (wfClass == null) {
061      adapter = new NativeWorkflowAdapter(object);
062    } else {
063      adapter = new Format2WorkflowAdapter(object);
064    }
065    final Map<String, Object> elements = new HashMap<>();
066    final List<Object> nodeElements = new ArrayList<>();
067    final List<Object> edgeElements = new ArrayList<>();
068    elements.put("nodes", nodeElements);
069    elements.put("edges", edgeElements);
070    nodeElements.add(createStartNode());
071    nodeElements.add(createEndNode());
072    List<WorkflowAdapter.NormalizedStep> normalizedSteps = adapter.normalizedSteps();
073    // Step definition ID is not really a perfect identifier because it may not exist, using label
074    // as an identifier otherwise
075    // TODO: Need to create another field that actually uniquely identifies the step (but still able
076    // to map to input connections and state
077    normalizedSteps.forEach(
078        normalizedStep -> {
079          if (normalizedStep.stepDefinition.get("id") == null) {
080            normalizedStep.stepDefinition.put("id", normalizedStep.label);
081          }
082        });
083    Set<String> endNodeIds =
084        normalizedSteps.stream()
085            .map(normalizedStep -> (normalizedStep.stepDefinition.get("id")).toString())
086            .collect(Collectors.toSet());
087    Set<IdAndLabel> allNodesIdsAndLabels =
088        normalizedSteps.stream()
089            .map(
090                normalizedStep -> {
091                  Map<String, Object> stepDefinition = normalizedStep.stepDefinition;
092                  String id = (stepDefinition.get("id")).toString();
093                  String label =
094                      stepDefinition.get("label") != null
095                          ? stepDefinition.get("label").toString()
096                          : null;
097                  return new IdAndLabel(id, label);
098                })
099            .collect(Collectors.toSet());
100    for (final WorkflowAdapter.NormalizedStep normalizedStep : normalizedSteps) {
101      Map<String, Object> stepDefinition = normalizedStep.stepDefinition;
102      final Map<String, Object> step = stepDefinition;
103      String stepId = (step.get("id")).toString();
104      String stepType = step.get("type") != null ? (String) step.get("type") : "tool";
105      List<String> classes = new ArrayList<>(Collections.singletonList("type_" + stepType));
106      if (stepType.equals("tool") || stepType.equals("subworkflow")) {
107        classes.add("runnable");
108      } else {
109        classes.add("input");
110      }
111
112      // It's not an end step if there's another step with a state that includes the node
113      Object state = normalizedStep.stepDefinition.get("state");
114      if (state != null) {
115        LinkedHashMap linkedHashMapstate = (LinkedHashMap) state;
116        linkedHashMapstate.keySet().forEach(key -> endNodeIds.remove(key));
117      }
118      String toolId = (String) step.get("tool_id");
119      if (toolId != null && toolId.startsWith(MAIN_TS_PREFIX)) {
120        toolId = toolId.substring(MAIN_TS_PREFIX.length());
121      }
122      String label = normalizedStep.label;
123      if ((label == null || isOrderIndexLabel(label)) && toolId != null) {
124        label = "tool:" + toolId;
125      }
126      final String repoLink;
127      if (step.containsKey("tool_shed_repository")) {
128        final Map<String, String> repo = (Map<String, String>) step.get("tool_shed_repository");
129        repoLink =
130            "https://"
131                + repo.get("tool_shed")
132                + "/view/"
133                + repo.get("owner")
134                + "/"
135                + repo.get("name")
136                + "/"
137                + repo.get("changeset_revision");
138      } else {
139        repoLink = null;
140      }
141      final Map<String, Object> nodeData = new HashMap<>();
142      nodeData.put("id", stepId);
143      nodeData.put("label", label);
144      // dockstore displays name, docker, type, tool, and run
145      nodeData.put("name", label);
146      // TODO: detect Docker image properly
147      nodeData.put("docker", "TBD");
148      nodeData.put("run", "TBD");
149
150      nodeData.put("tool_id", step.get("tool_id"));
151      nodeData.put("doc", normalizedStep.doc);
152      nodeData.put("repo_link", repoLink);
153      nodeData.put("step_type", stepType);
154      nodeData.put("type", stepType);
155      final Map<String, Object> nodeElement = new HashMap<>();
156      nodeElement.put("group", "nodes");
157      nodeElement.put("data", nodeData);
158      nodeElement.put("classes", classes);
159      nodeElement.put("position", elementPosition(step));
160      nodeElements.add(nodeElement);
161
162      // Create edge from start node if there are:
163      // 1. no input connections or empty input connection
164      // 2. no inputs
165      // 3. no state (questionable)
166      Object inputConnections = stepDefinition.get("input_connections");
167      if (state == null
168          && (normalizedStep.inputs.isEmpty()
169              && (inputConnections == null || inputConnections.toString().equals("{}")))) {
170        edgeElements.add(createEdge(START_ID, stepId));
171      }
172      if (state != null) {
173        LinkedHashMap linkedHashMapstate = (LinkedHashMap) state;
174        Set<String> keySet = linkedHashMapstate.keySet();
175        for (String key : keySet) {
176          createEdges(key, stepId, allNodesIdsAndLabels, edgeElements, endNodeIds, key, stepId);
177        }
178      }
179
180      for (final WorkflowAdapter.Input input : normalizedStep.inputs) {
181        createEdges(
182            input.sourceStepLabel,
183            stepId,
184            allNodesIdsAndLabels,
185            edgeElements,
186            endNodeIds,
187            input.inputName,
188            input.sourceOutputName);
189      }
190    }
191    // Create edges for end nodes to direct to the real end node
192    endNodeIds.forEach(endNodeId -> edgeElements.add(createEdge(endNodeId, END_ID)));
193    return elements;
194  }
195
196  private static void createEdges(
197      String sourceStepLabel,
198      String stepId,
199      Set<IdAndLabel> allNodesIdsAndLabels,
200      final List<Object> edgeElements,
201      Set<String> endNodeIds,
202      String inputName,
203      String outputName) {
204    final String edgeId = stepId + "__to__" + sourceStepLabel;
205    final Map<String, Object> edgeData = new HashMap<>();
206    edgeData.put("id", edgeId);
207    // Look up what the step ID is based on sourceStepLabel which is either the step ID itself
208    // or the step label
209    Optional<IdAndLabel> sourceId =
210        allNodesIdsAndLabels.stream()
211            .filter(
212                partialStep -> {
213                  boolean isLabel = sourceStepLabel.equals(partialStep.getLabel());
214                  boolean isId = sourceStepLabel.equals(partialStep.getId());
215                  return isLabel || isId;
216                })
217            .findFirst();
218    if (sourceId.isPresent()) {
219      edgeData.put("source", sourceId.get().id);
220      // Any node that's a source of an edge is not an end node
221      endNodeIds.remove(sourceId.get().id);
222      edgeData.put("target", stepId);
223      edgeData.put("input", inputName);
224      edgeData.put("output", outputName);
225      final Map<String, Object> edgeElement = new HashMap<>();
226      edgeElement.put("group", "edges");
227      edgeElement.put("data", edgeData);
228      edgeElements.add(edgeElement);
229    } else {
230      String errorMessage =
231          String.format("Could not find input \"%s\" from the workflow steps.", sourceStepLabel);
232      LOG.error(errorMessage);
233    }
234  }
235
236  static boolean isOrderIndexLabel(final String label) {
237    try {
238      Integer.parseInt(label);
239      return true;
240    } catch (NumberFormatException e) {
241      return false;
242    }
243  }
244
245  private static CytoscapeDAG.Edge createEdge(String source, String target) {
246    String id = source + "__to__" + target;
247    CytoscapeDAG.Edge edge = new CytoscapeDAG.Edge();
248    CytoscapeDAG.Edge.EdgeData edgeData = new CytoscapeDAG.Edge.EdgeData();
249    edgeData.setId(id);
250    edgeData.setSource(source);
251    edgeData.setTarget(target);
252    edge.setData(edgeData);
253    return edge;
254  }
255
256  private static Map<String, Long> elementPosition(final Map<String, Object> step) {
257    Map<String, Object> stepPosition = (Map<String, Object>) step.get("position");
258    Map<String, Long> elementPosition = new HashMap();
259    elementPosition.put("x", getIntegerValue(stepPosition, "left"));
260    elementPosition.put("y", getIntegerValue(stepPosition, "top"));
261    return elementPosition;
262  }
263
264  private static Long getIntegerValue(final Map<String, Object> fromMap, final String key) {
265    final Object value = fromMap.get(key);
266    if (value instanceof Float || value instanceof Double) {
267      return (long) Math.floor((double) value);
268    } else if (value instanceof Integer) {
269      return ((Integer) value).longValue();
270    } else {
271      return (long) value;
272    }
273  }
274
275  private static Map<String, Object> createStartNode() {
276    return createStartOrEndNode(START_ID);
277  }
278
279  private static Map<String, Object> createEndNode() {
280    return createStartOrEndNode(END_ID);
281  }
282
283  private static Map<String, Object> createStartOrEndNode(String id) {
284    CytoscapeDAG.Node.NodeData nodeData = new CytoscapeDAG.Node.NodeData();
285    nodeData.setId(id);
286    nodeData.setName(id);
287    CytoscapeDAG.Node node = new CytoscapeDAG.Node();
288    node.setData(nodeData);
289    return objectMapper.convertValue(node, Map.class);
290  }
291}