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

common-workflow-language / cwlviewer / #1992

12 May 2026 05:53PM UTC coverage: 70.861% (+0.5%) from 70.334%
#1992

Pull #751

github

kinow
Build CodeQL with JVM 25
Pull Request #751: Bump org.springframework.boot:spring-boot-starter-parent from 3.1.4 to 4.1.0-RC1

42 of 90 new or added lines in 23 files covered. (46.67%)

19 existing lines in 3 files now uncovered.

1695 of 2392 relevant lines covered (70.86%)

0.71 hits per line

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

81.0
/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✔
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 {
157
        throw new IOException("The file given was not recognised as a packed CWL file");
×
158
      }
159

160
      return overviews;
1✔
161

162
    } else {
163
      throw new IOException(
×
164
          "File '"
165
              + packedFile.getName()
×
166
              + "' is over singleFileSizeLimit - "
167
              + FileUtils.byteCountToDisplaySize(packedFile.length())
×
168
              + "/"
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 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✔
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✔
212
      throw new CWLNotAWorkflowException(
×
213
          "Not a 'class: Workflow' CWL document, is a " + extractProcess(cwlFile));
×
214
    }
215
    if (!found) {
1✔
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) {
×
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✔
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(
×
310
              workTreeUri.toString(), "https://w3id.org/cwl/view/git/" + latestCommit + "/");
×
311
      // Workaround for common-workflow-language/cwltool#427
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();
×
316
      model.read(new ByteArrayInputStream(rdf.getBytes()), null, "TURTLE");
×
317

318
      // Store the model
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✔
329
        label = labelAndDocSoln.get("label").toString();
×
330
      }
331
      if (labelAndDocSoln.contains("doc")) {
1✔
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 {
349
          type = typeURIToString(input.get("type").toString());
×
350
        }
351
        if (input.contains("null")) {
1✔
352
          type += " (Optional)";
×
353
        }
354
        wfInput.setType(type);
1✔
355
      }
356
      if (input.contains("format")) {
1✔
357
        String format = input.get("format").toString();
×
358
        setFormat(wfInput, format);
×
359
      }
360
      if (input.contains("label")) {
1✔
361
        wfInput.setLabel(input.get("label").toString());
×
362
      }
363
      if (input.contains("doc")) {
1✔
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✔
380
          type = typeURIToString(output.get("items").toString()) + "[]";
×
381
        } else {
382
          type = typeURIToString(output.get("type").toString());
1✔
383
        }
384
        if (output.contains("null")) {
1✔
385
          type += " (Optional)";
×
386
        }
387
        wfOutput.setType(type);
1✔
388
      }
389

390
      if (output.contains("src")) {
1✔
391
        wfOutput.addSourceID(rdfService.stepNameFromURI(gitPath, output.get("src").toString()));
×
392
      }
393
      if (output.contains("format")) {
1✔
394
        String format = output.get("format").toString();
×
395
        setFormat(wfOutput, format);
×
396
      }
397
      if (output.contains("label")) {
1✔
398
        wfOutput.setLabel(output.get("label").toString());
×
399
      }
400
      if (output.contains("doc")) {
1✔
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
        String runValue = step.get("run").asResource().toString();
1✔
429
        wfStep.setRun(workflowPath.relativize(runValue).toString());
1✔
430
        wfStep.setRunType(rdfService.strToRuntype(step.get("runtype").toString()));
1✔
431

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

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

476
    // Create workflow model
477
    Workflow workflowModel =
1✔
478
        new Workflow(label, doc, wfInputs, wfOutputs, wfSteps, dockerLink, licenseLink);
479

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

490
    return workflowModel;
1✔
491
  }
492

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

502
    // Get the content of this file from Github
503
    long fileSizeBytes = file.length();
1✔
504

505
    // Check file size limit before parsing
