/**
* 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.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.n52.wps.server.ExceptionReport;
import org.n52.wps.server.r.RWPSSessionVariables;
import org.n52.wps.server.r.syntax.RAnnotationException;
import org.n52.wps.server.r.syntax.RegExp;
import org.rosuda.REngine.REXPMismatchException;
import org.rosuda.REngine.Rserve.RConnection;
import org.rosuda.REngine.Rserve.RserveException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RExecutor {
private static final String COMMENT_CHARACTER = "#";
private static Logger log = LoggerFactory.getLogger(RExecutor.class);
private boolean debugScript = true; // TODO make configurable property
private boolean appendSwitchedOffCommandsAsComments = false; // TODO make configurable property
private boolean appendComments = false; // TODO make configurable property
private InputStream openScriptStream(File rScriptFile) throws ExceptionReport, RserveException {
InputStream rScriptStream = null;
try {
rScriptStream = new FileInputStream(rScriptFile);
}
catch (IOException e) {
log.error("Error reading script file.", e);
throw new ExceptionReport("Could not read script file " + rScriptFile,
ExceptionReport.NO_APPLICABLE_CODE,
e);
}
return rScriptStream;
}
/**
*
* @param script
* R input script
* @param rCon
* Connection - should be open usually / otherwise it will be opened and closed separately
* @return true if read was successful
*/
public boolean executeScript(File script, RConnection rCon) throws RserveException,
IOException,
RAnnotationException,
ExceptionReport {
log.debug("Executing script...");
InputStream rScriptStream = openScriptStream(script);
boolean success = true;
BufferedReader fr = new BufferedReader(new InputStreamReader(rScriptStream));
if ( !fr.ready())
return false;
// reading script:
StringBuilder scriptExecutionString = new StringBuilder();
// surrounds R script with try / catch block in R
scriptExecutionString.append("error = try({ \n");
// wrapper to retrieve warnings (workaround, because warnings() reliable
// for Rserve and often returns NULL)
scriptExecutionString.append("withCallingHandlers({\n\n");
// is set true when wps.off-annotations occur
// this indicates that parts of the script shall not pass to Rserve
boolean wpsoff_state = false;
while (fr.ready()) {
String line = fr.readLine();
if (line.isEmpty())
continue;
if (line.contains(RegExp.WPS_OFF) && line.contains(RegExp.WPS_ON))
throw new RAnnotationException("Invalid R-script: Only one wps.on; / wps.off; expression per line!");
if (line.contains(RegExp.WPS_OFF))
wpsoff_state = true;
else if (line.contains(RegExp.WPS_ON))
wpsoff_state = false;
else if (wpsoff_state) {
if (appendSwitchedOffCommandsAsComments)
line = "# (ignored by " + RegExp.WPS_OFF + ") " + line;
}
else {
// not switched off:
if (line.trim().startsWith(COMMENT_CHARACTER) && line.contains("updateStatus")) {
//remove comment in front of updateStatus call for execution
line = line.replaceFirst("#", "").trim();
scriptExecutionString.append(line);
scriptExecutionString.append("\n");
}else if (line.trim().startsWith(COMMENT_CHARACTER)) {
if (appendComments)
scriptExecutionString.append(line);
}
else {
// actually append the line
if (line.contains("setwd("))
log.warn("The running R script contains a call to \"setwd(...)\". "
+ "This may cause runtime-errors and unexpected behaviour of WPS4R. "
+ "It is strongly advised to not use this function in process scripts.");
scriptExecutionString.append(line);
scriptExecutionString.append("\n");
}
}
}
// apply handler to retrieve warnings:
scriptExecutionString.append("\n}, warning = function(w) {\n ");
scriptExecutionString.append(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
scriptExecutionString.append(" = get(\"");
scriptExecutionString.append(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
scriptExecutionString.append("\", envir = .GlobalEnv);");
scriptExecutionString.append("\n ");
scriptExecutionString.append(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
scriptExecutionString.append(" = append(");
scriptExecutionString.append(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
scriptExecutionString.append(", w$message);");
scriptExecutionString.append("\n");
scriptExecutionString.append(" assign(\"");
scriptExecutionString.append(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
scriptExecutionString.append("\", ");
scriptExecutionString.append(RWPSSessionVariables.WARNING_OUTPUT_STORAGE);
scriptExecutionString.append(", envir = .GlobalEnv);");
scriptExecutionString.append("\n");
scriptExecutionString.append("})\n"); // for "withCallingHandlers"
scriptExecutionString.append("});\n\n"); // for "try{..."
scriptExecutionString.append("hasError <- class(error) == \"try-error\" ");
scriptExecutionString.append("\n");
scriptExecutionString.append("if(hasError) ");
scriptExecutionString.append(RWPSSessionVariables.ERROR_MESSAGE);
scriptExecutionString.append(" <- as.character(error)");
scriptExecutionString.append("\n");
if (this.debugScript && log.isDebugEnabled())
log.debug(scriptExecutionString.toString());
// call the actual script here
rCon.eval(scriptExecutionString.toString());
try {
// handling internal R errors:
if (rCon.eval("hasError").asInteger() == 1) {
String message = "An R error occured while executing R script: \n"
+ rCon.eval(RWPSSessionVariables.ERROR_MESSAGE).asString();
log.error(message);
success = false;
throw new ExceptionReport(message, ExceptionReport.REMOTE_COMPUTATION_ERROR);
}
}
catch (REXPMismatchException e) {
log.error("Error handling during R script execution failed.", e);
success = false;
}
finally {
if (rScriptStream != null) {
try {
rScriptStream.close();
}
catch (IOException e) {
log.error("Connection to R script cannot be closed for process file {}", script);
}
}
}
return success;
}
}