• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

common-workflow-language / cwlviewer / #1694

30 Sep 2024 10:51AM UTC coverage: 70.306% (-0.5%) from 70.811%
#1694

push

github

mr-c
detect non-Workflow CWL documents and give a better error message

2 of 19 new or added lines in 4 files covered. (10.53%)

106 existing lines in 4 files now uncovered.

1700 of 2418 relevant lines covered (70.31%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.08
/src/main/java/org/commonwl/view/cwl/CWLService.java
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one
3
 * or more contributor license agreements.  See the NOTICE file
4
 * distributed with this work for additional information
5
 * regarding copyright ownership.  The ASF licenses this file
6
 * to you under the Apache License, Version 2.0 (the
7
 * "License"); you may not use this file except in compliance
8
 * with the License.  You may obtain a copy of the License at
9
 *
10
 *   http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing,
13
 * software distributed under the License is distributed on an
14
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
 * KIND, either express or implied.  See the License for the
16
 * specific language governing permissions and limitations
17
 * under the License.
18
 */
19

20
package org.commonwl.view.cwl;
21

22
import static org.apache.commons.io.FileUtils.readFileToString;
23

24
import java.io.ByteArrayInputStream;
25
import java.io.File;
26
import java.io.IOException;
27
import java.io.InputStream;
28
import java.io.StringWriter;
29
import java.net.URI;
30
import java.nio.charset.StandardCharsets;
31
import java.nio.file.Files;
32
import java.nio.file.Path;
33
import java.util.ArrayList;
34
import java.util.HashMap;
35
import java.util.List;
36
import java.util.Map;
37
import java.util.Map.Entry;
38
import java.util.Set;
39
import org.apache.commons.io.FileUtils;
40
import org.apache.commons.io.FilenameUtils;
41
import org.apache.commons.lang3.StringUtils;
42
import org.apache.jena.iri.IRI;
43
import org.apache.jena.iri.IRIFactory;
44
import org.apache.jena.ontology.OntModelSpec;
45
import org.apache.jena.query.QuerySolution;
46
import org.apache.jena.query.ResultSet;
47
import org.apache.jena.rdf.model.Model;
48
import org.apache.jena.rdf.model.ModelFactory;
49
import org.apache.jena.riot.RiotException;
50
import org.commonwl.view.docker.DockerService;
51
import org.commonwl.view.git.GitDetails;
52
import org.commonwl.view.git.GitLicenseException;
53
import org.commonwl.view.graphviz.ModelDotWriter;
54
import org.commonwl.view.graphviz.RDFDotWriter;
55
import org.commonwl.view.workflow.Workflow;
56
import org.commonwl.view.workflow.WorkflowNotFoundException;
57
import org.commonwl.view.workflow.WorkflowOverview;
58
import org.slf4j.Logger;
59
import org.slf4j.LoggerFactory;
60
import org.snakeyaml.engine.v2.api.Load;
61
import org.snakeyaml.engine.v2.api.LoadSettings;
62
import org.springframework.beans.factory.annotation.Autowired;
63
import org.springframework.beans.factory.annotation.Value;
64
import org.springframework.stereotype.Service;
65

66
/** Provides CWL parsing for workflows to gather an overview for display and visualisation */
67
@Service
68
public class CWLService {
69

70
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
1✔
71
  private final IRIFactory iriFactory = IRIFactory.iriImplementation();
1✔
72

73
  // Autowired properties/services
74
  private final RDFService rdfService;
75
  private final CWLTool cwlTool;
76
  private final Map<String, String> licenseVocab;
77
  private final int singleFileSizeLimit;
78

79
  // CWL specific strings
80
  private final String DOC_GRAPH = "$graph";
1✔
81
  private final String CLASS = "class";
1✔
82
  private final String WORKFLOW = "Workflow";
1✔
83
  private final String COMMANDLINETOOL = "CommandLineTool";
1✔
84
  private final String EXPRESSIONTOOL = "ExpressionTool";
1✔
85
  private final String STEPS = "steps";
1✔
86
  private final String INPUTS = "inputs";
1✔
87
  private final String IN = "in";
1✔
88
  private final String OUTPUTS = "outputs";
1✔
89
  private final String OUT = "out";
1✔
90
  private final String ID = "id";
1✔
91
  private final String TYPE = "type";
1✔
92
  private final String LABEL = "label";
1✔
93
  private final String DEFAULT = "default";
1✔
94
  private final String OUTPUT_SOURCE = "outputSource";
1✔
95
  private final String SOURCE = "source";
1✔
96
  private final String DOC = "doc";
1✔
97
  private final String DESCRIPTION = "description";
1✔
98
  private final String ARRAY = "array";
1✔
99
  private final String ARRAY_ITEMS = "items";
1✔
100
  private final String LOCATION = "location";
1✔
101
  private final String RUN = "run";
1✔
102

103
  /**
104
   * Constructor for the Common Workflow Language service
105
   *
106
   * @param rdfService A service for handling RDF queries
107
   * @param cwlTool Handles cwltool integration
108
   * @param singleFileSizeLimit The file size limit for single files
109
   */
110
  @Autowired
111
  public CWLService(
112
      RDFService rdfService,
113
      CWLTool cwlTool,
114
      Map<String, String> licenseVocab,
115
      @Value("${singleFileSizeLimit}") int singleFileSizeLimit) {
1✔
116
    this.rdfService = rdfService;
1✔
117
    this.cwlTool = cwlTool;
1✔
118
    this.licenseVocab = licenseVocab;
1✔
119
    this.singleFileSizeLimit = singleFileSizeLimit;
1✔
120
  }
1✔
121

122
  /**
123
   * Gets whether a file is packed using schema salad
124
   *
125
   * @param workflowFile The file to be parsed
126
   * @return Whether the file is packed
127
   */
128
  public boolean isPacked(File workflowFile) throws IOException {
129
    if (workflowFile.length() > singleFileSizeLimit) {
1✔
UNCOV
130
      return false;
×
131
    }
132
    String fileContent = readFileToString(workflowFile, StandardCharsets.UTF_8);
1✔
133
    return fileContent.contains("$graph");
1✔
134
  }
135

136
  /**
137
   * Gets a list of workflows from a packed CWL file
138
   *
139
   * @param packedFile The packed CWL file
140
   * @return The list of workflow overviews
141
   */
142
  public List<WorkflowOverview> getWorkflowOverviewsFromPacked(File packedFile) throws IOException {
143
    if (packedFile.length() <= singleFileSizeLimit) {
1✔
144
      List<WorkflowOverview> overviews = new ArrayList<>();
1✔
145

146
      Map<String, Object> packedJson = yamlPathToJson(packedFile.toPath());
1✔
147

148
      if (packedJson.containsKey(DOC_GRAPH)) {
1✔
149
        for (Map<String, Object> node : (Iterable<Map<String, Object>>) packedJson.get(DOC_GRAPH)) {
1✔
150
          if (extractProcess(node) == CWLProcess.WORKFLOW) {
1✔
151
            WorkflowOverview overview =
1✔
152
                new WorkflowOverview((String) node.get(ID), extractLabel(node), extractDoc(node));
1✔
153
            overviews.add(overview);
1✔
154
          }
155
        }
1✔
156
      } else {
UNCOV
157
        throw new IOException("The file given was not recognised as a packed CWL file");
×
158
      }
159

160
      return overviews;
1✔
161

162
    } else {
UNCOV
163
      throw new IOException(
×
164
          "File '"
UNCOV
165
              + packedFile.getName()
×
166
              + "' is over singleFileSizeLimit - "
UNCOV
167
              + FileUtils.byteCountToDisplaySize(packedFile.length())
×
168
              + "/"
UNCOV
169
              + FileUtils.byteCountToDisplaySize(singleFileSizeLimit));
×
170
    }
171
  }
172

173
  /**
174
   * Gets the Workflow object from internal parsing. Note, the length of the stream is not checked.
175
   *
176
   * @param workflowStream The workflow stream to be parsed
177
   * @param packedWorkflowId The ID of the workflow object if the file is packed. <code>null</code>
178
   *     means the workflow is not expected to be packed, while "" means the first workflow found is
179
   *     used, packed or non-packed.
180
   * @param defaultLabel Label to give workflow if not set
181
   * @return The constructed workflow object
182
   */
183
  public Workflow parseWorkflowNative(
184
      InputStream workflowStream, String packedWorkflowId, String defaultLabel)
185
      throws IOException, WorkflowNotFoundException, CWLValidationException {
186
    // Parse file as yaml
187
    Map<String, Object> cwlFile = yamlStreamToJson(workflowStream);
1✔
188

189
    // Check packed workflow occurs
190
    boolean found = false;
1✔
191
    if (packedWorkflowId != null) {
1✔
192
      if (cwlFile.containsKey(DOC_GRAPH)) {
1✔
193
        for (Map<String, Object> node : (Iterable<Map<String, Object>>) cwlFile.get(DOC_GRAPH)) {
1✔
194
          if (extractProcess(node) == CWLProcess.WORKFLOW) {
1✔
195
            String currentId = (String) node.get(ID);
1✔
196
            if (currentId.startsWith("#")) {
1✔
UNCOV
197
              currentId = currentId.substring(1);
×
198
            }
199
            if (packedWorkflowId.isEmpty() || currentId.equals(packedWorkflowId)) {
1✔
200
              cwlFile = node;
1✔
201
              found = true;
1✔
202
              break;
1✔
203
            }
204
          }
205
        }
1✔
206
      }
207
      if (!found && !packedWorkflowId.isEmpty()) throw new WorkflowNotFoundException();
1✔
208
    }
209
    if (!found && (extractProcess(cwlFile) == CWLProcess.WORKFLOW)) {
1✔
210
      found = true;
1✔
211
    } else if (found && (extractProcess(cwlFile) != CWLProcess.WORKFLOW)) {
1✔
NEW
212
      throw new CWLNotAWorkflowException(
×
NEW
213
          "Not a 'class: Workflow' CWL document, is a " + extractProcess(cwlFile));
×
214
    }
215
    if (!found) {
1✔
UNCOV
216
      throw new WorkflowNotFoundException();
×
217
    }
218

219
    // Use filename for label if there is no defined one
220
    String label = extractLabel(cwlFile);
1✔
221
    if (label == null) {
1✔
222
      label = defaultLabel;
1✔
223
    }
224

225
    // Construct the rest of the workflow model
226
    Workflow workflowModel =
1✔
227
        new Workflow(
228
            label, extractDoc(cwlFile), getInputs(cwlFile), getOutputs(cwlFile), getSteps(cwlFile));
1✔
229

230
    workflowModel.setCwltoolVersion(cwlTool.getVersion());
1✔
231

232
    // Generate DOT graph
233
    StringWriter graphWriter = new StringWriter();
1✔
234
    ModelDotWriter dotWriter = new ModelDotWriter(graphWriter);
1✔
235
    try {
236
      dotWriter.writeGraph(workflowModel);
1✔
237
      workflowModel.setVisualisationDot(graphWriter.toString());
1✔
238
    } catch (IOException ex) {
×
UNCOV
239
      logger.error("Failed to create DOT graph for workflow: " + ex.getMessage());
×
240
    }
1✔
241

242
    return workflowModel;
1✔
243
  }
244

245
  /**
246
   * Gets the Workflow object from internal parsing. The size of the workflow file must be below the
247
   * configured singleFileSizeLimit in the constructor/spring config.
248
   *
249
   * @param workflowFile The workflow file to be parsed
250
   * @param packedWorkflowId The ID of the workflow object if the file is packed
251
   * @return The constructed workflow object
252
   */
253
  public Workflow parseWorkflowNative(Path workflowFile, String packedWorkflowId)
254
      throws IOException, WorkflowNotFoundException, CWLValidationException {
255

256
    // Check file size limit before parsing
257
    long fileSizeBytes = Files.size(workflowFile);
1✔
258
    if (fileSizeBytes <= singleFileSizeLimit) {
1✔
259
      try (InputStream in = Files.newInputStream(workflowFile)) {
1✔
260
        return parseWorkflowNative(
1✔
261
            in, packedWorkflowId, workflowFile.getName(workflowFile.getNameCount() - 1).toString());
1✔
262
      }
263
    } else {
264
      throw new IOException(
1✔
265
          "File '"
266
              + workflowFile.getFileName()
1✔
267
              + "' is over singleFileSizeLimit - "
268
              + FileUtils.byteCountToDisplaySize(fileSizeBytes)
1✔
269
              + "/"
270
              + FileUtils.byteCountToDisplaySize(singleFileSizeLimit));
1✔
271
    }
272
  }
273

274
  /**
275
   * Create a workflow model using cwltool rdf output
276
   *
277
   * @param basicModel The basic workflow object created thus far
278
   * @param workflowFile The workflow file to run cwltool on
279
   * @return The constructed workflow object
280
   */
281
  public Workflow parseWorkflowWithCwltool(Workflow basicModel, Path workflowFile, Path workTree)
282
      throws CWLValidationException, GitLicenseException {
283
    GitDetails gitDetails = basicModel.getRetrievedFrom();
1✔
284
    String latestCommit = basicModel.getLastCommit();
1✔
285
    String packedWorkflowID = gitDetails.getPackedId();
1✔
286

287
    // Get paths to workflow
288
    String url = basicModel.getIdentifier();
1✔
289
    String workflowFileURI = workflowFile.toAbsolutePath().toUri().toString();
1✔
290
    URI workTreeUri = workTree.toAbsolutePath().toUri();
1✔
291
    String localPath = workflowFileURI;
1✔
292
    String gitPath = gitDetails.getPath();
1✔
293
    if (packedWorkflowID != null) {
1✔
294
      if (packedWorkflowID.charAt(0) != '#') {
1✔
295
        localPath += "#";
1✔
296
        gitPath += "#";
1✔
297
      }
298
      localPath += packedWorkflowID;
1✔
299
      gitPath += packedWorkflowID;
1✔
300
    }
301

302
    // Get RDF representation from cwltool
303
    if (!rdfService.graphExists(url)) {
1✔
UNCOV
304
      String rdf = cwlTool.getRDF(localPath);
×
305
      // Replace /tmp/123123 with permalink base
306
      // NOTE: We do not just replace workflowFileURI, all referenced files will also
307
      // get rewritten
308
      rdf =
×
309
          rdf.replace(
×
UNCOV
310
              workTreeUri.toString(), "https://w3id.org/cwl/view/git/" + latestCommit + "/");
×
311
      // Workaround for common-workflow-language/cwltool#427
UNCOV
312
      rdf = rdf.replace("<rdfs:>", "<http://www.w3.org/2000/01/rdf-schema#>");
×
313

314
      // Create a workflow model from RDF representation
315
      Model model = ModelFactory.createDefaultModel();
×
UNCOV
316
      model.read(new ByteArrayInputStream(rdf.getBytes()), null, "TURTLE");
×
317

318
      // Store the model
UNCOV
319
      rdfService.storeModel(url, model);
×
320
    }
321

322
    // Base workflow details
323
    String label = FilenameUtils.getName(url);
1✔
324
    String doc = null;
1✔
325
    ResultSet labelAndDoc = rdfService.getLabelAndDoc(url);
1✔
326
    if (labelAndDoc.hasNext()) {
1✔
327
      QuerySolution labelAndDocSoln = labelAndDoc.nextSolution();
1✔
328
      if (labelAndDocSoln.contains("label")) {
1✔
UNCOV
329
        label = labelAndDocSoln.get("label").toString();
×
330
      }
331
      if (labelAndDocSoln.contains("doc")) {
1✔
UNCOV
332
        doc = labelAndDocSoln.get("doc").toString();
×
333
      }
334
    }
335

336
    // Inputs
337
    Map<String, CWLElement> wfInputs = new HashMap<>();
1✔
338
    ResultSet inputs = rdfService.getInputs(url);
1✔
339
    while (inputs.hasNext()) {
1✔
340
      QuerySolution input = inputs.nextSolution();
1✔
341
      String inputName = rdfService.stepNameFromURI(gitPath, input.get("name").toString());
1✔
342

343
      CWLElement wfInput = new CWLElement();
1✔
344
      if (input.contains("type")) {
1✔
345
        String type;
346
        if (input.get("type").toString().equals("https://w3id.org/cwl/salad#array")) {
1✔
347
          type = typeURIToString(input.get("items").toString()) + "[]";
1✔
348
        } else {
UNCOV
349
          type = typeURIToString(input.get("type").toString());
×
350
        }
351
        if (input.contains("null")) {
1✔
UNCOV
352
          type += " (Optional)";
×
353
        }
354
        wfInput.setType(type);
1✔
355
      }
356
      if (input.contains("format")) {
1✔
357
        String format = input.get("format").toString();
×
UNCOV
358
        setFormat(wfInput, format);
×
359
      }
360
      if (input.contains("label")) {
1✔
UNCOV
361
        wfInput.setLabel(input.get("label").toString());
×
362
      }
363
      if (input.contains("doc")) {
1✔
UNCOV
364
        wfInput.setDoc(input.get("doc").toString());
×
365
      }
366
      wfInputs.put(rdfService.labelFromName(inputName), wfInput);
1✔
367
    }
1✔
368

369
    // Outputs
370
    Map<String, CWLElement> wfOutputs = new HashMap<>();
1✔
371
    ResultSet outputs = rdfService.getOutputs(url);
1✔
372
    while (outputs.hasNext()) {
1✔
373
      QuerySolution output = outputs.nextSolution();
1✔
374
      CWLElement wfOutput = new CWLElement();
1✔
375

376
      String outputName = rdfService.stepNameFromURI(gitPath, output.get("name").toString());
1✔
377
      if (output.contains("type")) {
1✔
378
        String type;
379
        if (output.get("type").toString().equals("https://w3id.org/cwl/salad#array")) {
1✔
UNCOV
380
          type = typeURIToString(output.get("items").toString()) + "[]";
×
381
        } else {
382
          type = typeURIToString(output.get("type").toString());
1✔
383
        }
384
        if (output.contains("null")) {
1✔
UNCOV
385
          type += " (Optional)";
×
386
        }
387
        wfOutput.setType(type);
1✔
388
      }
389

390
      if (output.contains("src")) {
1✔
UNCOV
391
        wfOutput.addSourceID(rdfService.stepNameFromURI(gitPath, output.get("src").toString()));
×
392
      }
393
      if (output.contains("format")) {
1✔
394
        String format = output.get("format").toString();
×
UNCOV
395
        setFormat(wfOutput, format);
×
396
      }
397
      if (output.contains("label")) {
1✔
UNCOV
398
        wfOutput.setLabel(output.get("label").toString());
×
399
      }
400
      if (output.contains("doc")) {
1✔
UNCOV
401
        wfOutput.setDoc(output.get("doc").toString());
×
402
      }
403
      wfOutputs.put(rdfService.labelFromName(outputName), wfOutput);
1✔
404
    }
1✔
405

406
    // Steps
407
    Map<String, CWLStep> wfSteps = new HashMap<>();
1✔
408
    ResultSet steps = rdfService.getSteps(url);
1✔
409
    while (steps.hasNext()) {
1✔
410
      QuerySolution step = steps.nextSolution();
1✔
411
      String uri = rdfService.stepNameFromURI(gitPath, step.get("step").toString());
1✔
412
      if (wfSteps.containsKey(uri)) {
1✔
413
        // Already got step details, add extra source ID
414
        if (step.contains("src")) {
1✔
415
          CWLElement src = new CWLElement();
1✔
416
          src.addSourceID(rdfService.stepNameFromURI(gitPath, step.get("src").toString()));
1✔
417
          wfSteps.get(uri).getSources().put(step.get("stepinput").toString(), src);
1✔
418
        } else if (step.contains("default")) {
1✔
419
          CWLElement src = new CWLElement();
1✔
420
          src.setDefaultVal(rdfService.formatDefault(step.get("default").toString()));
1✔
421
          wfSteps.get(uri).getSources().put(step.get("stepinput").toString(), src);
1✔
422
        }
1✔
423
      } else {
424
        // Add new step
425
        CWLStep wfStep = new CWLStep();
1✔
426

427
        IRI workflowPath = iriFactory.construct(url).resolve("./");
1✔
428
        Object runValue = step.get("run").asResource().toString();
1✔
429
        if (String.class.isAssignableFrom(runValue.getClass())) {
1✔
430
          String runPath = (String) runValue;
1✔
431
          wfStep.setRun(workflowPath.relativize(runPath).toString());
1✔
432
          wfStep.setRunType(rdfService.strToRuntype(step.get("runtype").toString()));
1✔
433
        }
434

435
        if (step.contains("src")) {
1✔
436
          CWLElement src = new CWLElement();
1✔
437
          src.addSourceID(rdfService.stepNameFromURI(gitPath, step.get("src").toString()));
1✔
438
          Map<String, CWLElement> srcList = new HashMap<>();
1✔
439
          srcList.put(rdfService.stepNameFromURI(gitPath, step.get("stepinput").toString()), src);
1✔
440
          wfStep.setSources(srcList);
1✔
441
        } else if (step.contains("default")) {
1✔
442
          CWLElement src = new CWLElement();
1✔
443
          src.setDefaultVal(rdfService.formatDefault(step.get("default").toString()));
1✔
444
          Map<String, CWLElement> srcList = new HashMap<>();
1✔
445
          srcList.put(rdfService.stepNameFromURI(gitPath, step.get("stepinput").toString()), src);
1✔
446
          wfStep.setSources(srcList);
1✔
447
        }
448
        if (step.contains("label")) {
1✔
UNCOV
449
          wfStep.setLabel(step.get("label").toString());
×
450
        }
451
        if (step.contains("doc")) {
1✔
UNCOV
452
          wfStep.setDoc(step.get("doc").toString());
×
453
        }
454
        wfSteps.put(rdfService.labelFromName(uri), wfStep);
1✔
455
      }
456
    }
1✔
457
    // Try to determine license
458
    ResultSet licenseResult = rdfService.getLicense(url);
1✔
459
    String licenseLink;
460
    if (licenseResult.hasNext()) {
1✔
461
      licenseLink = normaliseLicenseLink(licenseResult.next().get("license").toString());
1✔
462
    } else {
463
      // Check for "LICENSE"-like files in root of git repo
UNCOV
464
      licenseLink = basicModel.getRetrievedFrom().getLicense(workTree);
×
465
    }
466

467
    // Docker link
468
    ResultSet dockerResult = rdfService.getDockerLink(url);
1✔
469
    String dockerLink = null;
1✔
470
    if (dockerResult.hasNext()) {
1✔
471
      QuerySolution docker = dockerResult.nextSolution();
×
472
      if (docker.contains("pull")) {
×
UNCOV
473
        dockerLink = DockerService.getDockerHubURL(docker.get("pull").toString());
×
474
      } else {
UNCOV
475
        dockerLink = "true";
×
476
      }
477
    }
478

479
    // Create workflow model
480
    Workflow workflowModel =
1✔
481
        new Workflow(label, doc, wfInputs, wfOutputs, wfSteps, dockerLink, licenseLink);
482

483
    // Generate DOT graph
484
    StringWriter graphWriter = new StringWriter();
1✔
485
    RDFDotWriter RDFDotWriter = new RDFDotWriter(graphWriter, rdfService, gitPath);
1✔
486
    try {
487
      RDFDotWriter.writeGraph(url);
1✔
488
      workflowModel.setVisualisationDot(graphWriter.toString());
1✔
489
    } catch (IOException ex) {
×
UNCOV
490
      logger.error("Failed to create DOT graph for workflow: " + ex.getMessage());
×
491
    }
1✔
492

493
    return workflowModel;
1✔
494
  }
495

496
  /**
497
   * Get an overview of a workflow
498
   *
499
   * @param file A file, potentially a workflow
500
   * @return A constructed WorkflowOverview of the workflow
501
   * @throws IOException Any API errors which may have occurred
502
   */
503
  public WorkflowOverview getWorkflowOverview(File file) throws IOException {
504

505
    // Get the content of this file from Github
506
    long fileSizeBytes = file.length();
1✔
507

508
    // Check file size limit before parsing
509
    if (fileSizeBytes <= singleFileSizeLimit) {
1✔
510

511
      // Parse file as yaml
512
      Map<String, Object> cwlFile = yamlPathToJson(file.toPath());
1✔
513

514
      // If the CWL file is packed there can be multiple workflows in a file
515
      int packedCount = 0;
1✔
516
      if (cwlFile.containsKey(DOC_GRAPH)) {
1✔
517
        // Packed CWL, find the first subelement which is a workflow and take it
518
        for (Map<String, Object> node : (Iterable<Map<String, Object>>) cwlFile.get(DOC_GRAPH)) {
1✔
519
          if (extractProcess(node) == CWLProcess.WORKFLOW) {
1✔
520
            cwlFile = node;
1✔
521
            packedCount++;
1✔
522
          }
523
        }
1✔
524
        if (packedCount > 1) {
1✔
525
          return new WorkflowOverview(
×
UNCOV
526
              "/" + file.getName(), "Packed file", "contains " + packedCount + " workflows");
×
527
        }
528
      }
529

530
      // Can only make an overview if this is a workflow
531
      if (extractProcess(cwlFile) == CWLProcess.WORKFLOW) {
1✔
532
        // Use filename for label if there is no defined one
533
        String label = extractLabel(cwlFile);
1✔
534
        if (label == null) {
1✔
UNCOV
535
          label = file.getName();
×
536
        }
537

538
        // Return the constructed overview
539
        return new WorkflowOverview("/" + file.getName(), label, extractDoc(cwlFile));
1✔
540
      } else {
541
        // Return null if not a workflow file
UNCOV
542
        return null;
×
543
      }
544
    } else {
545
      throw new IOException(
1✔
546
          "File '"
547
              + file.getName()
1✔
548
              + "' is over singleFileSizeLimit - "
549
              + FileUtils.byteCountToDisplaySize(fileSizeBytes)
1✔
550
              + "/"
551
              + FileUtils.byteCountToDisplaySize(singleFileSizeLimit));
1✔
552
    }
553
  }
554

555
  /**
556
   * Set the format for an input or output, handling ontologies
557
   *
558
   * @param inputOutput The input or output CWL Element
559
   * @param format The format URI
560
   */
561
  private void setFormat(CWLElement inputOutput, String format) {
UNCOV
562
    inputOutput.setFormat(format);
×
563
    try {
564
      if (!rdfService.ontPropertyExists(format)) {
×
565
        Model ontModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM);
×
566
        ontModel.read(format, null, "RDF/XML");
×
UNCOV
567
        rdfService.addToOntologies(ontModel);
×
568
      }
569
      String formatLabel = rdfService.getOntLabel(format);
×
570
      inputOutput.setType(inputOutput.getType() + " [" + formatLabel + "]");
×
571
    } catch (RiotException ex) {
×
572
      inputOutput.setType(inputOutput.getType() + " [format]");
×
573
    }
×
UNCOV
574
  }
×
575

576
  /**
577
   * Convert RDF URI for a type to a name
578
   *
579
   * @param uri The URI for the type
580
   * @return The human readable name for that type
581
   */
582
  private String typeURIToString(String uri) {
583
    switch (uri) {
1✔
584
      case "http://www.w3.org/2001/XMLSchema#string":
585
        return "String";
1✔
586
      case "https://w3id.org/cwl/cwl#File":
587
        return "File";
1✔
588
      case "http://www.w3.org/2001/XMLSchema#boolean":
UNCOV
589
        return "Boolean";
×
590
      case "http://www.w3.org/2001/XMLSchema#int":
UNCOV
591
        return "Integer";
×
592
      case "http://www.w3.org/2001/XMLSchema#double":
UNCOV
593
        return "Double";
×
594
      case "http://www.w3.org/2001/XMLSchema#float":
UNCOV
595
        return "Float";
×
596
      case "http://www.w3.org/2001/XMLSchema#long":
UNCOV
597
        return "Long";
×
598
      case "https://w3id.org/cwl/cwl#Directory":
UNCOV
599
        return "Directory";
×
600
      default:
UNCOV
601
        return uri;
×
602
    }
603
  }
604

605
  /**
606
   * Converts a yaml String to JsonNode
607
   *
608
   * @param path A Path to a file containing the yaml content
609
   * @return A JsonNode with the content of the document
610
   * @throws IOException
611
   */
612
  private Map<String, Object> yamlPathToJson(Path path) throws IOException {
613
    LoadSettings settings = LoadSettings.builder().build();
1✔
614
    Load load = new Load(settings);
1✔
615
    try (InputStream in = Files.newInputStream(path)) {
1✔
616
      return (Map<String, Object>) load.loadFromInputStream(in);
1✔
617
    }
618
  }
619

620
  /**
621
   * Converts a yaml String to JsonNode
622
   *
623
   * @param yamlStream An InputStream containing the yaml content
624
   * @return A JsonNode with the content of the document
625
   */
626
  private Map<String, Object> yamlStreamToJson(InputStream yamlStream) {
627
    LoadSettings settings = LoadSettings.builder().build();
1✔
628
    Load load = new Load(settings);
1✔
629
    return (Map<String, Object>) load.loadFromInputStream(yamlStream);
1✔
630
  }
631

632
  /**
633
   * Extract the label from a node
634
   *
635
   * @param node The node to have the label extracted from
636
   * @return The string for the label of the node
637
   */
638
  private String extractLabel(Map<String, Object> node) {
639
    if (node != null && node.containsKey(LABEL)) {
1✔
640
      return (String) node.get(LABEL);
1✔
641
    }
642
    return null;
1✔
643
  }
644

645
  /**
646
   * Extract the class parameter from a node representing a document
647
   *
648
   * @param node The root node of a cwl document
649
   * @return Which process this document represents
650
   */
651
  private CWLProcess extractProcess(Map<String, Object> node) {
652
    if (node != null) {
1✔
653
      if (node.containsKey(CLASS)) {
1✔
654
        switch ((String) node.get(CLASS)) {
1✔
655
          case WORKFLOW:
656
            return CWLProcess.WORKFLOW;
1✔
657
          case COMMANDLINETOOL:
658
            return CWLProcess.COMMANDLINETOOL;
1✔
659
          case EXPRESSIONTOOL:
UNCOV
660
            return CWLProcess.EXPRESSIONTOOL;
×
661
        }
662
      }
663
    }
UNCOV
664
    return null;
×
665
  }
666

667
  /**
668
   * Get the steps for a particular document
669
   *
670
   * @param cwlDoc The document to get steps for
671
   * @return A map of step IDs and details related to them
672
   */
673
  private Map<String, CWLStep> getSteps(Map<String, Object> cwlDoc) {
674
    if (cwlDoc != null && cwlDoc.containsKey(STEPS)) {
1✔
675
      Map<String, CWLStep> returnMap = new HashMap<>();
1✔
676

677
      Object steps = cwlDoc.get(STEPS);
1✔
678
      if (List.class.isAssignableFrom(steps.getClass())) {
1✔
679
        // Explicit ID and other fields within each input list
680
        for (Map<String, Object> step : (List<Map<String, Object>>) steps) {
1✔
681
          CWLStep stepObject =
1✔
682
              new CWLStep(extractLabel(step), extractDoc(step), extractRun(step), getInputs(step));
1✔
683
          returnMap.put(extractID(step), stepObject);
1✔
684
        }
1✔
685
      } else if (Map.class.isAssignableFrom(steps.getClass())) {
1✔
686
        // ID is the key of each object
687
        for (Entry<String, Map<String, Object>> stepEntry :
688
            ((Map<String, Map<String, Object>>) steps).entrySet()) {
1✔
689
          Map<String, Object> step = stepEntry.getValue();
1✔
690
          CWLStep stepObject =
1✔
691
              new CWLStep(extractLabel(step), extractDoc(step), extractRun(step), getInputs(step));
1✔
692
          returnMap.put(stepEntry.getKey(), stepObject);
1✔
693
        }
1✔
694
      }
695

696
      return returnMap;
1✔
697
    }
UNCOV
698
    return null;
×
699
  }
700

701
  /**
702
   * Get a the inputs for a particular document
703
   *
704
   * @param cwlDoc The document to get inputs for
705
   * @return A map of input IDs and details related to them
706
   */
707
  private Map<String, CWLElement> getInputs(Map<String, Object> cwlDoc) {
708
    if (cwlDoc != null) {
1✔
709
      if (cwlDoc.containsKey(INPUTS)) {
1✔
710
        // For all version workflow inputs/outputs and draft steps
711
        return getInputsOutputs(cwlDoc.get(INPUTS));
1✔
712
      } else if (cwlDoc.containsKey(IN)) {
1✔
713
        // For V1.0 steps
714
        return getStepInputsOutputs(cwlDoc.get(IN));
1✔
715
      }
716
    }
UNCOV
717
    return null;
×
718
  }
719

720
  /**
721
   * Get the outputs for a particular document
722
   *
723
   * @param cwlDoc The document to get outputs for
724
   * @return A map of output IDs and details related to them
725
   */
726
  private Map<String, CWLElement> getOutputs(Map<String, Object> cwlDoc) {
727
    if (cwlDoc != null) {
1✔
728
      // For all version workflow inputs/outputs and draft steps
729
      if (cwlDoc.containsKey(OUTPUTS)) {
1✔
730
        return getInputsOutputs(cwlDoc.get(OUTPUTS));
1✔
731
      }
732
      // Outputs are not gathered for v1 steps
733
    }
UNCOV
734
    return null;
×
735
  }
736

737
  /**
738
   * Get inputs or outputs from an in or out node
739
   *
740
   * @param inOut The in or out node
741
   * @return A map of input IDs and details related to them
742
   */
743
  private Map<String, CWLElement> getStepInputsOutputs(Object inOut) {
744
    Map<String, CWLElement> returnMap = new HashMap<>();
1✔
745

746
    if (List.class.isAssignableFrom(inOut.getClass())) {
1✔
747
      // array<WorkflowStepInput>
748
      for (Map<String, Object> inOutNode : (List<Map<String, Object>>) inOut) {
1✔
749
        CWLElement inputOutput = new CWLElement();
1✔
750
        List<String> sources = extractSource(inOutNode);
1✔
751
        if (sources.size() > 0) {
1✔
752
          for (String source : sources) {
1✔
753
            inputOutput.addSourceID(stepIDFromSource(source));
1✔
754
          }
1✔
755
        } else {
756
          inputOutput.setDefaultVal(extractDefault(inOutNode));
1✔
757
        }
758
        returnMap.put(extractID(inOutNode), inputOutput);
1✔
759
      }
1✔
760
    } else if (Map.class.isAssignableFrom(inOut.getClass())) {
1✔
761
      // map<WorkflowStepInput.id, WorkflowStepInput.source>
762
      Set<Entry<String, Object>> iterator = ((Map<String, Object>) inOut).entrySet();
1✔
763
      for (Entry<String, Object> entry : iterator) {
1✔
764
        Object inOutNode = entry.getValue();
1✔
765
        CWLElement inputOutput = new CWLElement();
1✔
766
        if (Map.class.isAssignableFrom(inOutNode.getClass())) {
1✔
767
          Map<String, Object> properties = (Map<String, Object>) inOutNode;
1✔
768
          if (properties.containsKey(SOURCE)) {
1✔
769
            Object source = properties.get(SOURCE);
1✔
770
            if (List.class.isAssignableFrom(source.getClass())) {
1✔
771
              for (String sourceEntry : (List<String>) source) {
1✔
772
                inputOutput.addSourceID(stepIDFromSource(sourceEntry));
1✔
773
              }
1✔
774
            } else {
775
              inputOutput.addSourceID(stepIDFromSource((String) source));
1✔
776
            }
777
          } else {
1✔
778
            inputOutput.setDefaultVal(extractDefault(properties));
1✔
779
          }
780
        } else if (List.class.isAssignableFrom(inOutNode.getClass())) {
1✔
781
          for (String key : (List<String>) inOutNode) {
×
782
            inputOutput.addSourceID(stepIDFromSource(key));
×
UNCOV
783
          }
×
784
        } else {
785
          inputOutput.addSourceID(stepIDFromSource((String) inOutNode));
1✔
786
        }
787
        returnMap.put(entry.getKey(), inputOutput);
1✔
788
      }
1✔
789
    }
790

791
    return returnMap;
1✔
792
  }
793

794
  /**
795
   * Get inputs or outputs from an inputs or outputs node
796
   *
797
   * @param object The inputs or outputs node
798
   * @return A map of input IDs and details related to them
799
   */
800
  private Map<String, CWLElement> getInputsOutputs(Object object) {
801
    Map<String, CWLElement> returnMap = new HashMap<>();
1✔
802

803
    if (List.class.isAssignableFrom(object.getClass())) {
1✔
804
      // Explicit ID and other fields within each list
805
      for (Map<String, Object> inputOutput : (List<Map<String, Object>>) object) {
1✔
806
        String id = (String) inputOutput.get(ID);
1✔
807
        if (id.charAt(0) == '#') {
1✔
UNCOV
808
          id = id.substring(1);
×
809
        }
810
        returnMap.put(id, getDetails(inputOutput));
1✔
811
      }
1✔
812
    } else if (Map.class.isAssignableFrom(object.getClass())) {
1✔
813
      // ID is the key of each object
814
      Set<Entry<String, Object>> iterator = ((Map<String, Object>) object).entrySet();
1✔
815
      for (Entry<String, Object> inputOutputNode : iterator) {
1✔
816
        returnMap.put(inputOutputNode.getKey(), getDetails(inputOutputNode.getValue()));
1✔
817
      }
1✔
818
    }
819

820
    return returnMap;
1✔
821
  }
822

823
  /**
824
   * Gets the details of an input or output
825
   *
826
   * @param inputOutput The node of the particular input or output
827
   * @return An CWLElement object with the label, doc and type extracted
828
   */
829
  private CWLElement getDetails(Object inputOutput) {
830
    if (inputOutput != null) {
1✔
831
      CWLElement details = new CWLElement();
1✔
832

833
      // Shorthand notation "id: type" - no label/doc/other params
834
      if (inputOutput.getClass() == String.class) {
1✔
835
        details.setType((String) inputOutput);
1✔
836
      } else if (List.class.isAssignableFrom(inputOutput.getClass())) {
1✔
837
        details.setType(this.extractTypes(inputOutput));
1✔
838
      } else if (Map.class.isAssignableFrom(inputOutput.getClass())) {
1✔
839
        Map<String, Object> iOMap = (Map<String, Object>) inputOutput;
1✔
840
        details.setLabel(extractLabel(iOMap));
1✔
841
        details.setDoc(extractDoc(iOMap));
1✔
842
        extractSource(iOMap).forEach(details::addSourceID);
1✔
843
        details.setDefaultVal(extractDefault(iOMap));
1✔
844

845
        // Type is only for inputs
846
        if (iOMap.containsKey(TYPE)) {
1✔
847
          details.setType(extractTypes(iOMap.get(TYPE)));
1✔
848
        }
849
      }
850

851
      return details;
1✔
852
    }
UNCOV
853
    return null;
×
854
  }
855

856
  /**
857
   * Extract the id from a node
858
   *
859
   * @param step The node to have the id extracted from
860
   * @return The string for the id of the node
861
   */
862
  private String extractID(Map<String, Object> step) {
863
    if (step != null && step.containsKey(ID)) {
1✔
864
      String id = (String) step.get(ID);
1✔
865
      if (id.startsWith("#")) {
1✔
UNCOV
866
        return id.substring(1);
×
867
      }
868
      return id;
1✔
869
    }
UNCOV
870
    return null;
×
871
  }
872

873
  /**
874
   * Extract the default value from a node
875
   *
876
   * @param inputOutput The node to have the label extracted from
877
   * @return The string for the default value of the node
878
   */
879
  private String extractDefault(Map<String, Object> inputOutput) {
880
    if (inputOutput != null && inputOutput.containsKey(DEFAULT)) {
1✔
881
      Object default_value = ((Map<String, Object>) inputOutput).get(DEFAULT);
1✔
882
      if (default_value == null) {
1✔
883
        return null;
1✔
884
      }
885
      if (Map.class.isAssignableFrom(default_value.getClass())
1✔
886
          && ((Map<String, Object>) default_value).containsKey(LOCATION)) {
×
UNCOV
887
        return (String) ((Map<String, Object>) default_value).get(LOCATION);
×
888
      } else {
889
        return "\\\"" + default_value + "\\\"";
1✔
890
      }
891
    }
892
    return null;
1✔
893
  }
894

895
  /**
896
   * Extract the source or outputSource from a node
897
   *
898
   * @param inputOutput The node to have the sources extracted from
899
   * @return A list of strings for the sources
900
   */
901
  private List<String> extractSource(Map<String, Object> inputOutput) {
902
    if (inputOutput != null) {
1✔
903
      List<String> sources = new ArrayList<String>();
1✔
904
      Object sourceNode = null;
1✔
905

906
      // outputSource and source treated the same
907
      if (inputOutput.containsKey(OUTPUT_SOURCE)) {
1✔
908
        sourceNode = inputOutput.get(OUTPUT_SOURCE);
1✔
909
      } else if (inputOutput.containsKey(SOURCE)) {
1✔
910
        sourceNode = inputOutput.get(SOURCE);
1✔
911
      }
912

913
      if (sourceNode != null) {
1✔
914
        // Single source
915
        if (String.class.isAssignableFrom(sourceNode.getClass())) {
1✔
916
          sources.add(stepIDFromSource((String) sourceNode));
1✔
917
        }
918
        // Can be an array of multiple sources
919
        if (List.class.isAssignableFrom(sourceNode.getClass())) {
1✔
920
          for (String source : (List<String>) sourceNode) {
×
921
            sources.add(stepIDFromSource(source));
×
UNCOV
922
          }
×
923
        }
924
      }
925

926
      return sources;
1✔
927
    }
UNCOV
928
    return null;
×
929
  }
930

931
  /**
932
   * Gets just the step ID from source of format 'stepID</ or .>outputID'
933
   *
934
   * @param source The source
935
   * @return The step ID
936
   */
937
  private String stepIDFromSource(String source) {
938
    if (source != null && source.length() > 0) {
1✔
939
      // Strip leading # if it exists
940
      if (source.charAt(0) == '#') {
1✔
941
        source = source.substring(1);
1✔
942
      }
943

944
      // Draft 3/V1 notation is 'stepID/outputID'
945
      int slashSplit = source.indexOf("/");
1✔
946
      if (slashSplit != -1) {
1✔
947
        source = source.substring(0, slashSplit);
1✔
948
      } else {
949
        // Draft 2 notation was 'stepID.outputID'
950
        int dotSplit = source.indexOf(".");
1✔
951
        if (dotSplit != -1) {
1✔
UNCOV
952
          source = source.substring(0, dotSplit);
×
953
        }
954
      }
955
    }
956
    return source;
1✔
957
  }
958

959
  /**
960
   * Extract the doc or description from a node
961
   *
962
   * @param cwlFile The node to have the doc/description extracted from
963
   * @return The string for the doc/description of the node
964
   */
965
  private String extractDoc(Map<String, Object> cwlFile) {
966
    if (cwlFile != null) {
1✔
967
      if (cwlFile.containsKey(DOC)) {
1✔
968
        Object doc = cwlFile.get(DOC);
1✔
969
        if (doc == null) {
1✔
UNCOV
970
          return null;
×
971
        }
972
        if (doc.getClass().isAssignableFrom(String.class)) {
1✔
973
          return (String) doc;
1✔
974
        }
975
        if (doc instanceof List<?>) {
1✔
976
          List<String> docList = (List<String>) doc;
1✔
977
          return String.join("", docList);
1✔
978
        }
UNCOV
979
        return (String) cwlFile.get(DOC);
×
980
      } else if (cwlFile.containsKey(DESCRIPTION)) {
1✔
981
        // This is to support older standards of cwl which use description instead of
982
        // doc
983
        return (String) cwlFile.get(DESCRIPTION);
1✔
984
      }
985
    }
986
    return null;
1✔
987
  }
988

989
  /**
990
   * Extract the types from a node representing inputs or outputs
991
   *
992
   * @param typeNode The root node representing an input or output
993
   * @return A string with the types listed
994
   */
995
  private String extractTypes(Object typeNode) {
996
    if (typeNode != null) {
1✔
997
      if (typeNode.getClass() == String.class) {
1✔
998
        // Single type
999
        return (String) typeNode;
1✔
1000
      } else if (List.class.isAssignableFrom(typeNode.getClass())) {
1✔
1001
        // Multiple types, build a string to represent them
1002
        StringBuilder typeDetails = new StringBuilder();
1✔
1003
        boolean optional = false;
1✔
1004
        for (Object type : (List<Object>) typeNode) {
1✔
1005
          if (type.getClass() == String.class) {
1✔
1006
            // This is a simple type
1007
            if (((String) type).equals("null")) {
1✔
1008
              // null as a type means this field is optional
1009
              optional = true;
1✔
1010
            } else {
1011
              // Add a simple type to the string
1012
              typeDetails.append((String) type);
1✔
1013
              typeDetails.append(", ");
1✔
1014
            }
1015
          } else if (Map.class.isAssignableFrom(type.getClass())) {
1✔
1016
            // This is a verbose type with sub-fields broken down into type: and other
1017
            // params
1018
            if (((Map<String, Object>) type).get(TYPE).equals(ARRAY)) {
1✔
1019
              Object items = ((Map<String, Object>) type).get(ARRAY_ITEMS);
1✔
1020
              if (items.getClass() == String.class) {
1✔
1021
                typeDetails.append(items);
1✔
1022
                typeDetails.append("[], ");
1✔
1023
              } else {
1024
                typeDetails.append(type.toString() + ", ");
1✔
1025
              }
1026
            } else {
1✔
UNCOV
1027
              typeDetails.append((String) ((Map<String, Object>) type).get(TYPE));
×
1028
            }
1029
          }
1030
        }
1✔
1031

1032
        // Trim off excessive separators
1033
        if (typeDetails.length() > 1) {
1✔
1034
          typeDetails.setLength(typeDetails.length() - 2);
1✔
1035
        }
1036

1037
        // Add optional if null was included in the multiple types
1038
        if (optional) typeDetails.append("?");
1✔
1039

1040
        // Set the type to the constructed string
1041
        return typeDetails.toString();
1✔
1042

1043
      } else if (Map.class.isAssignableFrom(typeNode.getClass())) {
1✔
1044
        // Type: array and items:
1045
        if (((Map<String, Object>) typeNode).containsKey(ARRAY_ITEMS)) {
1✔
1046
          return extractTypes(((Map<String, String>) typeNode).get(ARRAY_ITEMS)) + "[]";
1✔
1047
        }
1048
      }
1049
    }
UNCOV
1050
    return null;
×
1051
  }
1052

1053
  /**
1054
   * Extract the run parameter from a node representing a step
1055
   *
1056
   * @param step The root node of a step
1057
   * @return A string with the run parameter if it exists
1058
   */
1059
  private Object extractRun(Map<String, Object> step) {
1060
    if (step != null) {
1✔
1061
      if (step.containsKey(RUN)) {
1✔
1062
        return step.get(RUN);
1✔
1063
      }
1064
    }
UNCOV
1065
    return null;
×
1066
  }
1067

1068
  public String normaliseLicenseLink(String licenseLink) {
1069
    if (licenseLink == null) {
1✔
UNCOV
1070
      return null;
×
1071
    }
1072
    String httpsLicenseLink = StringUtils.stripEnd(licenseLink.replace("http://", "https://"), "/");
1✔
1073
    return licenseVocab.getOrDefault(httpsLicenseLink, licenseLink);
1✔
1074
  }
1075
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc