/** * Copyright (C) 2010 - 2016 52°North Initiative for Geospatial Open Source * Software GmbH * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * If the program is linked with libraries which are licensed under one of * the following licenses, the combination of the program with the linked * library is not considered a "derivative work" of the program: * * • Apache License, version 2.0 * • Apache Software License, version 1.0 * • GNU Lesser General Public License, version 3 * • Mozilla Public License, versions 1.0, 1.1 and 2.0 * • Common Development and Distribution License (CDDL), version 1.0 * * Therefore the distribution of the program linked with libraries licensed * under the aforementioned licenses, is permitted by the copyright holders * if the distribution is compliant with both the GNU General Public * License version 2 and the aforementioned licenses. * * This program 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 General * Public License for more details. */ package org.n52.wps.server.r; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import org.n52.wps.commons.WPSConfig; import org.n52.wps.server.ExceptionReport; import org.n52.wps.server.WebProcessingService; import org.n52.wps.server.r.metadata.RAnnotationParser; import org.n52.wps.server.r.syntax.RAnnotation; import org.n52.wps.server.r.syntax.RAnnotationException; import org.n52.wps.server.r.syntax.RAnnotationType; import org.n52.wps.server.r.syntax.RAttribute; import org.n52.wps.server.r.util.RConnector; import org.n52.wps.server.r.util.RStarter; import org.rosuda.REngine.Rserve.RserveException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class R_Config { private static Logger LOGGER = LoggerFactory.getLogger(R_Config.class); public static final String SCRIPT_FILE_EXTENSION = "R"; public static final String SCRIPT_FILE_SUFFIX = "." + SCRIPT_FILE_EXTENSION; public static final String WKN_PREFIX = "org.n52.wps.server.r."; public static final String LOCK_SUFFIX = "lock"; // TODO for resources to be downloadable the cannot be in WEB-INF, or this // must be handled with a // servlet, which is probably a better solution to keep track of files, see // http://www.jguru.com/faq/view.jsp?EID=10646 private final String R_BASE_DIR = "R"; private final String UTILS_DIR = "utils"; private static final String DEFAULT_RSERVE_HOST = "localhost"; private static final int DEFAULT_RSERVE_PORT = 6311; private static final boolean DEFAULT_ENABLEBATCHSTART = false; private static final String DIR_DELIMITER = ";"; /** R scripts with utility functions to pre-load */ public String utilsDirFull; private HashMap<RWPSConfigVariables, String> configVariables = new HashMap<RWPSConfigVariables, String>(); private static R_Config instance = null; private RConnector connector; private RAnnotationParser annotationParser; /** Maps current R-script files to identifiers **/ private HashMap<File, String> fileToWknMap = new HashMap<File, String>(); /** Maps each identifier to an R script file **/ private HashMap<String, File> wknToFileMap = new HashMap<String, File>(); /** caches conflicts for the wkn-Rscript mapping until resetWknFileMapping is invoked **/ private HashMap<String, ExceptionReport> wknConflicts = new HashMap<String, ExceptionReport>(); private RStarter starter; private R_Config() { this.starter = new RStarter(); this.connector = new RConnector(starter); try { String wpsBasedir = WebProcessingService.BASE_DIR; if (wpsBasedir != null) { File f = new File(wpsBasedir, R_BASE_DIR); String baseDirFull = f.getAbsolutePath(); f = new File(baseDirFull, UTILS_DIR); this.utilsDirFull = f.getAbsolutePath(); } else LOGGER.error("Could not get basedir from WPS!"); } catch (Exception e) { LOGGER.error("Error getting full path of baseDir and configDir.", e); } this.annotationParser = new RAnnotationParser(this); } public static R_Config getInstance() { if (instance == null) instance = new R_Config(); return instance; } public void setConfigVariable(RWPSConfigVariables key, String value) { this.configVariables.put(key, value); } public String getConfigVariable(RWPSConfigVariables key) { return this.configVariables.get(key); } public String getConfigVariableFullPath(RWPSConfigVariables key) throws ExceptionReport { String path = getConfigVariable(key); if (path == null) throw new ExceptionReport("Config variable is not set!", "Inconsistent property"); File testFile = new File(path); if ( !testFile.isAbsolute()) { testFile = new File(WebProcessingService.BASE_DIR, path); } if ( !testFile.exists()) throw new ExceptionReport("Invalid config property of name \"" + key + "\" and value \"" + path + "\". It denotes a non-existent path.", "Inconsistent property"); return testFile.getAbsolutePath(); } public URL getSessionInfoURL() throws MalformedURLException { // FIXME implement service endpoint to retrieve r session information return new URL(WPSConfig.getServerBaseURL() + "/not_supported"); } // FIXME this should use generic WPS methods to get the URL public String getResourceDirURL() { String webapp = WPSConfig.getServerBaseURL(); String resourceDirectory = getResourceDirectory(); // important: this url should be appendable with a resource name, i.e. either end in "/" or "id=" if (webapp != null & resourceDirectory != null) return webapp + "/" + resourceDirectory.replace("\\", "/") + "/"; LOGGER.warn("Cannot create resource dir URL"); return null; } public String getResourceDirectory() { return getConfigVariable(RWPSConfigVariables.RESOURCE_DIR); } public URL getScriptURL(String wkn) throws MalformedURLException, ExceptionReport { String fname = getScriptFileForWKN(wkn).getName(); if (fname == null) return null; Collection<File> scriptDirFullPath = getScriptDir(); // find in which script dir the file is for (File dir : scriptDirFullPath) { File f = new File(dir, fname); if (f.isAbsolute()) { // FIXME can only access scripts that are in the webapp folder LOGGER.debug("Cannot create URL for script file {} at location {} of process {}", fname, dir, wkn); return null; } else { URL url = new URL(WPSConfig.getServerBaseURL() + "/" + f.toString().replace("\\", "/")); return url; } } return null; } public URL getOutputFileURL(String currentWorkdir, String filename) throws IOException { // check if file exists String path = currentWorkdir + "/" + filename; File out = new File(path); if ( ! (out.isFile() && out.canRead())) throw new IOException("Error in creating URL: " + currentWorkdir + " / " + path + " not found or broken."); // create URL path = path.substring(WebProcessingService.BASE_DIR.length() + 1, path.length()); String urlString = WPSConfig.getServerBaseURL() + "/" + path; return new URL(urlString); } protected boolean registerScript(File file) throws RAnnotationException, ExceptionReport { boolean registered = false; FileInputStream fis = null; try { fis = new FileInputStream(file); } catch (FileNotFoundException e) { LOGGER.error("Could not create input stream for file {}", file); } if (fileToWknMap.containsKey(file.getAbsoluteFile())) LOGGER.debug("File already registered, not doint it again: {}", file); else { LOGGER.info("Registering script file {} from input {}", file, fis); List<RAnnotation> annotations = annotationParser.parseAnnotationsfromScript(fis); if (annotations.size() < 1) { LOGGER.warn("Could not parse any annotations from file '{}'. Did not load the script.", file); registered = false; } else { RAnnotation descriptionAnnotation = RAnnotation.filterFirstMatchingAnnotation(annotations, RAnnotationType.DESCRIPTION); if (descriptionAnnotation == null) { LOGGER.error("No description annotation for script '{}' - cannot be registered!", file); registered = false; } else { String process_id = descriptionAnnotation.getStringValue(RAttribute.IDENTIFIER); String wkn = WKN_PREFIX + process_id; if (fileToWknMap.containsValue(wkn)) { File conflictFile = getScriptFileForWKN(wkn); if ( !conflictFile.exists()) { LOGGER.info("Cached mapping for process '{}' with file '{}' replaced by file '{}'", wkn, conflictFile.getName(), file.getName()); } else if ( !file.equals(conflictFile)) { String message = String.format("Conflicting identifier '{}' detected for R scripts '{}' and '{}'", wkn, file.getName(), conflictFile.getName()); ExceptionReport e = new ExceptionReport(message, ExceptionReport.NO_APPLICABLE_CODE); LOGGER.error(message); wknConflicts.put(wkn, e); throw e; } } fileToWknMap.put(file.getAbsoluteFile(), wkn); wknToFileMap.put(wkn, file.getAbsoluteFile()); registered = true; } } } if (fis != null) try { fis.close(); } catch (IOException e) { LOGGER.error("Could not close input stream for file {}", file); } return registered; } public String getWKNForScriptFile(File file) throws RAnnotationException, IOException, ExceptionReport { if ( !file.exists()) throw new FileNotFoundException("File not found: " + file.getName()); return fileToWknMap.get(file); } public File getScriptFileForWKN(String wkn) throws ExceptionReport { // check for existing identifier conflicts if (wknConflicts.containsKey(wkn)) throw wknConflicts.get(wkn); File out = wknToFileMap.get(wkn); if (out != null && out.exists() && out.isFile() && out.canRead()) { return out; } else { String fname = out == null ? "(unknown)" : out.getName(); throw new ExceptionReport("Error in Process: " + wkn + ", File " + fname + " not found or broken.", ExceptionReport.NO_APPLICABLE_CODE); } } public void resetWknFileMapping() { LOGGER.info("Resetting wkn mappings."); this.wknToFileMap.clear(); this.fileToWknMap.clear(); this.wknConflicts.clear(); } public Collection<File> getScriptDirFullPath() { String scriptDirConfigParam = getConfigVariable(RWPSConfigVariables.SCRIPT_DIR); Collection<File> scriptDirectories = new ArrayList<File>(); String[] scriptDirs = scriptDirConfigParam.split(DIR_DELIMITER); for (String s : scriptDirs) { File dir = new File(s); if ( !dir.isAbsolute()) dir = new File(WebProcessingService.BASE_DIR, s); scriptDirectories.add(dir); LOGGER.debug("Found script directory: {}", dir); } return scriptDirectories; } public Collection<File> getScriptDir() { String scriptDirConfigParam = getConfigVariable(RWPSConfigVariables.SCRIPT_DIR); Collection<File> scriptDirectories = new ArrayList<File>(); String[] scriptDirs = scriptDirConfigParam.split(DIR_DELIMITER); for (String s : scriptDirs) { File dir = new File(s); scriptDirectories.add(dir); } return scriptDirectories; } public boolean isScriptAvailable(String identifier) { try { File f = getScriptFileForWKN(identifier); boolean out = f.exists(); return out; } catch (Exception e) { LOGGER.error("Script file unavailable for process id " + identifier, e); return false; } } public boolean isScriptValid(String wkn) { FileInputStream fis = null; try { File file = getScriptFileForWKN(wkn); // RAnnotationParser parser = new RAnnotationParser(); fis = new FileInputStream(file); boolean valid = annotationParser.validateScript(fis, wkn); return valid; } catch (IOException e) { LOGGER.error("Script file unavailable for process " + wkn + ".", e); return false; } catch (Exception e) { LOGGER.error("Validation of process " + wkn + " failed.", e); return false; } finally { if (fis != null) try { fis.close(); } catch (IOException e) { LOGGER.error("Could not flose file input.", e); } } } public void killRserveOnWindows() { try { if (Runtime.getRuntime().exec("taskkill /IM RServe.exe /T /F").waitFor() == 0) ; return; } catch (Exception e1) { e1.printStackTrace(); } } public FilteredRConnection openRConnection() throws RserveException { return this.connector.getNewConnection(this.getEnableBatchStart(), this.getRServeHost(), this.getRServePort(), this.getRServeUser(), this.getRServePassword()); } public String getRServePassword() { return getConfigVariable(RWPSConfigVariables.RSERVE_PASSWORD); } public String getRServeUser() { return getConfigVariable(RWPSConfigVariables.RSERVE_USER); } public int getRServePort() { int port_number = DEFAULT_RSERVE_PORT; String port = getConfigVariable(RWPSConfigVariables.RSERVE_PORT); // try to retrieve config variable if (port != null && !port.equals("")) { try { port_number = Integer.parseInt(port); } catch (NumberFormatException e) { LOGGER.warn("Config variable " + RWPSConfigVariables.RSERVE_PORT + " does not contain a parseble integer. Using default port " + port_number); } } return port_number; } public String getRServeHost() { String host = getConfigVariable(RWPSConfigVariables.RSERVE_HOST); if (host == null || host.equals("")) { host = DEFAULT_RSERVE_HOST; } return host; } public boolean getEnableBatchStart() { boolean isBatch = DEFAULT_ENABLEBATCHSTART; // try to retrieve config variable String batch_c = getConfigVariable(RWPSConfigVariables.ENABLE_BATCH_START); if (batch_c != null && !batch_c.equals("")) { try { isBatch = Boolean.parseBoolean(batch_c); } catch (NumberFormatException e) { LOGGER.warn("Config variable " + RWPSConfigVariables.RSERVE_PORT + " does not contain a parseble boolean. Using default port " + isBatch); } } return isBatch; } public URL getProcessDescriptionURL(String processWKN) { String s = WPSConfig.getServerBaseURL() + "/WebProcessingService?Request=DescribeProcess&identifier=" + processWKN; try { return new URL(s); } catch (MalformedURLException e) { LOGGER.error("Could not create URL for process {}", processWKN, e); return null; } } public boolean getCacheProcesses() { String s = getConfigVariable(RWPSConfigVariables.R_CACHE_PROCESSES); return Boolean.valueOf(s); } }