SiLeBAT/FSK-Lab

View on GitHub
de.bund.bfr.knime.fsklab.preferences/src/de/bund/bfr/knime/fsklab/preferences/RBinUtil.java

Summary

Maintainability
C
1 day
Test Coverage
/*
 * ------------------------------------------------------------------ Copyright by KNIME GmbH,
 * Konstanz, Germany Website: http://www.knime.org; Email: contact@knime.org
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License, Version 3, as published by the Free Software Foundation.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License along with this program; if
 * not, see <http://www.gnu.org/licenses>.
 *
 * Additional permission under GNU GPL version 3 section 7:
 *
 * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. Hence, KNIME and ECLIPSE are
 * both independent programs and are not derived from each other. Should, however, the
 * interpretation of the GNU GPL Version 3 ("License") under any applicable laws result in KNIME and
 * ECLIPSE being a combined program, KNIME GMBH herewith grants you the additional permission to use
 * and propagate KNIME together with ECLIPSE with only the license terms in place for ECLIPSE
 * applying to ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the license terms of
 * ECLIPSE themselves allow for the respective use and propagation of ECLIPSE together with KNIME.
 *
 * Additional permission relating to nodes for KNIME that extend the Node Extension (and in
 * particular that are based on subclasses of NodeModel, NodeDialog, and NodeView) and that only
 * interoperate with KNIME through standard APIs ("Nodes"): Nodes are deemed to be separate and
 * independent programs and to not be covered works. Notwithstanding anything to the contrary in the
 * License, the License does not apply to Nodes, you are not required to license Nodes under the
 * License, and you are granted a license to prepare and propagate Nodes, in each case even if such
 * Nodes are propagated with or for interoperation with KNIME. The owner of a Node may freely choose
 * the license terms applicable to such Node, including when such Node is propagated with or for
 * interoperation with KNIME. ---------------------------------------------------------------------
 *
 * History 17.09.2007 (thiel): created
 */
package de.bund.bfr.knime.fsklab.preferences;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;

import org.apache.commons.io.FilenameUtils;
import org.eclipse.core.runtime.Platform;
import org.knime.core.node.KNIMEConstants;
import org.knime.core.node.NodeLogger;
import org.knime.core.util.FileUtil;

/**
 * Utility class with methods to call R binary.
 *
 * @author Heiko Hofer.
 */
public class RBinUtil {

    /**
     * The temp directory used as a working directory for R.
     */
    private static final String TEMP_PATH = FilenameUtils.separatorsToUnix(KNIMEConstants.getKNIMETempDir());

    private final static NodeLogger LOGGER = NodeLogger.getLogger(RBinUtil.class);

    /**
     * Exception thrown when the specified R_HOME directory is invalid.
     *
     * @author Jonathan Hale
     */
    public static class InvalidRHomeException extends Exception {

        /**
         * Generated serialVersionUID
         */
        private static final long serialVersionUID = -69909100381242901L;

        /**
         * Constructor
         *
         * @param msg
         *            error message
         */
        private InvalidRHomeException(final String msg) {
            super(msg);
        }
    }

    /**
     * Get properties about the used R.
     *
     * @return properties about used R.
     * @throws IOException
     *             in case that running R fails.
     */
    public static Properties retrieveRProperties() throws IOException {
        return retrieveRProperties(PreferenceInitializer.getR3Provider());
    }

    /**
     * Get properties about the used R installation.
     *
     * @param rpref
     *            provider for path to R executable
     * @return properties about used R
     */
    public static Properties retrieveRProperties(final RPreferenceProvider rpref) {
        
        final File tmpPath = new File(TEMP_PATH);
        File propsFile;
        File rOutFile;

        try {
            propsFile = FileUtil.createTempFile("R-propsTempFile-", ".r", true);
            rOutFile = FileUtil.createTempFile("R-propsTempFile-", ".Rout", tmpPath, true);
        } catch (IOException e2) {
            LOGGER.error("Could not create temporary files for R execution.");
            return new Properties();
        }

        final String propertiesPath = FilenameUtils.separatorsToUnix(propsFile.getAbsolutePath());
        final String script = "setwd('" + FilenameUtils.separatorsToUnix(tmpPath.getAbsolutePath()) + "')\n"
                + "foo <- paste(names(R.Version()), R.Version(), sep='=')\n"
                + "foo <- append(foo, paste('memory.limit', memory.limit(), sep='='))\n"
                + "foo <- append(foo, paste('Rserve.path', find.package('Rserve', quiet=TRUE), sep='='))\n"
                + "foo <- append(foo, paste('miniCRAN.path', find.package('miniCRAN', quiet=TRUE), sep='='))\n"
                + "foo <- append(foo, paste('svglite.path', find.package('svglite', quiet=TRUE), sep='='))\n"
                + "foo <- append(foo, paste('Cairo.path', find.package('Cairo', quiet=TRUE), sep='='))\n"
                + "foo <- append(foo, paste('rhome', R.home(), sep='='))\n" //
                + "write(foo, file='" + propertiesPath + "', ncolumns=1, append=FALSE, sep='\\n')\nq()";

        File rCommandFile;
        try {
            rCommandFile = writeRCommandFile(script);
        } catch (IOException e1) {
            LOGGER.error("Could not write R command file.");
            return new Properties();
        }
        ProcessBuilder builder = new ProcessBuilder();
        builder.command(rpref.getRBinPath("Rscript").toString(), "--vanilla", rCommandFile.getName(),
                rOutFile.getName());
        builder.directory(rCommandFile.getParentFile());

        /** Run R on the script to get properties */
        try {
            final Process process = builder.start();
            final BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            final BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));

