/**
* 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.workspace;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;
import org.n52.wps.server.ExceptionReport;
import org.n52.wps.server.r.FilteredRConnection;
import org.n52.wps.server.r.RWPSConfigVariables;
import org.n52.wps.server.r.util.RLogger;
import org.rosuda.REngine.REXP;
import org.rosuda.REngine.REXPMismatchException;
import org.rosuda.REngine.Rserve.RConnection;
import org.rosuda.REngine.Rserve.RFileOutputStream;
import org.rosuda.REngine.Rserve.RserveException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RWorkspace {
public enum CreationStrategy {
DEFAULT, MANUAL, MANUALBASEDIR, PRESET, TEMPORARY;
public String toString() {
return this.name().toLowerCase();
}
}
private static final CreationStrategy DEFAULT_STRATEGY = CreationStrategy.DEFAULT;
private static Logger log = LoggerFactory.getLogger(RWorkspace.class);
private static final int TEMPDIR_NAME_LENGTH = 8;
private static final String WORKSPACE_PREFIX = "wps4r-workspace-";
public static final String ROOT = ".";
private static SimpleDateFormat dateFormat = new SimpleDateFormat("YYYYMd-HHmmss");
private static String createNewWorkspaceDirectoryName() {
StringBuilder wd = new StringBuilder();
wd.append(WORKSPACE_PREFIX);
wd.append(dateFormat.format(new Date()));
wd.append("_");
wd.append(UUID.randomUUID().toString().substring(0, TEMPDIR_NAME_LENGTH));
return wd.toString();
}
/**
* Indicates if the R working directory should be deleted after process execution If wpsWorkDirIsRWorkDir
* is set true, deletion of the directory shall be determined by deleteWPSWorDirectory
*/
private boolean deleteRWorkDirectory = true;
private String path = null;
/**
* In case of errors, this variable may be changed to true during runtime to prevent the system from
* deleting the wrong files
*/
private boolean temporarilyPreventingRWorkingDirectoryFromDelete = false;
private boolean wpsWorkDirIsRWorkDir = true;
private REXP createAndSetNewWorkspaceDirectory(File directory, RConnection connection) throws RserveException {
boolean b = directory.mkdir();
if (b) {
log.debug("Created new workdir: {}", directory);
String wd = directory.getAbsolutePath();
return setwd(connection, wd);
}
else {
log.error("Could not create new temp workspace directory at {}", directory);
return null;
}
}
private REXP createAndSetNewWorkspaceDirectoryInRTempdir(RConnection connection) throws RserveException {
REXP oldWorkdir = connection.eval("setwd(\"tempdir()\")");
return oldWorkdir;
}
private REXP createAndSetNewWorkspaceDirectoryInSystemTemp(RConnection connection) throws RserveException {
File tempdir = new File(System.getProperty("java.io.tmpdir"), createNewWorkspaceDirectoryName());
return createAndSetNewWorkspaceDirectory(tempdir, connection);
}
private REXP createAndSetNewWorkspaceDirectoryWithinCurrentRWD(RConnection connection) throws RserveException {
String randomFolderName = createNewWorkspaceDirectoryName();
connection.eval("dir.create(\"" + randomFolderName + "\")");
REXP oldWorkdir = setwd(connection, randomFolderName);
return oldWorkdir;
}
private REXP createAndSetNewWorkspaceDirectoyInBasePath(File f, RConnection connection) throws RserveException {
String randomFolderName = createNewWorkspaceDirectoryName();
File newDir = new File(f, randomFolderName);
return createAndSetNewWorkspaceDirectory(newDir, connection);
}
/**
* @return true if the unlink call was made successfully
*/
public boolean deleteCurrentAndSetWorkdir(RConnection connection, String originalWorkDir) {
if (this.wpsWorkDirIsRWorkDir && !connection.isConnected()) {
// R won't delete the folder if it is the same as the wps work directory
log.warn("Cannot delete directory, connection is not connected or workdir is WPS workdir ({}).",
this.wpsWorkDirIsRWorkDir);
return false;
}
if ( !this.deleteRWorkDirectory) {
log.warn("Deleting of the directory is disabled.");
return false;
}
// delete R work directory
if (this.path != null) {
try {
RLogger.log(connection, "Deleting workspace.");
String wdToDelete = connection.eval("getwd()").asString();
REXP oldwd = setwd(connection, originalWorkDir);
log.debug("Set wd to {} (was: {})", oldwd.toDebugString(), wdToDelete);
// should be true usually, if not, workdirectory has been changed unexpectedly (probably
// inside script)
if (wdToDelete != this.path) {
if ( !this.temporarilyPreventingRWorkingDirectoryFromDelete) {
log.debug("Unlinking (recursive delete) the directory {}", wdToDelete);
REXP eval = connection.eval("(unlink(\"" + wdToDelete + "\", recursive=TRUE))");
int result = eval.asInteger();
if (result == 0)
return true;
return false;
}
else
this.temporarilyPreventingRWorkingDirectoryFromDelete = false;
}
else
log.warn("Unexpected R workdirectory at end of R session, check the R sript for unwanted workdirectory changes.");
}
catch (RserveException e) {
log.error("Could not reset the work directory.", e);
}
catch (REXPMismatchException e) {
log.error("Could not reset the work directory.", e);
}
}
return false;
}
public String getPath() {
return path;
}
public boolean isWpsWorkDirIsRWorkDir() {
return wpsWorkDirIsRWorkDir;
}
private REXP setwd(RConnection connection, String wd) throws RserveException {
String wdString = wd.replace("\\", "/");
REXP oldWorkdir = connection.eval("setwd(\"" + wdString + "\")");
return oldWorkdir;
}
/**
* Sets the R working directory according to the "R_Work_Dir" configuration parameter. 4 cases are
* supported: 'default', 'preset', 'temporary' and 'custom'.
*
* Do not confuse the R working directory with the temporary WPS working directory (this.currentworkdir)!
* R and WPS use the same directory under default configuration, with Rserve on localhost, but running R
* on a remote machine requires separate working directories for WPS and R.
*
* @param connection
* @param workDirName
*
* @return the new working directory, which is already set.
*/
public String setWorkingDirectory(RConnection connection,
String currentWorkDir,
String strategyName,
boolean isRserveOnLocalhost,
String workDirName) throws REXPMismatchException,
RserveException,
ExceptionReport {
log.debug("Setting the R working directory... current work directory: {}", currentWorkDir);
log.debug("Try to set R work directory according to {} = {}",
RWPSConfigVariables.R_WORK_DIR_STRATEGY,
strategyName);
REXP oldWorkdir = null;
log.debug("Working on localhost: {}", isRserveOnLocalhost);
if (strategyName == null || strategyName.equals("")) {
log.error("Strategy is not defined: {}. Returning current work directory.", strategyName);
return currentWorkDir;
}
CreationStrategy strategy = CreationStrategy.valueOf(strategyName.trim().toUpperCase());
if (strategy.equals(DEFAULT_STRATEGY)) {
// Default behaviour: R work directory is the same as temporary WPS work directory if R runs
// locally otherwise, for remote connections, it is dependent on the configuration of R and Rserve
if (isRserveOnLocalhost) {
this.wpsWorkDirIsRWorkDir = true;
oldWorkdir = createAndSetNewWorkspaceDirectoryInSystemTemp(connection);
}
else {
// setting the R working directory relative to default R directory R starts from a work
// directory dependent on the behaviour and configuration of the R/Rserve installation
this.wpsWorkDirIsRWorkDir = false;
oldWorkdir = createAndSetNewWorkspaceDirectoryWithinCurrentRWD(connection);
}
}
else if (strategy.equals(CreationStrategy.PRESET)) {
// setting the R working directory relative to default R directory using R
wpsWorkDirIsRWorkDir = false;
oldWorkdir = createAndSetNewWorkspaceDirectoryWithinCurrentRWD(connection);
}
// setting the R working directory in a temporal folder
else if (strategy.equals(CreationStrategy.TEMPORARY)) {
if (isRserveOnLocalhost) {
wpsWorkDirIsRWorkDir = true;
oldWorkdir = createAndSetNewWorkspaceDirectoryInSystemTemp(connection);
}
else {
wpsWorkDirIsRWorkDir = false;
try {
oldWorkdir = createAndSetNewWorkspaceDirectoryInRTempdir(connection);
}
catch (RserveException e) {
temporarilyPreventingRWorkingDirectoryFromDelete = true;
throw new ExceptionReport("Invalid configuration of WPS4R, failed to create temporal working directory",
ExceptionReport.REMOTE_COMPUTATION_ERROR,
e);
}
}
}
else if (strategy.equals(CreationStrategy.MANUAL)) {
// in the manual strategy, the path is simply used
if (workDirName != null && !workDirName.isEmpty())
oldWorkdir = setwd(connection, workDirName);
else {
log.error("Work directory name is not provided, falling back to default strategy.");
return setWorkingDirectory(connection,
currentWorkDir,
DEFAULT_STRATEGY.toString(),
isRserveOnLocalhost,
workDirName);
}
}
else if (strategy.equals(CreationStrategy.MANUALBASEDIR)) {
// in the manualBaseDir strategy, the defined path is used as the base directory for random
// workspace names
File f = new File(workDirName);
boolean isInvalidPath = false;
if (isRserveOnLocalhost) {
if (f.isDirectory()) {
oldWorkdir = createAndSetNewWorkspaceDirectoyInBasePath(f, connection);
}
else
isInvalidPath = true;
}
else {
this.wpsWorkDirIsRWorkDir = false;
boolean isExistingDir = connection.eval("isTRUE(file.info(\"" + strategy + "\")$isdir)").asInteger() == 1;
if (isExistingDir) {
oldWorkdir = createAndSetNewWorkspaceDirectoyInBasePath(f, connection);
}
else
isInvalidPath = true;
}
if (isInvalidPath) {
log.error("Invalid configurarion for work directory. | {}={} | {}={} | Falling back to '{}'.",
RWPSConfigVariables.R_WORK_DIR_STRATEGY,
strategy,
RWPSConfigVariables.R_WORK_DIR_NAME,
workDirName,
DEFAULT_STRATEGY);
return setWorkingDirectory(connection,
currentWorkDir,
DEFAULT_STRATEGY.toString(),
isRserveOnLocalhost,
workDirName);
}
}
REXP newWorkdir = connection.eval("getwd()");
log.debug("Set workdir to {}, was {}", newWorkdir.toDebugString(), oldWorkdir.toDebugString());
if (connection.eval("length(dir()) == 0").asInteger() != 1) {
this.temporarilyPreventingRWorkingDirectoryFromDelete = true;
throw new ExceptionReport("Non-empty R working directory on process startup: " + oldWorkdir.toDebugString()
+ "\nThe process will not be executed to prevent the system from damage."
+ " Please check the configuration of WPS4R.", ExceptionReport.REMOTE_COMPUTATION_ERROR);
}
RLogger.logGenericRProcess(connection, "working directory: " + connection.eval("getwd()").asString());
String workDirPath = newWorkdir.asString();
this.path = workDirPath;
return getPath();
}
public Collection<File> listFiles() {
File f = new File(this.path);
ArrayList<File> files = new ArrayList<File>(Arrays.asList(f.listFiles()));
return files;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("RWorkspace [");
if (path != null) {
builder.append("path=");
builder.append(path);
builder.append(", ");
}
builder.append("deleteRWorkDirectory=");
builder.append(deleteRWorkDirectory);
builder.append(", temporarilyPreventingRWorkingDirectoryFromDelete=");
builder.append(temporarilyPreventingRWorkingDirectoryFromDelete);
builder.append(", wpsWorkDirIsRWorkDir=");
builder.append(wpsWorkDirIsRWorkDir);
builder.append("]");
return builder.toString();
}
public synchronized void createDirectory(String name, RConnection connection) throws RserveException {
if (name.equals(ROOT))
return;
log.debug("Creating directory {} in workspace {}", name, this);
StringBuilder sb = new StringBuilder();
sb.append("dir.create(\"");
sb.append(name);
sb.append("\")");
REXP result = connection.eval(sb.toString());
log.debug("Output of creating directory in workdir: {}", result.toDebugString());
}
public REXP copyFile(String dirName, String file, RConnection connection) throws RserveException {
log.debug("Copying file {} to dir '{}' in workspace {}", file, dirName, this);
StringBuilder sb = new StringBuilder();
sb.append("file.copy(from = \"");
sb.append(file);
sb.append("\", to = \"");
sb.append(dirName);
sb.append("/");
sb.append(file);
sb.append("\")");
REXP result = connection.eval(sb.toString());
return result;
}
public synchronized void copyFilesToDirectoryInWorkDir(String dirName, String[] files, RConnection connection) throws RserveException {
for (String file : files) {
REXP result = copyFile(dirName, file, connection);
log.debug("Output of copying file to dir: {}", result.toDebugString());
}
}
public void deleteFile(String file, RConnection connection) throws RserveException {
log.debug("Removing file from workdir: {}", file);
connection.removeFile(file);
}
public synchronized void deleteFilesInWorkDir(String[] files, RConnection connection) throws RserveException {
for (String file : files) {
deleteFile(file, connection);
}
}
public void moveFile(String dirName, String file, RConnection connection) throws RserveException {
log.debug("Moving file '{}' to '{}'", file, dirName);
copyFile(dirName, file, connection);
deleteFile(file, connection);
}
public synchronized void moveFilesToDirectoryInWorkDir(String dirName, String[] files, RConnection connection) throws RserveException {
copyFilesToDirectoryInWorkDir(dirName, files, connection);
deleteFilesInWorkDir(files, connection);
}
/**
*
* @param source
* @param name
* can be a path (starting with '.'), but all intermediate folders must exist
* @param connection
* @throws IOException
*/
public void copyFile(File source, String name, FilteredRConnection connection) throws IOException {
// use RFileOutputStream so that remote RServe is supported
RFileOutputStream rfos = connection.createFile(name);
byte[] buffer = new byte[2048];
FileInputStream is = new FileInputStream(source);
int stop = is.read(buffer);
while (stop != -1) {
rfos.write(buffer, 0, stop);
stop = is.read(buffer);
}
rfos.flush();
rfos.close();
is.close();
log.debug("File copied with R from {} as '{}' to {}", source, name, rfos);
}
}