Skip to content

Commit 8b13c2a

Browse files
Let users launch NextFlow from file browser (#470)
1 parent d106645 commit 8b13c2a

File tree

9 files changed

+331
-49
lines changed

9 files changed

+331
-49
lines changed

nextflow/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@ plugins {
66

77
dependencies {
88
BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: BuildUtils.getPlatformModuleProjectPath(project.gradle, "pipeline"), depProjectConfig: "published", depExtension: "module")
9-
}
109

10+
compileOnly "org.projectlombok:lombok:${lombokVersion}"
11+
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
12+
13+
BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: BuildUtils.getPlatformModuleProjectPath(project.gradle, "experiment"), depProjectConfig: "published", depExtension: "module")
14+
BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: BuildUtils.getPlatformModuleProjectPath(project.gradle, "pipeline"), depProjectConfig: "published", depExtension: "module")
15+
}

nextflow/src/org/labkey/nextflow/NextFlowController.java

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.labkey.nextflow;
22

3+
import lombok.Data;
4+
import lombok.Getter;
5+
import lombok.Setter;
36
import org.apache.commons.lang3.StringUtils;
47
import org.apache.logging.log4j.Logger;
58
import org.labkey.api.action.ApiResponse;
@@ -15,15 +18,21 @@
1518
import org.labkey.api.pipeline.PipelineJob;
1619
import org.labkey.api.pipeline.PipelineService;
1720
import org.labkey.api.pipeline.PipelineStatusUrls;
21+
import org.labkey.api.pipeline.browse.PipelinePathForm;
1822
import org.labkey.api.security.AdminConsoleAction;
1923
import org.labkey.api.security.RequiresPermission;
2024
import org.labkey.api.security.permissions.AdminOperationsPermission;
2125
import org.labkey.api.security.permissions.InsertPermission;
2226
import org.labkey.api.security.permissions.ReadPermission;
2327
import org.labkey.api.security.permissions.SiteAdminPermission;
2428
import org.labkey.api.util.Button;
29+
import org.labkey.api.util.DOM;
30+
import org.labkey.api.util.FileUtil;
31+
import org.labkey.api.util.HtmlString;
2532
import org.labkey.api.util.PageFlowUtil;
33+
import org.labkey.api.util.Path;
2634
import org.labkey.api.util.URLHelper;
35+
import org.labkey.api.util.element.Select;
2736
import org.labkey.api.util.logging.LogHelper;
2837
import org.labkey.api.view.ActionURL;
2938
import org.labkey.api.view.HtmlView;
@@ -36,14 +45,24 @@
3645
import org.springframework.validation.Errors;
3746
import org.springframework.web.servlet.ModelAndView;
3847

48+
import javax.swing.text.html.FormView;
49+
50+
import java.io.File;
51+
import java.nio.file.Paths;
52+
import java.util.Arrays;
53+
import java.util.List;
54+
3955
import static org.labkey.api.util.DOM.Attribute.checked;
56+
import static org.labkey.api.util.DOM.Attribute.hidden;
4057
import static org.labkey.api.util.DOM.Attribute.method;
4158
import static org.labkey.api.util.DOM.Attribute.name;
4259
import static org.labkey.api.util.DOM.Attribute.type;
4360
import static org.labkey.api.util.DOM.Attribute.value;
4461
import static org.labkey.api.util.DOM.DIV;
4562
import static org.labkey.api.util.DOM.INPUT;
63+
import static org.labkey.api.util.DOM.LI;
4664
import static org.labkey.api.util.DOM.LK.FORM;
65+
import static org.labkey.api.util.DOM.UL;
4766
import static org.labkey.api.util.DOM.at;
4867
import static org.labkey.nextflow.NextFlowManager.NEXTFLOW_CONFIG;
4968

@@ -245,11 +264,18 @@ public URLHelper getSuccessURL(EnabledForm o)
245264
}
246265
}
247266