            // Consume the output produced by the R process, otherwise may block
            // process on some operating
            // system
            new Thread(() -> {
                try {
                    final StringBuilder b = new StringBuilder();
                    String line;
                    while ((line = outputReader.readLine()) != null) {
                        b.append(line);
                    }
                    LOGGER.debug("External RScript process output: " + b.toString());
                } catch (Exception e) {
                    LOGGER.error("Error reading output of external R process.", e);
                }
            }, "R Output Reader").start();

            new Thread(() -> {
                try {
                    final StringBuilder b = new StringBuilder();
                    String line;
                    while ((line = errorReader.readLine()) != null) {
                        b.append(line);
                    }
                    LOGGER.debug("External Rscript process error output: " + b.toString());
                } catch (Exception e) {
                    LOGGER.error("Error reading error output of external R process.", e);
                }
            }, "R Error Reader").start();

            process.waitFor();
        } catch (Exception e) {
            LOGGER.debug(e.getMessage(), e);
            return new Properties();
        }

        // load properties from propsFile
        Properties props = new Properties();
        try (FileInputStream fis = new FileInputStream(propsFile)) {
            props.load(fis);
        } catch (IOException e) {
            LOGGER.warn("Could not retrieve properties from R.", e);
        }

        return props;
    }

    /**
     * Writes the given string into a file and returns it.
     *
     * @param cmd
     *            The string to write into a file.
     * @return The file containing the given string.
     * @throws IOException
     *             If string could not be written to a file.
     */
    private static File writeRCommandFile(final String cmd) throws IOException {
        File tempCommandFile = FileUtil.createTempFile("R-readPropsTempFile-", ".r", new File(TEMP_PATH), true);
        try (FileWriter fw = new FileWriter(tempCommandFile)) {
            fw.write(cmd);
        }

        return tempCommandFile;
    }

    /**
     * @param rHomePath
     * @throws InvalidRHomeException
     */
    public static void checkRHome(final String rHomePath) throws InvalidRHomeException {
        checkRHome(rHomePath, false);
    }

    /**
     * Checks whether the given path is a valid R_HOME directory. It checks the
     * presence of the bin and library folder.
     *
     * @param rHomePath
     *            path to R_HOME.
     * @param fromPreferences
     *            Set to true if this function is called from the R preference page.
     * @throws InvalidRHomeException
     *             If the specified R_HOME path is invalid.
     */
    public static void checkRHome(final String rHomePath, final boolean fromPreferences) throws InvalidRHomeException {

        final Path rHome = Paths.get(rHomePath);
        final String msgSuffix = ((fromPreferences) ? ""
                : " R_HOME ('" + rHomePath + "')" + " is meant to be the path to the folder which is the root of R's "
                        + "installation tree. \nIt contains a 'bin' folder which itself contains the R executable and a "
                        + "'library' folder. Please change the R settings in the preferences.");
        final String R_HOME_NAME = (fromPreferences) ? "Path to R Home" : "R_HOME";

        /* check if the directory exists. */
        if (Files.notExists(rHome)) {
            throw new InvalidRHomeException(R_HOME_NAME + " does not exist." + msgSuffix);
        }

        /* Make sure R home is not a file. */
        if (!Files.isDirectory(rHome)) {
            throw new InvalidRHomeException(R_HOME_NAME + " is not a directory." + msgSuffix);
        }

        /* Check if there is a bin directory. */
        final Path binDir = rHome.resolve("bin");
        if (!Files.isDirectory(binDir)) {
            throw new InvalidRHomeException(R_HOME_NAME + " does not contain a folder with name 'bin'." + msgSuffix);
        }

        /* Check if there is an R Executable. */
        final Path rExecutable = new DefaultRPreferenceProvider(rHomePath).getRBinPath("R");
        if (Files.notExists(rExecutable)) {
            throw new InvalidRHomeException(R_HOME_NAME + " does not contain an R executable." + msgSuffix);
        }

        /* Make sure there is a library directory. */
        final Path libraryDir = rHome.resolve("library");
        if (!Files.isDirectory(libraryDir)) {
            throw new InvalidRHomeException(
                    R_HOME_NAME + " does not contain a folder with name 'library'." + msgSuffix);
        }

        /*
         * On Windows, we expect the appropiate platform-specific folders corresponding
         * to out Platform.
         */
        if (Platform.getOS().equals(Platform.OS_WIN32)) {
            if (Platform.getOSArch().equals(Platform.ARCH_X86_64)) {
                final Path x64Path = binDir.resolve("x64");
                final Path i386Path = binDir.resolve("i386");

                if (!Files.isDirectory(x64Path) && !Files.isDirectory(i386Path)) {
                    throw new InvalidRHomeException(R_HOME_NAME
                            + " does not contain a folder with name 'bin\\x64'. Please install R 64-bit files."
                            + msgSuffix);
                }
            } else {
                final Path i386Path = binDir.resolve("i386");
                if (!Files.isDirectory(i386Path)) {
                    throw new InvalidRHomeException(R_HOME_NAME
                            + " does not contain a folder with name '\\bin\\i386'. Please install R 32-bit files."
                            + msgSuffix);
                }
            }
        }
    }
}