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