/*
* This is eMonocot, a global online biodiversity information resource.
*
* Copyright © 2011–2015 The Board of Trustees of the Royal Botanic Gardens, Kew and The University of Oxford
*
* eMonocot is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* eMonocot is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* The complete text of the GNU Affero General Public License is in the source repository as the file
* ‘COPYING’. It is also available from <http://www.gnu.org/licenses/>.
*/
package org.emonocot.portal.controller;
import java.io.ByteArrayInputStream;
import java.security.Principal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.emonocot.api.PhylogeneticTreeService;
import org.emonocot.api.ResourceService;
import org.emonocot.api.SearchableObjectService;
import org.emonocot.api.UserService;
import org.emonocot.api.job.DarwinCorePropertyMap;
import org.emonocot.api.job.JobExecutionException;
import org.emonocot.api.job.JobExecutionInfo;
import org.emonocot.model.PhylogeneticTree;
import org.emonocot.model.SearchableObject;
import org.emonocot.model.auth.Permission;
import org.emonocot.model.auth.User;
import org.emonocot.model.constants.ResourceType;
import org.emonocot.model.registry.Resource;
import org.emonocot.pager.Page;
import org.emonocot.portal.format.annotation.FacetRequestFormat;
import org.emonocot.service.DownloadService;
import org.forester.io.parsers.PhylogenyParser;
import org.forester.io.parsers.phyloxml.PhyloXmlParser;
import org.forester.io.writers.PhylogenyWriter;
import org.forester.io.writers.PhylogenyWriter.FORMAT;
import org.forester.phylogeny.Phylogeny;
import org.forester.phylogeny.PhylogenyNode.NH_CONVERSION_SUPPORT_VALUE_STYLE;
import org.gbif.dwc.terms.DwcTerm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
*
* @author ben
*
*/
@Controller
@RequestMapping(value = "/download")
public class DownloadController {
private static Logger queryLog = LoggerFactory.getLogger("query");
private static Logger logger = LoggerFactory.getLogger(DownloadController.class);
private SearchableObjectService searchableObjectService;
private ResourceService resourceService;
private UserService userService;
private JobExplorer jobExplorer;
private MessageSource messageSource;
private DownloadService downloadService;
private PhylogeneticTreeService phylogeneticTreeService;
@Autowired
public void setSearchableObjectService(SearchableObjectService searchableObjectService) {
this.searchableObjectService = searchableObjectService;
}
@Autowired
public void setResourceService(ResourceService resourceService) {
this.resourceService = resourceService;
}
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
@Autowired
public void setJobExplorer(JobExplorer jobExplorer) {
this.jobExplorer = jobExplorer;
}
@Autowired
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Autowired
public void setDownloadService(DownloadService downloadService) {
this.downloadService = downloadService;
}
@Autowired
public void setPhylogeneticTreeService(PhylogeneticTreeService phylogeneticTreeService) {
this.phylogeneticTreeService = phylogeneticTreeService;
}
/**
*
* @param query
* Set the query
* @param limit
* Limit the number of returned results
* @param start
* Set the offset
* @param facets
* The facets to set
* @param view Set the view
* @param model Set the model
*
* @return a model and view
*/
@RequestMapping(method = RequestMethod.GET)
public String search(
@RequestParam(value = "query", required = false) String query,
@RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
@RequestParam(value = "sort", required = false) String sort,
@RequestParam(value = "x1", required = false) Double x1,
@RequestParam(value = "y1", required = false) Double y1,
@RequestParam(value = "x2", required = false) Double x2,
@RequestParam(value = "y2", required = false) Double y2,
Principal principal,
Model model) throws SolrServerException {
User requestingUser = userService.load(principal.getName());
Map<String, String> selectedFacets = null;
if (facets != null && !facets.isEmpty()) {
selectedFacets = new HashMap<String, String>();
for (FacetRequest facetRequest : facets) {
selectedFacets.put(facetRequest.getFacet(),
facetRequest.getSelected());
}
}
String spatial = null;
DecimalFormat decimalFormat = new DecimalFormat("###0.0");
if (x1 != null && y1 != null && x2 != null && y2 != null && (x1 != 0.0 && y1 != 0.0 && x2 != 0.0 && x2 != 0.0 && y2 != 0.0)) {
model.addAttribute("x1",x1);
model.addAttribute("y1",y1);
model.addAttribute("x2",x2);
model.addAttribute("y2",y2);
spatial = "{!join to=taxon.distribution_ss from=location.tdwg_code_s}geo:\"Intersects(" + decimalFormat.format(x1) + " " + decimalFormat.format(y1) + " " + decimalFormat.format(x2) + " " + decimalFormat.format(y2) + ")\"";
}
//Run the search
Page<? extends SearchableObject> result = searchableObjectService.search(query, spatial, 10, 0, null, null, selectedFacets, sort, null);
result.setSort(sort);
result.putParam("query", query);
model.addAttribute("taxonTerms", DarwinCorePropertyMap.getConceptTerms(DwcTerm.Taxon));
model.addAttribute("taxonMap", DarwinCorePropertyMap.getPropertyMap(DwcTerm.Taxon));
model.addAttribute("result", result);
if(result.getSize() > downloadService.getDownloadLimit() && !requestingUser.getAuthorities().contains(Permission.PERMISSION_ADMINISTRATE)) {
String[] codes = new String[] { "download.truncated" };
Object[] args = new Object[] { result.getSize(), downloadService.getDownloadLimit() };
DefaultMessageSourceResolvable message = new DefaultMessageSourceResolvable(
codes, args);
model.addAttribute("warn",message);
}
return "download/download";
}
@RequestMapping(method = RequestMethod.POST, produces = "text/html", params = "download")
public String download(
@RequestParam(value = "query", required = false) String query,
@RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
@RequestParam(value = "sort", required = false) String sort,
@RequestParam(value = "x1", required = false) Double x1,
@RequestParam(value = "y1", required = false) Double y1,
@RequestParam(value = "x2", required = false) Double x2,
@RequestParam(value = "y2", required = false) Double y2,
@RequestParam(value = "purpose", required = false) String purpose,
Model model,
HttpServletRequest request,
@RequestParam(value="downloadFormat", required = true) String downloadFormat,
@RequestParam(value = "archiveOptions", required = false) List<String> archiveOptions,
RedirectAttributes redirectAttributes,
Principal principal) throws SolrServerException {
User user = userService.load(principal.getName());
Map<String, String> selectedFacets = null;
if (facets != null && !facets.isEmpty()) {
selectedFacets = new HashMap<String, String>();
for (FacetRequest facetRequest : facets) {
selectedFacets.put(facetRequest.getFacet(),
facetRequest.getSelected());
}
}
String spatial = null;
DecimalFormat decimalFormat = new DecimalFormat("###0.0");
if (x1 != null && y1 != null && x2 != null && y2 != null && (x1 != 0.0 && y1 != 0.0 && x2 != 0.0 && x2 != 0.0 && y2 != 0.0)) {
spatial = "{!join to=taxon.distribution_ss from=location.tdwg_code_s}geo:\"Intersects(" + decimalFormat.format(x1) + " " + decimalFormat.format(y1) + " " + decimalFormat.format(x2) + " " + decimalFormat.format(y2) + ")\"";
}
if(archiveOptions == null) {
archiveOptions = new ArrayList<String>();
}
Page<? extends SearchableObject> result = searchableObjectService.search(query, spatial, 10, 0, null, null, selectedFacets, sort, null);
Resource resource = new Resource();
resource.setTitle("download" + Long.toString(System.currentTimeMillis()));
//Save the 'resource'
try {
StringBuffer downloadFileName = new StringBuffer(UUID.randomUUID().toString()); // Download file - either the file or the directory
resource.setResourceType(ResourceType.DOWNLOAD);
resource.setStartTime(null);
resource.setDuration(null);
resource.setExitCode(null);
resource.setExitDescription(null);
resource.setJobId(null);
resource.setStatus(BatchStatus.UNKNOWN);
resource.setRecordsRead(0);
resource.setReadSkip(0);
resource.setProcessSkip(0);
resource.setWriteSkip(0);
resource.setWritten(0);
switch (downloadFormat) {
case "taxon":
downloadFileName.append(".txt");
break;
case "alphabeticalChecklist":
case "hierarchicalChecklist":
downloadFileName.append(".pdf");
break;
default://archive
downloadFileName.append(".zip");
break;
}
resource.getParameters().put("download.file", downloadFileName.toString());
resourceService.save(resource);
//Launch the job
downloadService.requestDownload(query, selectedFacets, sort, spatial, result.getSize(),
purpose, downloadFormat, archiveOptions, resource, user);
String[] codes = new String[] { "download.submitted" };
Object[] args = new Object[] {};
DefaultMessageSourceResolvable message = new DefaultMessageSourceResolvable(
codes, args);
redirectAttributes.addFlashAttribute("info", message);
queryLog.info("Download: \'{}\', format: {}, purpose: {},"
+ "facet: [{}], {} results", new Object[] {query,
downloadFormat, purpose, selectedFacets, result.getSize() });
} catch (JobExecutionException e) {
String[] codes = new String[] { "download.failed" };
Object[] args = new Object[] { e.getMessage() };
DefaultMessageSourceResolvable message = new DefaultMessageSourceResolvable(
codes, args);
redirectAttributes.addFlashAttribute("error", message);
}
return "redirect:/download/progress?downloadId=" + resource.getId();
}
@RequestMapping(value = "/progress", method = RequestMethod.GET, produces = "text/html")
public String getProgress(@RequestParam("downloadId") Long downloadId, Model model, Locale locale) {
Resource resource = resourceService.load(downloadId);
model.addAttribute("resource", resource);
String downloadFileName = resource.getParameters().get("download.file");
if(resource.getBaseUrl() == null) {
resource.setExitDescription(messageSource.getMessage("download.being.prepared", new Object[] {}, locale));
} else {
resource.setExitDescription(messageSource.getMessage("download.will.be.available", new Object[] {messageSource.getMessage("portal.baseUrl", new Object[] {}, locale), downloadFileName}, locale));
}
return "download/progress";
}
@RequestMapping(value = "/progress", method = RequestMethod.GET, produces="application/json")
@ResponseBody
public JobExecutionInfo getProgress(@RequestParam("downloadId") Long downloadId, Locale locale) throws Exception {
JobExecutionInfo jobExecutionInfo = new JobExecutionInfo();
Resource resource = resourceService.load(downloadId);
JobExecution jobExecution = jobExplorer.getJobExecution(resource.getJobId());
if(jobExecution != null) {
Float recordsRead = 0f;
jobExecutionInfo.setStatus(jobExecution.getStatus());
if(jobExecution.getExitStatus() != null) {
ExitStatus exitStatus = jobExecution.getExitStatus();
jobExecutionInfo.setExitCode(exitStatus.getExitCode());
jobExecutionInfo.setExitDescription(exitStatus.getExitDescription());
Integer readSkip = 0;
Integer processSkip = 0;
Integer writeSkip = 0;
Integer written = 0;
for(StepExecution stepExecution : jobExecution.getStepExecutions()) {
recordsRead += stepExecution.getReadCount();
readSkip += stepExecution.getReadSkipCount();
processSkip += stepExecution.getProcessSkipCount();
writeSkip += stepExecution.getWriteSkipCount();
written += stepExecution.getWriteCount();
}
jobExecutionInfo.setRecordsRead(recordsRead.intValue());
jobExecutionInfo.setReadSkip(readSkip);
jobExecutionInfo.setProcessSkip(processSkip);
jobExecutionInfo.setWriteSkip(writeSkip);
jobExecutionInfo.setWritten(written);
}
Float total = Float.parseFloat(jobExecution.getJobInstance().getJobParameters().getString("job.total.reads"));
jobExecutionInfo.setProgress(Math.round((recordsRead/ total) * 100f));
if(resource.getBaseUrl() == null) {
jobExecutionInfo.setExitDescription(messageSource.getMessage("download.being.prepared", new Object[] {}, locale));
} else {
String downloadFileName = resource.getParameters().get("download.file");
if(jobExecutionInfo.getExitCode() == null) {
jobExecutionInfo.setExitDescription(messageSource.getMessage("download.will.be.available", new Object[] {resource.getBaseUrl(), downloadFileName}, locale));
} else {
if(jobExecutionInfo.getExitCode().equals("COMPLETED")) {
jobExecutionInfo.setExitDescription(messageSource.getMessage("download.is.available", new Object[] {messageSource.getMessage("portal.baseUrl", new Object[] {}, locale), downloadFileName}, locale));
} else if(jobExecutionInfo.getExitCode().equals("FAILED")) {
jobExecutionInfo.setExitDescription(messageSource.getMessage("download.failed", new Object[] {}, locale));
} else {
jobExecutionInfo.setExitDescription(messageSource.getMessage("download.will.be.available", new Object[] {messageSource.getMessage("portal.baseUrl", new Object[] {}, locale), downloadFileName}, locale));
}
}
}
}
return jobExecutionInfo;
}
@RequestMapping(value = "/phylo", params = {"!format"}, method = RequestMethod.GET, produces = {"text/html", "*/*"})
public String getDownloadPage(@RequestParam(value = "id", required = true) Long id, Model model) {
PhylogeneticTree tree = phylogeneticTreeService.load(id);
model.addAttribute(tree);
model.addAttribute("formats", Arrays.asList(FORMAT.values()));
return "phylo/download";
}
@RequestMapping(value = "/phylo", params = {"format!=PHYLO_XML"}, method = RequestMethod.POST, produces = "text/plain")
public @ResponseBody String getDownload(@RequestParam(value = "id", required = true) Long id,
@RequestParam(value = "format", required = false, defaultValue = "NH") FORMAT format,
@RequestParam(value = "purpose", required = false) String purpose,
Model model) throws Exception {
PhylogeneticTree tree = phylogeneticTreeService.load(id);
String output = null;
PhylogenyParser parser = new PhyloXmlParser();
parser.setSource(new ByteArrayInputStream(tree.getPhylogeny().getBytes()));
Phylogeny phylogeny = parser.parse()[0];
PhylogenyWriter phylogenyWriter = PhylogenyWriter.createPhylogenyWriter();
StringBuffer stringBuffer = null;
switch (format) {
case NEXUS:
stringBuffer = phylogenyWriter.toNexus(phylogeny, NH_CONVERSION_SUPPORT_VALUE_STYLE.IN_SQUARE_BRACKETS);
output = stringBuffer.toString();
break;
case NHX:
stringBuffer = phylogenyWriter.toNewHampshireX(phylogeny);
output = stringBuffer.toString();
break;
case NH:
default:
stringBuffer = phylogenyWriter.toNewHampshire(phylogeny, false, tree.getHasBranchLengths());
output = stringBuffer.toString();
break;
}
queryLog.info("DownloadPhylogeny: \'{}\', format: {}, purpose: {}", new Object[] {id, format, purpose});
return output;
}
@RequestMapping(value = "/phylo", params = {"format=PHYLO_XML"}, method = RequestMethod.POST, produces = "application/xml")
public @ResponseBody String getDownloadPhyloXml(@RequestParam(value = "id", required = true) Long id,
@RequestParam(value = "format", required = false, defaultValue = "NH") FORMAT format,
@RequestParam(value = "purpose", required = false) String purpose,
Model model) throws Exception {
PhylogeneticTree tree = phylogeneticTreeService.load(id);
queryLog.info("DownloadPhylogeny: \'{}\', format: {}, purpose: {}", new Object[] {id, format, purpose});
return tree.getPhylogeny();
}
}