package org.molgenis.dataexplorer.controller;
import com.google.gson.Gson;
import freemarker.core.ParseException;
import org.molgenis.data.*;
import org.molgenis.data.annotation.web.meta.AnnotationJobExecutionMetaData;
import org.molgenis.data.i18n.LanguageService;
import org.molgenis.data.jobs.model.JobExecutionMetaData;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.AttributeFactory;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.support.GenomicDataSettings;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.dataexplorer.download.DataExplorerDownloadHandler;
import org.molgenis.dataexplorer.galaxy.GalaxyDataExportException;
import org.molgenis.dataexplorer.galaxy.GalaxyDataExportRequest;
import org.molgenis.dataexplorer.galaxy.GalaxyDataExporter;
import org.molgenis.dataexplorer.settings.DataExplorerSettings;
import org.molgenis.security.core.MolgenisPermissionService;
import org.molgenis.security.core.Permission;
import org.molgenis.security.core.utils.SecurityUtils;
import org.molgenis.ui.MolgenisPluginController;
import org.molgenis.ui.menumanager.MenuManagerService;
import org.molgenis.util.ErrorMessageResponse;
import org.molgenis.util.ErrorMessageResponse.ErrorMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.*;
import static java.util.stream.Collectors.toList;
import static org.molgenis.data.annotation.web.meta.AnnotationJobExecutionMetaData.ANNOTATION_JOB_EXECUTION;
import static org.molgenis.dataexplorer.controller.DataExplorerController.*;
import static org.molgenis.security.core.Permission.READ;
import static org.molgenis.security.core.Permission.WRITE;
import static org.molgenis.util.EntityUtils.getTypedValue;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/**
* Controller class for the data explorer.
*/
@Controller
@RequestMapping(URI)
@SessionAttributes({ ATTR_GALAXY_URL, ATTR_GALAXY_API_KEY })
public class DataExplorerController extends MolgenisPluginController
{
private static final Logger LOG = LoggerFactory.getLogger(DataExplorerController.class);
public static final String ID = "dataexplorer";
public static final String URI = MolgenisPluginController.PLUGIN_URI_PREFIX + ID;
static final String ATTR_GALAXY_URL = "galaxyUrl";
static final String ATTR_GALAXY_API_KEY = "galaxyApiKey";
public static final String MOD_ANNOTATORS = "annotators";
public static final String MOD_ENTITIESREPORT = "entitiesreport";
public static final String MOD_DATA = "data";
@Autowired
private DataExplorerSettings dataExplorerSettings;
@Autowired
private GenomicDataSettings genomicDataSettings;
@Autowired
private DataService dataService;
@Autowired
private MolgenisPermissionService molgenisPermissionService;
@Autowired
private FreeMarkerConfigurer freemarkerConfigurer;
@Autowired
MenuManagerService menuManager;
@Autowired
private Gson gson;
@Autowired
private LanguageService languageService;
@Autowired
private AttributeFactory attrMetaFactory;
public DataExplorerController()
{
super(URI);
}
/**
* Show the explorer page
*
* @param model
* @return the view name
*/
@RequestMapping(method = RequestMethod.GET)
public String init(@RequestParam(value = "entity", required = false) String selectedEntityName, Model model)
throws Exception
{
boolean entityExists = false;
boolean hasEntityPermission = false;
List<EntityType> entitiesMeta = dataService.getMeta().getEntityTypes()
.filter(entityType -> !entityType.isAbstract()).collect(toList());
model.addAttribute("entitiesMeta", entitiesMeta);
if (selectedEntityName != null)
{
entityExists = dataService.hasRepository(selectedEntityName);
hasEntityPermission = molgenisPermissionService.hasPermissionOnEntity(selectedEntityName, Permission.COUNT);
}
if (!(entityExists && hasEntityPermission))
{
if (selectedEntityName != null)
{
StringBuilder message = new StringBuilder(
"Entity does not exist or you do not have permission on this entity");
if (!SecurityUtils.currentUserIsAuthenticated())
{
message.append(", log in to view more entities");
}
else
{
message.append(", please specify the fully qualified entity name");
}
model.addAttribute("warningMessage", message.toString());
}
}
model.addAttribute("selectedEntityName", selectedEntityName);
model.addAttribute("isAdmin", SecurityUtils.currentUserIsSu());
return "view-dataexplorer";
}
@RequestMapping(value = "/module/{moduleId}", method = GET)
public String getModule(@PathVariable("moduleId") String moduleId, @RequestParam("entity") String entityName,
Model model)
{
if (moduleId.equals(MOD_DATA))
{
model.addAttribute("genomicDataSettings", genomicDataSettings);
model.addAttribute("genomeEntities", getGenomeBrowserEntities());
}
else if (moduleId.equals(MOD_ENTITIESREPORT))
{
model.addAttribute("datasetRepository", dataService.getRepository(entityName));
model.addAttribute("viewName", dataExplorerSettings.getEntityReport(entityName));
}
else if (moduleId.equals(MOD_ANNOTATORS))
{
// throw exception rather than disable the tab, users can act on the message. Hiding the tab is less
// self-explanatory
if (!molgenisPermissionService.hasPermissionOnEntity(entityName, Permission.WRITEMETA))
{
throw new MolgenisDataAccessException(
"No " + Permission.WRITEMETA + " permission on entity [" + entityName
+ "], this permission is necessary run the annotators.");
}
Entity annotationRun = dataService.findOne(ANNOTATION_JOB_EXECUTION,
new QueryImpl<Entity>().eq(AnnotationJobExecutionMetaData.TARGET_NAME, entityName)
.sort(new Sort(JobExecutionMetaData.START_DATE, Sort.Direction.DESC)));
model.addAttribute("annotationRun", annotationRun);
model.addAttribute("entityName", entityName);
}
return "view-dataexplorer-mod-" + moduleId; // TODO bad request in case of invalid module id
}
@RequestMapping(value = "/copy", method = GET)
@ResponseBody
public boolean showCopy(@RequestParam("entity") String entityName)
{
boolean showCopy = molgenisPermissionService.hasPermissionOnEntity(entityName, READ) && dataService
.getCapabilities(entityName).contains(RepositoryCapability.WRITABLE);
return showCopy;
}
/**
* Returns modules configuration for this entity based on current user permissions.
*
* @param entityName
* @return
*/
@RequestMapping(value = "/modules", method = GET)
@ResponseBody
public ModulesConfigResponse getModules(@RequestParam("entity") String entityName)
{
boolean modAggregates = dataExplorerSettings.getModAggregates();
boolean modAnnotators = dataExplorerSettings.getModAnnotators();
boolean modCharts = dataExplorerSettings.getModCharts();
boolean modData = dataExplorerSettings.getModData();
boolean modReports = dataExplorerSettings.getModReports();
if (modAggregates)
{
modAggregates = dataService.getCapabilities(entityName).contains(RepositoryCapability.AGGREGATEABLE);
}
// set data explorer permission
Permission pluginPermission = null;
if (molgenisPermissionService.hasPermissionOnEntity(entityName, WRITE)) pluginPermission = WRITE;
else if (molgenisPermissionService.hasPermissionOnEntity(entityName, READ)) pluginPermission = READ;
else if (molgenisPermissionService.hasPermissionOnEntity(entityName, Permission.COUNT))
pluginPermission = Permission.COUNT;
ModulesConfigResponse modulesConfig = new ModulesConfigResponse();
ResourceBundle i18n = languageService.getBundle();
String aggregatesTitle = i18n.getString("dataexplorer_aggregates_title");
if (pluginPermission != null)
{
switch (pluginPermission)
{
case COUNT:
if (modAggregates)
{
modulesConfig.add(new ModuleConfig("aggregates", aggregatesTitle, "grid-icon.png"));
}
break;
case READ:
case WRITE:
if (modData)
{
modulesConfig.add(new ModuleConfig("data", "Data", "grid-icon.png"));
}
if (modAggregates)
{
modulesConfig.add(new ModuleConfig("aggregates", aggregatesTitle, "aggregate-icon.png"));
}
if (modCharts)
{
modulesConfig.add(new ModuleConfig("charts", "Charts", "chart-icon.png"));
}
if (modAnnotators && pluginPermission == WRITE)
{
modulesConfig.add(new ModuleConfig("annotators", "Annotators", "annotator-icon.png"));
}
if (modReports)
{
String modEntitiesReportName = dataExplorerSettings.getEntityReport(entityName);
if (modEntitiesReportName != null)
{
modulesConfig
.add(new ModuleConfig("entitiesreport", modEntitiesReportName, "report-icon.png"));
}
}
break;
case NONE:
break;
default:
throw new RuntimeException("unknown plugin permission: " + pluginPermission);
}
}
return modulesConfig;
}
/**
* TODO Improve performance by rewriting to query that returns all genomic entities instead of retrieving all entities and determining which one is genomic
* Get readable genome entities
*
* @return
*/
private Map<String, String> getGenomeBrowserEntities()
{
Map<String, String> genomeEntities = new HashMap<>();
dataService.getMeta().getEntityTypes().filter(this::isGenomeBrowserEntity).forEach(entityType ->
{
boolean canRead = molgenisPermissionService.hasPermissionOnEntity(entityType.getName(), READ);
boolean canWrite = molgenisPermissionService.hasPermissionOnEntity(entityType.getName(), WRITE);
if (canRead || canWrite)
{
genomeEntities.put(entityType.getName(), entityType.getLabel());
}
});
return genomeEntities;
}
private boolean isGenomeBrowserEntity(EntityType entityType)
{
Attribute attributeStartPosition = genomicDataSettings
.getAttributeMetadataForAttributeNameArray(GenomicDataSettings.Meta.ATTRS_POS, entityType);
Attribute attributeChromosome = genomicDataSettings
.getAttributeMetadataForAttributeNameArray(GenomicDataSettings.Meta.ATTRS_CHROM, entityType);
return attributeStartPosition != null && attributeChromosome != null;
}
@RequestMapping(value = "/download", method = POST)
public void download(@RequestParam("dataRequest") String dataRequestStr, HttpServletResponse response)
throws IOException
{
DataExplorerDownloadHandler download = new DataExplorerDownloadHandler(dataService, attrMetaFactory);
// Workaround because binding with @RequestBody is not possible:
// http://stackoverflow.com/a/9970672
dataRequestStr = URLDecoder.decode(dataRequestStr, "UTF-8");
LOG.info("Download request: [" + dataRequestStr + "]");
DataRequest dataRequest = gson.fromJson(dataRequestStr, DataRequest.class);
String fileName = "";
ServletOutputStream outputStream = null;
switch (dataRequest.getDownloadType())
{
case DOWNLOAD_TYPE_CSV:
response.setContentType("text/csv");
fileName = dataRequest.getEntityName() + '_' + new SimpleDateFormat("yyyy-MM-dd_hh:mm:ss")
.format(new Date()) + ".csv";
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
outputStream = response.getOutputStream();
download.writeToCsv(dataRequest, outputStream, ',');
break;
case DOWNLOAD_TYPE_XLSX:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
fileName = dataRequest.getEntityName() + '_' + new SimpleDateFormat("yyyy-MM-dd_hh:mm:ss")
.format(new Date()) + ".xlsx";
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
outputStream = response.getOutputStream();
download.writeToExcel(dataRequest, outputStream);
break;
}
}
@RequestMapping(value = "/galaxy/export", method = POST)
@ResponseStatus(HttpStatus.OK)
public void exportToGalaxy(@Valid @RequestBody GalaxyDataExportRequest galaxyDataExportRequest, Model model)
throws IOException
{
boolean galaxyEnabled = dataExplorerSettings.getGalaxyExport();
if (!galaxyEnabled) throw new MolgenisDataAccessException("Galaxy export disabled");
DataExplorerDownloadHandler download = new DataExplorerDownloadHandler(dataService, attrMetaFactory);
String galaxyUrl = galaxyDataExportRequest.getGalaxyUrl();
String galaxyApiKey = galaxyDataExportRequest.getGalaxyApiKey();
GalaxyDataExporter galaxyDataSetExporter = new GalaxyDataExporter(galaxyUrl, galaxyApiKey);
DataRequest dataRequest = galaxyDataExportRequest.getDataRequest();
File csvFile = File.createTempFile("galaxydata_" + System.currentTimeMillis(), ".tsv");
try
{
download.writeToCsv(dataRequest, new FileOutputStream(csvFile), '\t', true);
galaxyDataSetExporter.export(dataRequest.getEntityName(), csvFile);
}
finally
{
csvFile.delete();
}
// store url and api key in session for subsequent galaxy export requests
model.addAttribute(ATTR_GALAXY_URL, galaxyUrl);
model.addAttribute(ATTR_GALAXY_API_KEY, galaxyApiKey);
}
/**
* Builds a model containing one entity and returns the entityReport ftl view
*
* @param entityName
* @param entityId
* @param model
* @return entity report view
* @throws Exception if an entity name or id is not found
* @author mdehaan, fkelpin
*/
@RequestMapping(value = "/details", method = RequestMethod.POST)
public String viewEntityDetails(@RequestParam(value = "entityName") String entityName,
@RequestParam(value = "entityId") String entityId, Model model) throws Exception
{
EntityType entityType = dataService.getEntityType(entityName);
Object id = getTypedValue(entityId, entityType.getIdAttribute());
model.addAttribute("entity", dataService.getRepository(entityName).findOneById(id));
model.addAttribute("entityType", entityType);
model.addAttribute("viewName", getViewName(entityName));
return "view-entityreport";
}
private String getViewName(String entityName)
{
// check if entity report is set for this entity
String reportTemplate = dataExplorerSettings.getEntityReport(entityName);
if (reportTemplate != null)
{
String specificViewname = "view-entityreport-specific-" + reportTemplate;
if (viewExists(specificViewname))
{
return specificViewname;
}
}
// if there are no RuntimeProperty mappings, execute existing behaviour
final String specificViewname = "view-entityreport-specific-" + entityName;
if (viewExists(specificViewname))
{
return specificViewname;
}
if (viewExists("view-entityreport-generic"))
{
return "view-entityreport-generic";
}
return "view-entityreport-generic-default";
}
private boolean viewExists(String viewName)
{
try
{
return freemarkerConfigurer.getConfiguration().getTemplate(viewName + ".ftl") != null;
}
catch (ParseException e)
{
LOG.info("error parsing template: ", e);
return false;
}
catch (IOException e)
{
return false;
}
}
@ExceptionHandler(GalaxyDataExportException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorMessageResponse handleGalaxyDataExportException(GalaxyDataExportException e)
{
LOG.debug("", e);
return new ErrorMessageResponse(Collections.singletonList(new ErrorMessage(e.getMessage())));
}
@ExceptionHandler(RuntimeException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorMessageResponse handleRuntimeException(RuntimeException e)
{
LOG.error(e.getMessage(), e);
return new ErrorMessageResponse(new ErrorMessageResponse.ErrorMessage(
"An error occurred. Please contact the administrator.<br />Message:" + e.getMessage()));
}
}