/**
* 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);
}
}