Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.commonwl.view.workflow; import java.io.File; import java.io.IOException; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import org.apache.commons.lang.StringUtils; import org.commonwl.view.WebConfig; import org.commonwl.view.cwl.CWLToolStatus; import org.commonwl.view.git.GitDetails; import org.commonwl.view.graphviz.GraphVizService; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.TransportException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller public class WorkflowController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final WorkflowFormValidator workflowFormValidator; private final WorkflowService workflowService; private final GraphVizService graphVizService; /** * Autowired constructor to initialise objects used by the controller. * * @param workflowFormValidator * Validator to validate the workflow form * @param workflowService * Builds new Workflow objects * @param graphVizService * Generates and stores images */ @Autowired public WorkflowController(WorkflowFormValidator workflowFormValidator, WorkflowService workflowService, GraphVizService graphVizService) { this.workflowFormValidator = workflowFormValidator; this.workflowService = workflowService; this.graphVizService = graphVizService; } /** * List all the workflows in the database, paginated * @param model The model for the page * @param pageable Pagination for the list of workflows * @return The workflows view */ @GetMapping(value = "/workflows") public String listWorkflows(Model model, @PageableDefault(size = 10) Pageable pageable) { model.addAttribute("workflows", workflowService.getPageOfWorkflows(pageable)); model.addAttribute("pages", pageable); return "workflows"; } /** * Search all the workflows in the database, paginated * @param model The model for the page * @param pageable Pagination for the list of workflows * @return The workflows view */ @GetMapping(value = "/workflows", params = "search") public String searchWorkflows(Model model, @PageableDefault(size = 10) Pageable pageable, @RequestParam(value = "search") String search) { model.addAttribute("workflows", workflowService.searchPageOfWorkflows(search, pageable)); model.addAttribute("pages", pageable); model.addAttribute("search", search); return "workflows"; } /** * Create a new workflow from the given URL in the form * @param workflowForm The data submitted from the form * @param bindingResult Spring MVC Binding Result object * @return The workflow view with new workflow as a model */ @PostMapping("/workflows") public ModelAndView createWorkflow(@Valid WorkflowForm workflowForm, BindingResult bindingResult) { // Run validator which checks the git URL is valid GitDetails gitInfo = workflowFormValidator.validateAndParse(workflowForm, bindingResult); if (bindingResult.hasErrors() || gitInfo == null) { // Go back to index if there are validation errors return new ModelAndView("index"); } else { // Get workflow or create if does not exist Workflow workflow = workflowService.getWorkflow(gitInfo); if (workflow == null) { try { if (gitInfo.getPath().endsWith(".cwl")) { QueuedWorkflow result = workflowService.createQueuedWorkflow(gitInfo); if (result.getWorkflowList() == null) { workflow = result.getTempRepresentation(); } else { if (result.getWorkflowList().size() == 1) { gitInfo.setPackedId(result.getWorkflowList().get(0).getFileName()); } return new ModelAndView("redirect:" + gitInfo.getInternalUrl()); } } else { return new ModelAndView("redirect:" + gitInfo.getInternalUrl()); } } catch (TransportException ex) { logger.warn("git.sshError " + workflowForm, ex); bindingResult.rejectValue("url", "git.sshError"); return new ModelAndView("index"); } catch (GitAPIException ex) { logger.error("git.retrievalError " + workflowForm, ex); bindingResult.rejectValue("url", "git.retrievalError"); return new ModelAndView("index"); } catch (WorkflowNotFoundException ex) { logger.warn("git.notFound " + workflowForm, ex); bindingResult.rejectValue("url", "git.notFound"); return new ModelAndView("index"); } catch (Exception ex) { logger.warn("url.parsingError " + workflowForm, ex); bindingResult.rejectValue("url", "url.parsingError"); return new ModelAndView("index"); } } gitInfo = workflow.getRetrievedFrom(); // Redirect to the workflow return new ModelAndView("redirect:" + gitInfo.getInternalUrl()); } } /** * Display a page for a particular workflow from Github or Github format URL * @param domain The domain of the hosting site, Github or Gitlab * @param owner The owner of the repository * @param repoName The name of the repository * @param branch The branch of repository * @return The workflow view with the workflow as a model */ @GetMapping(value = { "/workflows/{domain}.com/{owner}/{repoName}/tree/{branch}/**", "/workflows/{domain}.com/{owner}/{repoName}/blob/{branch}/**" }) public ModelAndView getWorkflow(@PathVariable("domain") String domain, @PathVariable("owner") String owner, @PathVariable("repoName") String repoName, @PathVariable("branch") String branch, HttpServletRequest request, RedirectAttributes redirectAttrs) { // The wildcard end of the URL is the path String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); path = extractPath(path, 7); // Construct a GitDetails object to search for in the database GitDetails gitDetails = getGitDetails(domain, owner, repoName, branch, path); // Get workflow return getWorkflow(gitDetails, redirectAttrs); } /** * Display page for a workflow from a generic Git URL * @param branch The branch of the repository * @return The workflow view with the workflow as a model */ @GetMapping(value = "/workflows/**/*.git/{branch}/**") public ModelAndView getWorkflowGeneric(@Value("${applicationURL}") String applicationURL, @PathVariable("branch") String branch, HttpServletRequest request, RedirectAttributes redirectAttrs) { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); GitDetails gitDetails = getGitDetails(11, path, branch); return getWorkflow(gitDetails, redirectAttrs); } /** * Download the Research Object Bundle for a particular workflow * @param domain The domain of the hosting site, Github or Gitlab * @param owner The owner of the repository * @param repoName The name of the repository * @param branch The branch of repository */ @GetMapping(value = { "/robundle/{domain}.com/{owner}/{repoName}/tree/{branch}/**", "/robundle/{domain}.com/{owner}/{repoName}/blob/{branch}/**" }, produces = { "application/vnd.wf4ever.robundle+zip", "application/zip" }) @ResponseBody public FileSystemResource getROBundle(@PathVariable("domain") String domain, @PathVariable("owner") String owner, @PathVariable("repoName") String repoName, @PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); path = extractPath(path, 7); GitDetails gitDetails = getGitDetails(domain, owner, repoName, branch, path); File bundleDownload = workflowService.getROBundle(gitDetails); response.setHeader("Content-Disposition", "attachment; filename=bundle.zip;"); return new FileSystemResource(bundleDownload); } /** * Download the Research Object Bundle for a particular workflow * @param branch The branch of repository */ @GetMapping(value = "/robundle/**/*.git/{branch}/**", produces = "application/vnd.wf4ever.robundle+zip") @ResponseBody public FileSystemResource getROBundleGeneric(@PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); GitDetails gitDetails = getGitDetails(10, path, branch); File bundleDownload = workflowService.getROBundle(gitDetails); response.setHeader("Content-Disposition", "attachment; filename=bundle.zip;"); return new FileSystemResource(bundleDownload); } /** * Download a generated graph for a workflow in SVG format * @param domain The domain of the hosting site, Github or Gitlab * @param owner The owner of the repository * @param repoName The name of the repository * @param branch The branch of repository */ @GetMapping(value = { "/graph/svg/{domain}.com/{owner}/{repoName}/tree/{branch}/**", "/graph/svg/{domain}.com/{owner}/{repoName}/blob/{branch}/**" }, produces = "image/svg+xml") @ResponseBody public FileSystemResource downloadGraphSvg(@PathVariable("domain") String domain, @PathVariable("owner") String owner, @PathVariable("repoName") String repoName, @PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); path = extractPath(path, 8); GitDetails gitDetails = getGitDetails(domain, owner, repoName, branch, path); response.setHeader("Content-Disposition", "inline; filename=\"graph.svg\""); return workflowService.getWorkflowGraph("svg", gitDetails); } /** * Download a generated graph for a workflow in SVG format * @param branch The branch of repository */ @GetMapping(value = "/graph/svg/**/*.git/{branch}/**", produces = "image/svg+xml") @ResponseBody public FileSystemResource downloadGraphSvgGeneric(@PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); GitDetails gitDetails = getGitDetails(11, path, branch); response.setHeader("Content-Disposition", "inline; filename=\"graph.svg\""); return workflowService.getWorkflowGraph("svg", gitDetails); } /** * Download a generated graph for a workflow in PNG format * @param domain The domain of the hosting site, Github or Gitlab * @param owner The owner of the repository * @param repoName The name of the repository * @param branch The branch of repository */ @GetMapping(value = { "/graph/png/{domain}.com/{owner}/{repoName}/tree/{branch}/**", "/graph/png/{domain}.com/{owner}/{repoName}/blob/{branch}/**" }, produces = "image/png") @ResponseBody public FileSystemResource downloadGraphPng(@PathVariable("domain") String domain, @PathVariable("owner") String owner, @PathVariable("repoName") String repoName, @PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); path = extractPath(path, 8); GitDetails gitDetails = getGitDetails(domain, owner, repoName, branch, path); response.setHeader("Content-Disposition", "inline; filename=\"graph.png\""); return workflowService.getWorkflowGraph("png", gitDetails); } /** * Download a generated graph for a workflow in PNG format * @param branch The branch of repository */ @GetMapping(value = "/graph/png/**/*.git/{branch}/**", produces = "image/png") @ResponseBody public FileSystemResource downloadGraphPngGeneric(@PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); GitDetails gitDetails = getGitDetails(11, path, branch); response.setHeader("Content-Disposition", "inline; filename=\"graph.png\""); return workflowService.getWorkflowGraph("png", gitDetails); } /** * Download a generated graph for a workflow in XDOT format * @param domain The domain of the hosting site, Github or Gitlab * @param owner The owner of the repository * @param repoName The name of the repository * @param branch The branch of repository */ @GetMapping(value = { "/graph/xdot/{domain}.com/{owner}/{repoName}/tree/{branch}/**", "/graph/xdot/{domain}.com/{owner}/{repoName}/blob/{branch}/**" }, produces = "text/vnd.graphviz") @ResponseBody public FileSystemResource downloadGraphDot(@PathVariable("domain") String domain, @PathVariable("owner") String owner, @PathVariable("repoName") String repoName, @PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); path = extractPath(path, 8); GitDetails gitDetails = getGitDetails(domain, owner, repoName, branch, path); response.setHeader("Content-Disposition", "inline; filename=\"graph.dot\""); return workflowService.getWorkflowGraph("xdot", gitDetails); } /** * Download a generated graph for a workflow in XDOT format * @param branch The branch of repository */ @GetMapping(value = "/graph/xdot/**/*.git/{branch}/**", produces = "text/vnd.graphviz") @ResponseBody public FileSystemResource downloadGraphDotGeneric(@PathVariable("branch") String branch, HttpServletRequest request, HttpServletResponse response) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); GitDetails gitDetails = getGitDetails(12, path, branch); response.setHeader("Content-Disposition", "inline; filename=\"graph.dot\""); return workflowService.getWorkflowGraph("xdot", gitDetails); } /** * Get a temporary graph for a pending workflow * @param queueID The ID in the queue * @return The visualisation image */ @GetMapping(value = { "/queue/{queueID}/tempgraph.png" }, produces = "image/png") @ResponseBody public FileSystemResource getTempGraphAsPng(@PathVariable("queueID") String queueID, HttpServletResponse response) throws IOException { QueuedWorkflow queued = workflowService.getQueuedWorkflow(queueID); if (queued == null) { throw new WorkflowNotFoundException(); } File out = graphVizService.getGraph(queued.getId() + ".png", queued.getTempRepresentation().getVisualisationDot(), "png"); response.setHeader("Content-Disposition", "inline; filename=\"graph.png\""); return new FileSystemResource(out); } /** * Extract the path from the end of a full request string * @param path The full request string path * @param startSlashNum The ordinal slash index of the start of the path * @return The path from the end */ public static String extractPath(String path, int startSlashNum) { int pathStartIndex = StringUtils.ordinalIndexOf(path, "/", startSlashNum); if (pathStartIndex > -1 && pathStartIndex < path.length() - 1) { return path.substring(pathStartIndex + 1).replaceAll("\\/$", ""); } else { return "/"; } } /** * Constructs a GitDetails object for Github/Gitlab details * @param domain The domain name (always .com) * @param owner The owner of the repository * @param repoName The name of the repository * @param branch The branch of the repository * @param path The path within the repository * @return A constructed GitDetails object */ public static GitDetails getGitDetails(String domain, String owner, String repoName, String branch, String path) { String repoUrl; switch (domain) { case "github": repoUrl = "https://github.com/" + owner + "/" + repoName + ".git"; break; case "gitlab": repoUrl = "https://gitlab.com/" + owner + "/" + repoName + ".git"; break; default: throw new WorkflowNotFoundException(); } String[] pathSplit = path.split("#"); GitDetails details = new GitDetails(repoUrl, branch, path); if (pathSplit.length > 1) { details.setPath(pathSplit[pathSplit.length - 2]); details.setPackedId(pathSplit[pathSplit.length - 1]); } return details; } /** * Constructs a GitDetails object for a generic path * @param startIndex The start of the repository URL * @param path The entire URL path * @param branch The branch of the repository * @return A constructed GitDetails object */ public static GitDetails getGitDetails(int startIndex, String path, String branch) { // The repository URL is the part after startIndex and up to and including .git String repoUrl = path.substring(startIndex); int extensionIndex = repoUrl.indexOf(".git"); if (extensionIndex == -1) { throw new WorkflowNotFoundException(); } repoUrl = "https://" + repoUrl.substring(0, extensionIndex + 4); // The path is after the branch int slashAfterBranch = path.indexOf("/", path.indexOf(branch)); if (slashAfterBranch == -1 || slashAfterBranch == path.length()) { throw new WorkflowNotFoundException(); } path = path.substring(slashAfterBranch + 1); // Construct GitDetails object for this workflow return new GitDetails(repoUrl, branch, path); } /** * Get a workflow from Git Details, creating if it does not exist * @param gitDetails The details of the Git repository * @param redirectAttrs Error attributes for redirect * @return The model and view to be returned by the controller */ private ModelAndView getWorkflow(GitDetails gitDetails, RedirectAttributes redirectAttrs) { // Get workflow QueuedWorkflow queued = null; Workflow workflowModel = workflowService.getWorkflow(gitDetails); if (workflowModel == null) { // Check if already queued queued = workflowService.getQueuedWorkflow(gitDetails); if (queued == null) { // Validation String packedPart = (gitDetails.getPackedId() == null) ? "" : "#" + gitDetails.getPackedId(); WorkflowForm workflowForm = new WorkflowForm(gitDetails.getRepoUrl(), gitDetails.getBranch(), gitDetails.getPath() + packedPart); BeanPropertyBindingResult errors = new BeanPropertyBindingResult(workflowForm, "errors"); workflowFormValidator.validateAndParse(workflowForm, errors); if (!errors.hasErrors()) { try { if (gitDetails.getPath().endsWith(".cwl")) { queued = workflowService.createQueuedWorkflow(gitDetails); if (queued.getWorkflowList() != null) { // Packed workflow listing if (queued.getWorkflowList().size() == 1) { gitDetails.setPackedId(queued.getWorkflowList().get(0).getFileName()); return new ModelAndView("redirect:" + gitDetails.getInternalUrl()); } return new ModelAndView("selectworkflow", "workflowOverviews", queued.getWorkflowList()).addObject("gitDetails", gitDetails); } } else { List<WorkflowOverview> workflowOverviews = workflowService .getWorkflowsFromDirectory(gitDetails); if (workflowOverviews.size() > 1) { return new ModelAndView("selectworkflow", "workflowOverviews", workflowOverviews) .addObject("gitDetails", gitDetails); } else if (workflowOverviews.size() == 1) { return new ModelAndView("redirect:" + gitDetails.getInternalUrl() + workflowOverviews.get(0).getFileName()); } else { errors.rejectValue("url", "url.noWorkflowsInDirectory", "No workflow files were found in the given directory"); } } } catch (TransportException ex) { logger.warn("git.sshError " + workflowForm, ex); errors.rejectValue("url", "git.sshError", "SSH URLs are not supported, please provide a HTTPS URL for the repository or submodules"); } catch (GitAPIException ex) { logger.error("git.retrievalError " + workflowForm, ex); errors.rejectValue("url", "git.retrievalError", "The workflow could not be retrieved from the Git repository using the details given"); } catch (WorkflowNotFoundException ex) { logger.warn("git.notFound " + workflowForm, ex); errors.rejectValue("url", "git.notFound", "The workflow could not be found within the repository"); } catch (IOException ex) { logger.warn("url.parsingError " + workflowForm, ex); errors.rejectValue("url", "url.parsingError", "The workflow could not be parsed from the given URL"); } } // Redirect to main page with errors if they occurred if (errors.hasErrors()) { redirectAttrs.addFlashAttribute("errors", errors); return new ModelAndView("redirect:/?url=" + gitDetails.getUrl()); } } } // Display this model along with the view if (queued != null) { // Retry creation if there has been an error in cwltool parsing if (queued.getCwltoolStatus() == CWLToolStatus.ERROR) { workflowService.retryCwltool(queued); } return new ModelAndView("loading", "queued", queued); } else { return new ModelAndView("workflow", "workflow", workflowModel).addObject("formats", WebConfig.Format.values()); } } }