506
    if (fileSizeBytes <= singleFileSizeLimit) {
1✔
507

508
      // Parse file as yaml
509
      Map<String, Object> cwlFile = yamlPathToJson(file.toPath());
1✔
510

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

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

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

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

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

593
  /**
594
   * Converts a yaml String to JsonNode
595
   *
596
   * @param path A Path to a file containing the YAML content
597
   * @return A JsonNode with the content of the document
598
   * @throws IOException If it fails to read open a stream to the given path
599
   */
600
  private Map<String, Object> yamlPathToJson(Path path) throws IOException {
601
    LoadSettings settings = LoadSettings.builder().build();
1✔
602
    Load load = new Load(settings);
1✔
603
    try (InputStream in = Files.newInputStream(path)) {
1✔
604
      return (Map<String, Object>) load.loadFromInputStream(in);
1✔
605
    }
606
  }
607

608
  /**
609
   * Converts a YAML String to JsonNode
610
   *
611
   * @param yamlStream An InputStream containing the YAML content
612
   * @return A JsonNode with the content of the document
613
   */
614
  private Map<String, Object> yamlStreamToJson(InputStream yamlStream) {
615
    LoadSettings settings = LoadSettings.builder().build();
1✔
616
    Load load = new Load(settings);
1✔
617
    return (Map<String, Object>) load.loadFromInputStream(yamlStream);
1✔
618
  }
619

620
  /**
621
   * Extract the label from a node
622
   *
623
   * @param node The node to have the label extracted from
624
   * @return The string for the label of the node
625
   */
626
  private String extractLabel(Map<String, Object> node) {
627
    if (node != null && node.containsKey(LABEL)) {
1✔
628
      return (String) node.get(LABEL);
1✔
629
    }
630
    return null;
1✔
631
  }
632

633
  /**
634
   * Extract the class parameter from a node representing a document
635
   *
636
   * @param node The root node of a cwl document
637
   * @return Which process this document represents
638
   */
639
  private CWLProcess extractProcess(Map<String, Object> node) {
640
    if (node != null) {
1✔
641
      if (node.containsKey(CLASS)) {
1✔
642
        switch ((String) node.get(CLASS)) {
1✔
643
          case WORKFLOW:
644
            return CWLProcess.WORKFLOW;
1✔
645
          case COMMANDLINETOOL:
646
            return CWLProcess.COMMANDLINETOOL;
1✔
647
          case EXPRESSIONTOOL:
648
            return CWLProcess.EXPRESSIONTOOL;
×
649
        }
650
      }
651
    }
652
    return null;
×
653
  }
654

655
  /**
656
   * Get the steps for a particular document
657
   *
658
   * @param cwlDoc The document to get steps for
659
   * @return A map of step IDs and details related to them
660
   */
661
  private Map<String, CWLStep> getSteps(Map<String, Object> cwlDoc) {
662
    if (cwlDoc != null && cwlDoc.containsKey(STEPS)) {
1✔
663
      Map<String, CWLStep> returnMap = new HashMap<>();
1✔
664

665
      Object steps = cwlDoc.get(STEPS);
1✔
666
      if (List.class.isAssignableFrom(steps.getClass())) {
1✔
667
        // Explicit ID and other fields within each input list
668
        for (Map<String, Object> step : (List<Map<String, Object>>) steps) {
1✔
669
          CWLStep stepObject =
1✔
670
              new CWLStep(extractLabel(step), extractDoc(step), extractRun(step), getInputs(step));
1✔
671
          returnMap.put(extractID(step), stepObject);
1✔
672
        }
1✔
673
      } else if (Map.class.isAssignableFrom(steps.getClass())) {
1✔
674
        // ID is the key of each object
675
        for (Entry<String, Map<String, Object>> stepEntry :
676
            ((Map<String, Map<String, Object>>) steps).entrySet()) {
1✔
677
          Map<String, Object> step = stepEntry.getValue();
1✔
678
          CWLStep stepObject =
1✔
679
              new CWLStep(extractLabel(step), extractDoc(step), extractRun(step), getInputs(step));
1✔
680
          returnMap.put(stepEntry.getKey(), stepObject);
1✔
681
        }
1✔
682
      }
683

684
      return returnMap;
1✔
685
    }
686
    return null;
×
687
  }
688

689
  /**
690
   * Get a the inputs for a particular document
691
   *
692
   * @param cwlDoc The document to get inputs for
693
   * @return A map of input IDs and details related to them
694
   */
695
  private Map<String, CWLElement> getInputs(Map<String, Object> cwlDoc) {
696
    if (cwlDoc != null) {
1✔
697
      if (cwlDoc.containsKey(INPUTS)) {
1✔
698
        // For all version workflow inputs/outputs and draft steps
699
        return getInputsOutputs(cwlDoc.get(INPUTS));
1✔
700
      } else if (cwlDoc.containsKey(IN)) {
1✔
701
        // For V1.0 steps
702
        return getStepInputsOutputs(cwlDoc.get(IN));
1✔
703
      }
704
    }
705
    return null;
×
706
  }
707

708
  /**
709
   * Get the outputs for a particular document
710
   *
711
   * @param cwlDoc The document to get outputs for
712
   * @return A map of output IDs and details related to them
713
   */
714
  private Map<String, CWLElement> getOutputs(Map<String, Object> cwlDoc) {
715
    if (cwlDoc != null) {
1✔
716
      // For all version workflow inputs/outputs and draft steps
717
      if (cwlDoc.containsKey(OUTPUTS)) {
1✔
718
        return getInputsOutputs(cwlDoc.get(OUTPUTS));
1✔
719
      }
720
      // Outputs are not gathered for v1 steps
721
    }
722
    return null;
×
723
  }
724

725
  /**
726
   * Get inputs or outputs from an in or out node
727
   *
728
   * @param inOut The in or out node
729
   * @return A map of input IDs and details related to them
730
   */
731
  private Map<String, CWLElement> getStepInputsOutputs(Object inOut) {
732
    Map<String, CWLElement> returnMap = new HashMap<>();
1✔
733

734
    if (List.class.isAssignableFrom(inOut.getClass())) {
1✔
735
      // array<WorkflowStepInput>
736
      for (Map<String, Object> inOutNode : (List<Map<String, Object>>) inOut) {
1✔
737
        CWLElement inputOutput = new CWLElement();
1✔
738
        List<String> sources = extractSource(inOutNode);
1✔
739
        if (!sources.isEmpty()) {
1✔
740
          for (String source : sources) {
1✔
741
            inputOutput.addSourceID(stepIDFromSource(source));
1✔
742
          }
1✔
743
        } else {
744
          inputOutput.setDefaultVal(extractDefault(inOutNode));
1✔
745
        }
746
        returnMap.put(extractID(inOutNode), inputOutput);
1✔
747
      }
1✔
748
    } else if (Map.class.isAssignableFrom(inOut.getClass())) {
1✔
749
      // map<WorkflowStepInput.id, WorkflowStepInput.source>
750
      Set<Entry<String, Object>> iterator = ((Map<String, Object>) inOut).entrySet();
1✔
751
      for (Entry<String, Object> entry : iterator) {
1✔
752
        Object inOutNode = entry.getValue();
1✔
753
        CWLElement inputOutput = new CWLElement();
1✔
754
        if (Map.class.isAssignableFrom(inOutNode.getClass())) {
1✔
755
          Map<String, Object> properties = (Map<String, Object>) inOutNode;
1✔
756
          if (properties.containsKey(SOURCE)) {
1✔
757
            Object source = properties.get(SOURCE);
1✔
758
            if (List.class.isAssignableFrom(source.getClass())) {
1✔
759
              for (String sourceEntry : (List<String>) source) {
1✔
760
                inputOutput.addSourceID(stepIDFromSource(sourceEntry));
1✔
761
              }
1✔
762
            } else {
763
              inputOutput.addSourceID(stepIDFromSource((String) source));
1✔
764
            }
765
          } else {
1✔
766
            inputOutput.setDefaultVal(extractDefault(properties));
1✔
767
          }
768
        } else if (List.class.isAssignableFrom(inOutNode.getClass())) {
1✔
769
          for (String key : (List<String>) inOutNode) {
×
770
            inputOutput.addSourceID(stepIDFromSource(key));
×
771
          }
×
772
        } else {
773
          inputOutput.addSourceID(stepIDFromSource((String) inOutNode));
1✔
774
        }
775
        returnMap.put(entry.getKey(), inputOutput);
1✔
776
      }
1✔
777
    }
778

779
    return returnMap;
1✔
780
  }
781

782
  /**
783
   * Get inputs or outputs from an inputs or outputs node
784
   *
785
   * @param object The inputs or outputs node
786
   * @return A map of input IDs and details related to them
787
   */
788
  private Map<String, CWLElement> getInputsOutputs(Object object) {
789
    Map<String, CWLElement> returnMap = new HashMap<>();
1✔
790

791
    if (List.class.isAssignableFrom(object.getClass())) {
1✔
792
      // Explicit ID and other fields within each list
793
      for (Map<String, Object> inputOutput : (List<Map<String, Object>>) object) {
1✔
794
        String id = (String) inputOutput.get(ID);
1✔
795
        if (id.charAt(0) == '#') {
1✔
796
          id = id.substring(1);
×
797
        }
798
        returnMap.put(id, getDetails(inputOutput));
1✔
799
      }
1✔
800
    } else if (Map.class.isAssignableFrom(object.getClass())) {
1✔
801
      // ID is the key of each object
802
      Set<Entry<String, Object>> iterator = ((Map<String, Object>) object).entrySet();
1✔
803
      for (Entry<String, Object> inputOutputNode : iterator) {
1✔
804
        returnMap.put(inputOutputNode.getKey(), getDetails(inputOutputNode.getValue()));
1✔
805
      }
1✔
806
    }
807

808
    return returnMap;
1✔
809
  }
810

811
  /**
812
   * Gets the details of an input or output
813
   *
814
   * @param inputOutput The node of the particular input or output
815
   * @return An CWLElement object with the label, doc and type extracted
816
   */
817
  private CWLElement getDetails(Object inputOutput) {
818
    if (inputOutput != null) {
1✔
819
      CWLElement details = new CWLElement();
1✔
820

821
      // Shorthand notation "id: type" - no label/doc/other params
822
      if (inputOutput.getClass() == String.class) {
1✔
823
        details.setType((String) inputOutput);
1✔
824
      } else if (List.class.isAssignableFrom(inputOutput.getClass())) {
1✔
825
        details.setType(this.extractTypes(inputOutput));
1✔
826
      } else if (Map.class.isAssignableFrom(inputOutput.getClass())) {
1✔
827
        Map<String, Object> iOMap = (Map<String, Object>) inputOutput;
1✔
828
        details.setLabel(extractLabel(iOMap));
1✔
829
        details.setDoc(extractDoc(iOMap));
1✔
830
        extractSource(iOMap).forEach(details::addSourceID);
1✔
831
        details.setDefaultVal(extractDefault(iOMap));
1✔
832

833
        // Type is only for inputs
834
        if (iOMap.containsKey(TYPE)) {
1✔
835
          details.setType(extractTypes(iOMap.get(TYPE)));
1✔
836
        }
837
      }
838

839
      return details;
1✔
840
    }
841
    return null;
×
842
  }
843

844
  /**
845
   * Extract the id from a node
846
   *
847
   * @param step The node to have the id extracted from
848
   * @return The string for the id of the node
849
   */
850
  private String extractID(Map<String, Object> step) {
851
    if (step != null && step.containsKey(ID)) {
1✔
852
      String id = (String) step.get(ID);
1✔
853
      if (id.startsWith("#")) {
1✔
854
        return id.substring(1);
×
855
      }
856
      return id;
1✔
857
    }
858
    return null;
×
859
  }
860

861
  /**
862
   * Extract the default value from a node
863
   *
864
   * @param inputOutput The node to have the label extracted from
865
   * @return The string for the default value of the node
866
   */
867
  private String extractDefault(Map<String, Object> inputOutput) {
868
    if (inputOutput != null && inputOutput.containsKey(DEFAULT)) {
1✔
869
      Object default_value = inputOutput.get(DEFAULT);
1✔
870
      if (default_value == null) {
1✔
871
        return null;
1✔
872
      }
873
      if (Map.class.isAssignableFrom(default_value.getClass())
1✔
874
          && ((Map<String, Object>) default_value).containsKey(LOCATION)) {
×
875
        return (String) ((Map<String, Object>) default_value).get(LOCATION);
×
876
      } else {
877
        return "\\\"" + default_value + "\\\"";
1✔
878
      }
879
    }
880
    return null;
1✔
881
  }
882

883
  /**
884
   * Extract the source or outputSource from a node
885
   *
886
   * @param inputOutput The node to have the sources extracted from
887
   * @return A list of strings for the sources
888
   */
889
  private List<String> extractSource(Map<String, Object> inputOutput) {
890
    if (inputOutput != null) {
1✔
891
      List<String> sources = new ArrayList<>();
1✔
892
      Object sourceNode = null;
1✔
893

894
      // outputSource and source treated the same
895
      if (inputOutput.containsKey(OUTPUT_SOURCE)) {
1✔
896
        sourceNode = inputOutput.get(OUTPUT_SOURCE);
1✔
897
      } else if (inputOutput.containsKey(SOURCE)) {
1✔
898
        sourceNode = inputOutput.get(SOURCE);
1✔
899
      }
900

901
      if (sourceNode != null) {
1✔
902
        // Single source
903
        if (String.class.isAssignableFrom(sourceNode.getClass())) {
1✔
904
          sources.add(stepIDFromSource((String) sourceNode));
1✔
905
        }
906
        // Can be an array of multiple sources
907
        if (List.class.isAssignableFrom(sourceNode.getClass())) {
1✔
908
          for (String source : (List<String>) sourceNode) {
×
909
            sources.add(stepIDFromSource(source));
×
910
          }
×
911
        }
912
      }
913

914
      return sources;
1✔
915
    }
916
    return null;
×
917
  }
918

919
  /**
920
   * Gets just the step ID from source of format 'stepID</ or .>outputID'
921
   *
922
   * @param source The source
923
   * @return The step ID
924
   */
925
  private String stepIDFromSource(String source) {
926
    if (source != null && !source.isEmpty()) {
1✔
927
      // Strip leading # if it exists
928
      if (source.charAt(0) == '#') {
1✔
929
        source = source.substring(1);
1✔
930
      }
931

932
      // Draft 3/V1 notation is 'stepID/outputID'
933
      int slashSplit = source.indexOf("/");
1✔
934
      if (slashSplit != -1) {
1✔
935
        source = source.substring(0, slashSplit);
1✔
936
      } else {
937
        // Draft 2 notation was 'stepID.outputID'
938
        int dotSplit = source.indexOf(".");
1✔
939
        if (dotSplit != -1) {
1✔
940
          source = source.substring(0, dotSplit);
×
941
        }
942
      }
943
    }
944
    return source;
1✔
945
  }
946

947
  /**
948
   * Extract the doc or description from a node
949
   *
950
   * @param cwlFile The node to have the doc/description extracted from
951
   * @return The string for the doc/description of the node
952
   */
953
  private String extractDoc(Map<String, Object> cwlFile) {
954
    if (cwlFile != null) {
1✔
955
      if (cwlFile.containsKey(DOC)) {
1✔
956
        Object doc = cwlFile.get(DOC);
1✔
957
        if (doc == null) {
1✔
958
          return null;
×
959
        }
960
        if (doc.getClass().isAssignableFrom(String.class)) {
1✔
961
          return (String) doc;
1✔
962
        }
963
        if (doc instanceof List<?>) {
1✔
964
          List<String> docList = (List<String>) doc;
1✔
965
          return String.join("", docList);
1✔
966
        }
967
        return (String) cwlFile.get(DOC);
×
968
      } else if (cwlFile.containsKey(DESCRIPTION)) {
1✔
969
        // This is to support older standards of cwl which use description instead of
970
        // doc
971
        return (String) cwlFile.get(DESCRIPTION);
1✔
972
      }
973
    }
974
    return null;
1✔
975
  }
976

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

1020
        // Trim off excessive separators
1021
        if (typeDetails.length() > 1) {
1✔
1022
          typeDetails.setLength(typeDetails.length() - 2);
1✔
1023
        }
1024

1025
        // Add optional if null was included in the multiple types
1026
        if (optional) typeDetails.append("?");
1✔
1027

1028
        // Set the type to the constructed string
1029
        return typeDetails.toString();
1✔
1030

1031
      } else if (Map.class.isAssignableFrom(typeNode.getClass())) {
1✔
1032
        // Type: array and items:
1033
        if (((Map<String, Object>) typeNode).containsKey(ARRAY_ITEMS)) {
1✔
1034
          return extractTypes(((Map<String, String>) typeNode).get(ARRAY_ITEMS)) + "[]";
1✔
1035
        }
1036
      }
1037
    }
1038
    return null;
×
1039
  }
1040

1041
  /**
1042
   * Extract the run parameter from a node representing a step
1043
   *
1044
   * @param step The root node of a step
1045
   * @return A string with the run parameter if it exists
1046
   */
1047
  private Object extractRun(Map<String, Object> step) {
1048
    if (step != null) {
1✔
1049
      if (step.containsKey(RUN)) {
1✔
1050
        return step.get(RUN);
1✔
1051
      }
1052
    }
1053
    return null;
×
1054
  }
1055

1056
  public String normaliseLicenseLink(String licenseLink) {
1057
    if (licenseLink == null) {
1✔
1058
      return null;
×
1059
    }
1060
    String httpsLicenseLink = StringUtils.stripEnd(licenseLink.replace("http://", "https://"), "/");
1✔
1061
    return licenseVocab.getOrDefault(httpsLicenseLink, licenseLink);
1✔
1062
  }
1063
}
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