267+
@Getter @Setter
268+
public static class AnalyzeForm extends PipelinePathForm
269+
{
270+
private boolean launch = false;
271+
private String configFile;
272+
}
273+
248274
@RequiresPermission(AdminOperationsPermission.class)
249-
public class NextFlowRunAction extends FormViewAction<Object>
275+
public class NextFlowRunAction extends FormViewAction<AnalyzeForm>
250276
{
251277
@Override
252-
public void validateCommand(Object o, Errors errors)
278+
public void validateCommand(AnalyzeForm o, Errors errors)
253279
{
254280
if (!NextFlowManager.get().isEnabled(getContainer()))
255281
{
@@ -258,26 +284,69 @@ public void validateCommand(Object o, Errors errors)
258284
}
259285

260286
@Override
261-
public ModelAndView getView(Object o, boolean b, BindException errors)
287+
public ModelAndView getView(AnalyzeForm o, boolean b, BindException errors)
262288
{
263-
return new HtmlView("NextFlow Runner", DIV("Run NextFlow Pipeline",
264-
FORM(at(method, "POST"),
265-
new Button.ButtonBuilder("Start NextFlow").submit(true).build())));
289+
NextFlowConfiguration config = NextFlowManager.get().getConfiguration();
290+
if (config.getNextFlowConfigFilePath() != null)
291+
{
292+
File configDir = new File(config.getNextFlowConfigFilePath());
293+
if (configDir.isDirectory())
294+
{
295+
File[] files = configDir.listFiles();
296+
if (files != null && files.length > 0)
297+
{
298+
List<File> configFiles = Arrays.asList(files);
299+
return new HtmlView("NextFlow Runner", DIV(
300+
FORM(at(method, "POST"),
301+
INPUT(at(hidden, true, name, "launch", value, true)),
302+
Arrays.stream(o.getFile()).map(f -> INPUT(at(hidden, true, name, "file", value, f))).toList(),
303+
"Files: ",
304+
UL(Arrays.stream(o.getFile()).map(DOM::LI)),
305+
"Config: ",
306+
new Select.SelectBuilder().name("configFile").addOptions(configFiles.stream().filter(f -> f.isFile() && f.getName().toLowerCase().endsWith(".config")).map(File::getName).sorted(String.CASE_INSENSITIVE_ORDER).toList()).build(),
307+
new Button.ButtonBuilder("Start NextFlow").submit(true).build())));
308+
}
309+
}
310+
}
311+
return new HtmlView(HtmlString.of("Couldn't find NextFlow config file(s)"));
266312
}
267313

268314
@Override
269-
public boolean handlePost(Object o, BindException errors) throws Exception
315+
public boolean handlePost(AnalyzeForm form, BindException errors) throws Exception
270316
{
271-
ViewBackgroundInfo info = getViewBackgroundInfo();
272-
PipeRoot root = PipelineService.get().findPipelineRoot(info.getContainer());
273-
PipelineJob job = new NextFlowPipelineJob(info, root);
274-
PipelineService.get().queueJob(job);
317+
if (!form.isLaunch())
318+
{
319+
return false;
320+
}
321+
322+
NextFlowConfiguration config = NextFlowManager.get().getConfiguration();
323+
File configDir = new File(config.getNextFlowConfigFilePath());
324+
File configFile = FileUtil.appendPath(configDir, Path.parse(form.getConfigFile()));
325+
if (!configFile.exists())
326+
{
327+
errors.reject(ERROR_MSG, "Config file does not exist");
328+
}
329+
else
330+
{
331+
List<File> inputFiles = form.getValidatedFiles(getContainer());
332+
if (inputFiles.isEmpty())
333+
{
334+
errors.reject(ERROR_MSG, "No input files");
335+
}
336+
else
337+
{
338+
ViewBackgroundInfo info = getViewBackgroundInfo();
339+
PipeRoot root = PipelineService.get().findPipelineRoot(info.getContainer());
340+
PipelineJob job = NextFlowPipelineJob.create(info, root, configFile.toPath(), inputFiles.stream().map(File::toPath).toList());
341+
PipelineService.get().queueJob(job);
342+
}
343+
}
275344

276345
return !errors.hasErrors();
277346
}
278347

279348
@Override
280-
public URLHelper getSuccessURL(Object o)
349+
public URLHelper getSuccessURL(AnalyzeForm o)
281350
{
282351
return PageFlowUtil.urlProvider(PipelineStatusUrls.class).urlBegin(getContainer());
283352
}

nextflow/src/org/labkey/nextflow/NextFlowManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import org.labkey.api.data.PropertyManager;
88
import org.springframework.validation.BindException;
99

10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.Paths;
1013
import java.util.HashMap;
1114
import java.util.Map;
1215

@@ -44,6 +47,12 @@ private void checkArgs(NextFlowConfiguration config, BindException errors)
4447
if (StringUtils.isEmpty(config.getNextFlowConfigFilePath()))
4548
errors.rejectValue("nextFlowConfigFilePath", ERROR_MSG, "NextFlow config file path is required");
4649

50+
Path configPath = Paths.get(config.getNextFlowConfigFilePath());
51+
if (Files.isDirectory(configPath))
52+
{
53+
errors.rejectValue("nextFlowConfigFilePath", ERROR_MSG, "NextFlow config file path must be a directory");
54+
}
55+
4756
// Not yet used
4857
// if (StringUtils.isEmpty(config.getAccountName()))
4958
// errors.rejectValue("accountName", ERROR_MSG, "AWS account name is required");

nextflow/src/org/labkey/nextflow/NextFlowModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext)
2121
{
2222
ActionURL adminUrl = new ActionURL(NextFlowController.NextFlowConfigurationAction.class, ContainerManager.getRoot());
2323
AdminConsole.addLink(AdminConsole.SettingsLinkType.Configuration, "NextFlow Configuration", adminUrl, AdminPermission.class);
24+
25+
PipelineService.get().registerPipelineProvider(new NextFlowPipelineProvider(this));
2426
}
2527

2628
@Override
Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,120 @@
11
package org.labkey.nextflow.pipeline;
22

3+
import lombok.Getter;
4+
import org.apache.commons.lang3.StringUtils;
35
import org.jetbrains.annotations.NotNull;
6+
import org.labkey.api.data.Container;
7+
import org.labkey.api.files.FileContentService;
8+
import org.labkey.api.pipeline.ParamParser;
49
import org.labkey.api.pipeline.PipeRoot;
5-
import org.labkey.api.pipeline.PipelineJob;
610
import org.labkey.api.pipeline.PipelineJobService;
711
import org.labkey.api.pipeline.TaskId;
812
import org.labkey.api.pipeline.TaskPipeline;
13+
import org.labkey.api.pipeline.file.AbstractFileAnalysisJob;
914
import org.labkey.api.util.FileUtil;
10-
import org.labkey.api.util.URLHelper;
15+
import org.labkey.api.util.PageFlowUtil;
1116
import org.labkey.api.view.ViewBackgroundInfo;
1217

18+
import java.io.BufferedWriter;
1319
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.List;
1425

15-
public class NextFlowPipelineJob extends PipelineJob
26+
@Getter
27+
public class NextFlowPipelineJob extends AbstractFileAnalysisJob
1628
{
29+
private Path config;
30+
1731
@SuppressWarnings("unused") // For serialization
1832
protected NextFlowPipelineJob()
1933
{}
2034

21-
public NextFlowPipelineJob(ViewBackgroundInfo info, @NotNull PipeRoot root)
35+
public static NextFlowPipelineJob create(ViewBackgroundInfo info, @NotNull PipeRoot root, Path templateConfig, List<Path> inputFiles) throws IOException
2236
{
23-
super(null, info, root);
24-
setLogFile(new File(String.valueOf(root.getLogDirectory()), FileUtil.makeFileNameWithTimestamp("NextFlowPipelineJob", "log")).toPath());
37+
Path parentDir = inputFiles.get(0).getParent();
38+
39+
String jobName = FileUtil.makeFileNameWithTimestamp("NextFlow");
40+
Path jobDir = parentDir.resolve(jobName);
41+
Path log = jobDir.resolve(jobName + ".log");
42+
FileUtil.createDirectory(jobDir);
43+
44+
Path config = createConfig(templateConfig, log.getParent(), jobDir, info.getContainer());
45+
46+
return new NextFlowPipelineJob(info, root, config, inputFiles, log);
2547
}
2648

27-
@Override
28-
public URLHelper getStatusHref()
49+
public NextFlowPipelineJob(ViewBackgroundInfo info, @NotNull PipeRoot root, Path config, List<Path> inputFiles, Path log) throws IOException
2950
{
30-
return null;
51+
super(new NextFlowProtocol(), NextFlowPipelineProvider.NAME, info, root, config.getFileName().toString(), config, inputFiles, false, false);
52+
this.config = config;
53+
setLogFile(log);
54+
}
55+
56+
@Override
57+
public ParamParser getInputParameters()
58+
{
59+
return PipelineJobService.get().createParamParser();
60+
}
61+
62+
/** Take the template config file and substitute in the values for this job */
63+
private static Path createConfig(Path configTemplate, Path parentDir, Path jobDir, Container container) throws IOException
64+
{
65+
String template;
66+
try (InputStream in = Files.newInputStream(configTemplate))
67+
{
68+
template = PageFlowUtil.getStreamContentsAsString(in);
69+
}
70+
71+
String webdavUrl = FileContentService.get().getWebDavUrl(parentDir, container, FileContentService.PathType.full);
72+
webdavUrl = StringUtils.stripEnd(webdavUrl, "/");
73+
74+
String substitutedContent = template.replace("${quant_spectra_dir}", "quant_spectra_dir = '" + webdavUrl + "'");
75+
76+
Path substitutedFile = jobDir.resolve(configTemplate.getFileName());
77+
try (BufferedWriter writer = Files.newBufferedWriter(substitutedFile))
78+
{
79+
writer.write(substitutedContent);
80+
}
81+
return substitutedFile;
3182
}
3283

3384
@Override
3485
public String getDescription()
3586
{
36-
return "NextFlow Job";
87+
return "NextFlow analysis using " + config.getFileName() + " of " + getInputFilePaths().size() + " files";
3788
}
3889

3990
@Override
4091
public TaskPipeline<?> getTaskPipeline()
4192
{
42-
return PipelineJobService.get().getTaskPipeline(new TaskId(NextFlowPipelineJob.class));
93+
return PipelineJobService.get().getTaskPipeline(getTaskPipelineId());
4394
}
95+
96+
@Override
97+
public TaskId getTaskPipelineId()
98+
{
99+
return new TaskId(NextFlowPipelineJob.class);
100+
}
101+
102+
@Override
103+
public AbstractFileAnalysisJob createSingleFileJob(File file)
104+
{
105+
throw new UnsupportedOperationException();
106+
}
107+
108+
@Override
109+
public File findInputFile(String name)
110+
{
111+
throw new UnsupportedOperationException();
112+
}
113+
114+
@Override
115+
public File findOutputFile(String name)
116+
{
117+
return null;
118+
}
119+
44120
}
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package org.labkey.nextflow.pipeline;
22

3-
import org.labkey.api.module.Module;
43
import org.labkey.api.pipeline.PipeRoot;
54
import org.labkey.api.pipeline.PipelineDirectory;
65
import org.labkey.api.pipeline.PipelineProvider;
76
import org.labkey.api.security.permissions.InsertPermission;
87
import org.labkey.api.view.ViewContext;
8+
import org.labkey.nextflow.NextFlowController;
99
import org.labkey.nextflow.NextFlowManager;
1010
import org.labkey.nextflow.NextFlowModule;
1111

1212
public class NextFlowPipelineProvider extends PipelineProvider
1313
{
14+
15+
public static final String NAME = "NextFlow";
16+
1417
public NextFlowPipelineProvider(NextFlowModule owningModule)
1518
{
16-
super("NextFlow", owningModule);
19+
super(NAME, owningModule);
1720
}
1821

1922
@Override
@@ -23,7 +26,15 @@ public void updateFileProperties(ViewContext context, PipeRoot pr, PipelineDirec
2326
return;
2427
if (!NextFlowManager.get().isEnabled(context.getContainer()))
2528
return;
26-
}
27-
2829

30+
String actionId = createActionId(NextFlowController.NextFlowRunAction.class, "Analyze with NextFlow");
31+
addAction(actionId,
32+
NextFlowController.NextFlowRunAction.class,
33+
"Analyze with NextFlow",
34+
directory,
35+
directory.listPaths(new FileTypesEntryFilter(NextFlowProtocol.INPUT_TYPES)),
36+
true,
37+
true,
38+
includeAll);
39+
}
2940
}

0 commit comments

Comments
 (0)