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}