001package org.galaxyproject.dockstore_galaxy_interface.language; 002 003import static org.galaxyproject.gxformat2.Cytoscape.END_ID; 004import static org.galaxyproject.gxformat2.Cytoscape.START_ID; 005 006import com.fasterxml.jackson.databind.ObjectMapper; 007import com.google.common.collect.Lists; 008import com.google.common.collect.Sets; 009import io.dockstore.common.DescriptorLanguage; 010import io.dockstore.common.VersionTypeValidation; 011import io.dockstore.language.CompleteLanguageInterface; 012import io.dockstore.language.RecommendedLanguageInterface; 013import java.net.MalformedURLException; 014import java.net.URL; 015import java.nio.file.Path; 016import java.nio.file.Paths; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020import java.util.Optional; 021import java.util.Set; 022import java.util.regex.Pattern; 023import java.util.stream.Collectors; 024import org.apache.commons.lang3.ObjectUtils; 025import org.galaxyproject.gxformat2.Cytoscape; 026import org.galaxyproject.gxformat2.Lint; 027import org.galaxyproject.gxformat2.LintContext; 028import org.pf4j.Extension; 029import org.pf4j.Plugin; 030import org.pf4j.PluginWrapper; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033import org.yaml.snakeyaml.Yaml; 034import org.yaml.snakeyaml.error.YAMLException; 035 036/** @author jmchilton */ 037public class GalaxyWorkflowPlugin extends Plugin { 038 public static final Logger LOG = LoggerFactory.getLogger(GalaxyWorkflowPlugin.class); 039 public static final String[] TEST_SUFFIXES = {"-tests", "_tests", "-test", "-tests"}; 040 public static final String[] TEST_EXTENSIONS = {".yml", ".yaml", ".json"}; 041 042 /** 043 * Constructor to be used by plugin manager for plugin instantiation. Your plugins have to provide 044 * constructor with this exact signature to be successfully loaded by manager. 045 * 046 * @param wrapper 047 */ 048 public GalaxyWorkflowPlugin(PluginWrapper wrapper) { 049 super(wrapper); 050 } 051 052 @Extension 053 public static class GalaxyWorkflowPluginImpl implements CompleteLanguageInterface { 054 private ObjectMapper mapper = new ObjectMapper(); 055 056 /** 057 * This is basically stolen from org.galaxyproject.gxformat2.Lint. However, that is generated 058 * code and cannot be modified so putting it here along with this warning that the two should 059 * probably be in sync. 060 * 061 * @param workflow 062 * @return 063 */ 064 public static boolean isGXFormat2Workflow(final Map<String, Object> workflow) { 065 final String wfClass = (String) workflow.get("class"); 066 return "GalaxyWorkflow".equals(wfClass); 067 } 068 069 @Override 070 public String launchInstructions(String trsID) { 071 return null; 072 } 073 074 @Override 075 public Map<String, Object> loadCytoscapeElements( 076 String initialPath, String contents, Map<String, FileMetadata> indexedFiles) { 077 final Map<String, Object> workflow = loadWorkflow(contents); 078 try { 079 return Cytoscape.getElements(workflow); 080 } catch (ClassCastException e) { 081 LOG.error( 082 "ClassCastException, looks like an invalid workflow that passed the linter: " 083 + e.getMessage()); 084 return Map.of(); 085 } 086 } 087 088 @Override 089 public List<RowData> generateToolsTable( 090 String initialPath, String contents, Map<String, FileMetadata> indexedFiles) { 091 final Map<String, Object> workflow = loadWorkflow(contents); 092 final Map<String, Object> elements = Cytoscape.getElements(workflow); 093 final List<Map> nodes = (List<Map>) elements.getOrDefault("nodes", Lists.newArrayList()); 094 removeStartAndEndNodes(nodes); 095 return nodes.stream() 096 .map( 097 node -> { 098 final RowData rowData = new RowData(); 099 final Map<String, Object> nodeData = (Map<String, Object>) node.get("data"); 100 rowData.label = (String) nodeData.getOrDefault("label", ""); 101 rowData.dockerContainer = (String) nodeData.getOrDefault("docker", ""); 102 rowData.filename = (String) nodeData.getOrDefault("run", ""); 103 // TODO: get a sane link here when Docker is hooked up 104 try { 105 rowData.link = new URL((String) node.getOrDefault("repo_link", "")); 106 } catch (MalformedURLException e) { 107 rowData.link = null; 108 } 109 rowData.rowType = RowType.TOOL; 110 rowData.toolid = (String) nodeData.getOrDefault("id", ""); 111 return rowData; 112 }) 113 .collect(Collectors.toList()); 114 } 115 116 // Remove start and end nodes that were added for DAG 117 private void removeStartAndEndNodes(List<Map> nodes) { 118 nodes.removeIf( 119 node -> { 120 Map data = (Map) node.get("data"); 121 String id = (String) data.get("id"); 122 return START_ID.equals(id) || END_ID.equals(id); 123 }); 124 } 125 126 @Override 127 public VersionTypeValidation validateWorkflowSet( 128 String initialPath, String contents, Map<String, FileMetadata> indexedFiles) { 129 final LintContext lintContext = Lint.lint(loadWorkflow(contents)); 130 final boolean valid; 131 valid = !lintContext.getFoundErrors(); 132 final Map<String, String> messagesAsMap = new HashMap<>(); 133 final List<String> validationMessages = lintContext.collectMessages(); 134 final StringBuilder builder = new StringBuilder(); 135 if (validationMessages.size() == 1) { 136 builder.append(validationMessages.get(0)); 137 } else if (validationMessages.size() > 1) { 138 for (final String validationMessage : validationMessages) { 139 builder.append("- ").append(validationMessage).append("\n"); 140 } 141 } 142 final String validationMessageMerged = builder.toString(); 143 if (validationMessageMerged.length() > 0) { 144 messagesAsMap.put(initialPath, validationMessageMerged); 145 } 146 return new VersionTypeValidation(valid, messagesAsMap); 147 } 148 149 @Override 150 public VersionTypeValidation validateTestParameterSet(Map<String, FileMetadata> indexedFiles) { 151 return new VersionTypeValidation(true, new HashMap<>()); 152 } 153 154 @Override 155 public DescriptorLanguage getDescriptorLanguage() { 156 return DescriptorLanguage.GXFORMAT2; 157 } 158 159 @Override 160 public Pattern initialPathPattern() { 161 // Why doesn't this seem to be called anywhere? 162 return Pattern.compile("/(.*\\.gxwf\\.y[a]?ml|.*\\.ga)"); 163 } 164 165 @Override 166 public Map<String, FileMetadata> indexWorkflowFiles( 167 final String initialPath, final String contents, final FileReader reader) { 168 Map<String, FileMetadata> results = new HashMap<>(); 169 170 // identify filetype of initial descriptor, copied from Lint.java 171 String languageVersion = null; 172 try { 173 final Map<String, Object> workflowMap = loadWorkflow(contents); 174 if (isGXFormat2Workflow(workflowMap)) { 175 languageVersion = "gxformat2"; 176 } else { 177 languageVersion = "gxformat1"; 178 } 179 } catch (Exception e) { 180 // do nothing 181 } 182 183 results.put( 184 initialPath, 185 new FileMetadata(contents, GenericFileType.IMPORTED_DESCRIPTOR, languageVersion)); 186 187 final Optional<String> testParameterFile = findTestParameterFile(initialPath, reader); 188 testParameterFile.ifPresent( 189 // TODO - get language version into here 190 s -> results.put(s, new FileMetadata(s, GenericFileType.TEST_PARAMETER_FILE, null))); 191 return results; 192 } 193 194 protected Optional<String> findTestParameterFile( 195 final String initialPath, final FileReader reader) { 196 final int extensionPos = initialPath.lastIndexOf("."); 197 final String base = initialPath.substring(0, extensionPos); 198 199 final Path parent = Paths.get(initialPath).getParent(); 200 // listing files is more rate limit friendly (e.g. GitHub counts each 404 "miss" as an API 201 // call, 202 // but listing a directory can be free if previously requested/cached) 203 final Set<String> filenameset = 204 parent == null ? Sets.newHashSet() : Sets.newHashSet(reader.listFiles(parent.toString())); 205 206 for (final String suffix : TEST_SUFFIXES) { 207 for (final String extension : TEST_EXTENSIONS) { 208 final String testFile = base + suffix + extension; 209 if (filenameset.contains(testFile)) { 210 return Optional.of(testFile); 211 } 212 } 213 } 214 return Optional.empty(); 215 } 216 217 protected Boolean pathExistsFromReader(final FileReader reader, final String path) { 218 try { 219 reader.readFile(path); 220 return true; 221 } catch (Exception e) { 222 return false; 223 } 224 } 225 226 static Map<String, Object> loadWorkflow(final String content) { 227 final Yaml yaml = new Yaml(); 228 final Map map = yaml.loadAs(content, Map.class); 229 return (Map<String, Object>) map; 230 } 231 232 @Override 233 public RecommendedLanguageInterface.WorkflowMetadata parseWorkflowForMetadata( 234 String initialPath, String content, Map<String, FileMetadata> indexedFiles) { 235 RecommendedLanguageInterface.WorkflowMetadata metadata = 236 new RecommendedLanguageInterface.WorkflowMetadata(); 237 if (content != null && !content.isEmpty()) { 238 try { 239 final Map<String, Object> map = loadWorkflow(content); 240 String name = null; 241 try { 242 name = (String) map.get("name"); 243 } catch (ClassCastException e) { 244 LOG.debug("\"name:\" is malformed"); 245 } 246 247 // FOLLOWING CODE PULLED FROM cwl handler... 248 String label = null; 249 try { 250 label = (String) map.get("label"); 251 } catch (ClassCastException e) { 252 LOG.debug("\"label:\" is malformed"); 253 } 254 255 String annotation = null; 256 try { 257 annotation = (String) map.get("annotation"); 258 } catch (ClassCastException e) { 259 LOG.debug("\"annotation:\" is malformed"); 260 } 261 262 // "doc:" added for CWL 1.0 263 String doc = null; 264 if (map.containsKey("doc")) { 265 Object objectDoc = map.get("doc"); 266 if (objectDoc instanceof String) { 267 doc = (String) objectDoc; 268 } else if (objectDoc instanceof Map) { 269 Map docMap = (Map) objectDoc; 270 if (docMap.containsKey("$include")) { 271 String enclosingFile = (String) docMap.get("$include"); 272 Optional<FileMetadata> first = 273 indexedFiles.values().stream() 274 .filter(fileMetadata -> fileMetadata.content().equals(enclosingFile)) 275 .findFirst(); 276 if (first.isPresent()) { 277 // No way to fetch this here... 278 LOG.info( 279 "$include would have this but reader not passed through, not implemented"); 280 doc = null; 281 } 282 } 283 } else if (objectDoc instanceof List) { 284 // arrays for "doc:" added in CWL 1.1 285 List docList = (List) objectDoc; 286 doc = String.join(System.getProperty("line.separator"), docList); 287 } 288 } 289 290 final String finalChoiceForDescription = 291 ObjectUtils.firstNonNull(doc, annotation, name, label); 292 if (finalChoiceForDescription != null) { 293 metadata.setDescription(finalChoiceForDescription); 294 } else { 295 LOG.info("Description not found!"); 296 } 297 298 } catch (YAMLException | NullPointerException | ClassCastException ex) { 299 String message; 300 if (ex.getCause() != null) { 301 // seems to be possible to get underlying cause in some cases 302 message = ex.getCause().toString(); 303 } else { 304 // in other cases, the above will NullPointer 305 message = ex.toString(); 306 } 307 LOG.info("Galaxy Workflow file is malformed " + message); 308 // CWL parser gets to put validation information in here, 309 // plugin interface doesn't consume the right information though. 310 // https://github.com/dockstore/dockstore/blob/develop/dockstore-webservice/src/main/java/io/dockstore/webservice/languages/CWLHandler.java#L139 311 } 312 } 313 return metadata; 314 } 315 } 316}