de.bund.bfr.knime.fsklab.r/src/de/bund/bfr/knime/fsklab/r/client/LibRegistry.java
/* *************************************************************************************************** * Copyright (c) 2017 Federal Institute for Risk Assessment (BfR), Germany * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * 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/>. * * Contributors: Department Biological Safety - BfR ************************************************************************************************* */package de.bund.bfr.knime.fsklab.r.client; import java.io.IOException;import java.net.InetAddress;import java.net.InetSocketAddress;import java.net.MalformedURLException;import java.net.Proxy;import java.net.URL;import java.net.URLConnection;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.Arrays;import java.util.HashSet;import java.util.List;import java.util.Set;import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils;import org.rosuda.REngine.REXP;import org.rosuda.REngine.REXPMismatchException;import org.rosuda.REngine.RList; import com.sun.jna.Platform;import de.bund.bfr.knime.fsklab.preferences.PreferenceInitializer;import de.bund.bfr.knime.fsklab.r.client.IRController.RException;import de.bund.bfr.knime.fsklab.r.server.RConnectionFactory.RConnectionResource; /** * Singleton!! There can only be one. * * @author Miguel Alba */public class LibRegistry { private static LibRegistry instance; /** Installation path. */ private Path installPath; /** miniCRAN repository path. */ private Path repoPath; /** Utility set to keep count of installed libraries. */ private final Set<String> installedLibs; /** Utility RController for running R commands. */ public final RController controller = new RController(); private String type; private RWrapper rWrapper; private final String MIRROR = "https://cran.rstudio.com"; Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed. private LibRegistry() throws IOException, RException { if (Platform.isWindows()) { type = "win.binary"; } else if (Platform.isMac()) { type = "mac.binary"; } else { type = "source"; } // Prepare rWrapper rWrapper = new RWrapper(); rWrapper.library("miniCRAN"); if((!PreferenceInitializer.getRPath().contains(RprofileManager.BFR_R_PLUGIN_NAME) && Platform.isWindows()) || Platform.isMac()) { try { String[] rPath= controller.eval(".libPaths()", true).asStrings(); //get default library path. installPath = Paths.get(rPath[0]); repoPath = installPath.getParent().resolve("cran"); } catch (REXPMismatchException | RException e1) { e1.printStackTrace(); } }else { Path userFolder = Paths.get(System.getProperty("user.home")); Path fskFolder = userFolder.resolve(".fsk"); // CRAN and library folders installPath = fskFolder.resolve("library"); repoPath = fskFolder.resolve("cran"); } // Validate .fsk folder if (Files.exists(installPath) && Files.exists(repoPath)) { // TODO: Need to validate further: library and CRAN // Initialize `installedLibs` with `installPath` String[] pkgArray = installPath.toFile().list(); installedLibs = Arrays.stream(pkgArray).collect(Collectors.toSet()); } else { // Create directories if(!Files.exists(repoPath)) Files.createDirectory(repoPath); if(!Files.exists(installPath)) Files.createDirectory(installPath); // Create CRAN structure in repoPath rWrapper.makeRepo(repoPath); installedLibs = new HashSet<>(); } } public synchronized static LibRegistry instance() throws IOException, RException { if (instance == null ) { instance = new LibRegistry(); PreferenceInitializer.refresh = false; } if(PreferenceInitializer.refresh) refreshInstance(); return instance; } private static void refreshInstance() { synchronized(instance) { try { instance.controller.close(); // Wait until controller is actually closed. Remove this call to "wait" or move it into a "while" loop. instance.wait(RConnectionResource.RPROCESS_TIMEOUT + 2000); instance = new LibRegistry(); } catch(Exception e) { instance = null; } finally { PreferenceInitializer.refresh = false; } } } /** * Install a list of packages into the repository. Already installed packages * are ignored. * * @param libs list of names of R libraries * @throws RException * @throws REXPMismatchException * @throws NoInternetException */Method `install` has a Cognitive Complexity of 20 (exceeds 5 allowed). Consider refactoring.
Method `install` has 36 lines of code (exceeds 25 allowed). Consider refactoring.
Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed. public synchronized void install(final List<String> packages) throws RException, REXPMismatchException, NoInternetException { if (Platform.isLinux() || Platform.isMac()) { // Install missing packages controller.addPackagePath(installPath); String[] installedPackagesArray = controller.eval("rownames(installed.packages())", true).asStrings(); Set<String> installedPackagesSet = Arrays.stream(installedPackagesArray).collect(Collectors.toSet()); for (String pkg : packages) { if (!installedPackagesSet.contains(pkg)) { if (!isNetAvailable()) { throw new NoInternetException(packages); } String cmd = String.format("install.packages('%s', lib = '%s', repos = '%s')", pkg, rWrapper._path2String(installPath), MIRROR); controller.eval(cmd, false); } } installedPackagesArray = controller.eval("rownames(installed.packages())", true).asStrings(); installedPackagesSet = Arrays.stream(installedPackagesArray).collect(Collectors.toSet()); } else { if (installedLibs.containsAll(packages)) return; if (rWrapper.areAllInstalled(packages)) return; // pkgDep requires miniCRAN to be loaded, on repeated executions of the Runner this might not have happened rWrapper.library("miniCRAN"); // Gets missing packages List<String> missingPackages; // try to collect missing packages; if it fails, consider all packages as missing try { missingPackages = rWrapper.pkgDep(packages).stream().filter(pkg -> !installedLibs.contains(pkg)) .collect(Collectors.toList()); } catch(Exception e) { missingPackages = packages; } if (!missingPackages.isEmpty()) { // Adds the dependencies to the miniCRAN repository rWrapper.addPackage(missingPackages, repoPath); // Install with install.packages directly on Linux // Gets the paths to the binaries of these dependencies List<Path> paths = rWrapper.checkVersions(missingPackages, repoPath); // Install binaries rWrapper.installPackages(paths, installPath); // Adds names of installed libraries to utility set installedLibs.addAll(missingPackages); } } } /** * Gets list of paths to the binaries of the desired libraries. * * @param libs * @return list of paths to the binaries of the desired libraries * @throws RException * @throws REXPMismatchException */ public Set<Path> getPaths(List<String> libs) throws RException, REXPMismatchException { // Gets list of R dependencies of libs List<String> deps = rWrapper.pkgDep(libs); // Gets the paths to the binaries of these dependencies List<Path> paths = rWrapper.checkVersions(deps, repoPath); return new HashSet<>(paths); } /** * @return Path of a single R package or null if lib cannot be found. * @throws RException * @throws REXPMismatchException */ public Path getPath(String lib) throws REXPMismatchException, RException { List<String> libs = Arrays.asList(lib); List<Path> paths = rWrapper.checkVersions(libs, repoPath); return paths.isEmpty() ? null : paths.get(0); } public Path getInstallationPath() { return installPath; } public Path getRepositoryPath() { return repoPath; } private boolean isNetAvailable() { try { final URL url = new URL(MIRROR); final URLConnection conn; // Open url with proxy if on BfR computer if (InetAddress.getLocalHost().getCanonicalHostName().endsWith("it.bfr-science.de")) { conn = url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("webproxy", 8080))); } else { conn = url.openConnection(); } conn.connect(); conn.getInputStream().close(); return true; } catch (MalformedURLException e) { throw new RuntimeException(e); } catch (IOException e) { return false; } } public static class NoInternetException extends Exception { /** Generated serialVersionUID */ private static final long serialVersionUID = 2815440774381106769L; /** Constructor */ public NoInternetException(final List<String> packages) { super("Not connected to Internet. Packages {" + String.join(",", packages) + "} could not be downloaded"); } } private class RWrapper { // R commands /** * Load and attach add-on packages. * * @param pkg The name of a package. * @throws RException * * @see <a href= * "https://stat.ethz.ch/R-manual/R-devel/library/base/html/library.html"> * R documentation</a> */ void library(final String pkg) throws RException { String cmd = "library(" + pkg + ")"; controller.eval(cmd, false); } Refactor this method to not always return the same value. boolean areAllInstalled(final List<String> pkgs) { for (String pkg : pkgs) { try { library(pkg); } catch (RException e) { return false; } } return true; } /** * Install packages from local files. * * @param pkgs List of package files. The files can be source distributions * (.tar.gz) or binary distributions (.zip for Windows and .tgz for * Mac). * @param lib Directory where packages are installed. * @throws RException * * @see <a href= * "https://stat.ethz.ch/R-manual/R-devel/library/utils/html/install.packages.html"> * R documentation</a> */ void installPackages(final List<Path> pkgs, final Path lib) throws RException { String pkgsAsString = pkgs.stream().map(Path::toString).map(FilenameUtils::separatorsToUnix) .map(str -> "'" + str + "'").collect(Collectors.joining(", ")); String pkgList = "c(" + pkgsAsString + ")";Define a constant instead of duplicating this literal "', type = '" 4 times. String cmd = "install.packages(" + pkgList + ", repos = NULL, lib = '" + _path2String(lib) + "', type = '" + type + "')"; controller.eval(cmd, false); } // miniCRAN commands /** * Add packages to a miniCRAN repository. * * @param pkgs List of names of packages to be downloaded. * @param path Destination download path. This path is the root folder of the * repository. * @throws RException * * @see <a href= * "https://cran.r-project.org/web/packages/miniCRAN/miniCRAN.pdf"> * miniCRAN documentation</a> */ void addPackage(final List<String> pkgs, final Path path) throws RException {Define a constant instead of duplicating this literal "', repos = '" 3 times. String cmd = "addPackage(" + _pkgList(pkgs) + ", '" + _path2String(path) + "', repos = '" + MIRROR + "', type = '" + type + "', deps = FALSE)"; controller.eval(cmd, false); } /** * Returns the file paths for the specified packages. * * @param pkgs List of names of packages. * @param path The local path to the directory where the miniCRAN repo resides. * @return the file paths for the specified packages * @throws REXPMismatchException * @throws RException * * @see <a href= * "https://cran.r-project.org/web/packages/miniCRAN/miniCRAN.pdf"> * miniCRAN documentation</a> */ List<Path> checkVersions(final List<String> pkgs, final Path path) throws REXPMismatchException, RException { String cmd = "checkVersions(" + _pkgList(pkgs) + ", '" + _path2String(path) + "', type = '" + type + "')"; REXP rexp = controller.eval(cmd, true); // Sometimes checkVersions returns a list (specially on Mac) if (rexp.isList()) { RList list = rexp.asList(); REXP element0 = list.at(0); String[] values = element0.asStrings(); return Arrays.stream(values).map(Paths::get).collect(Collectors.toList()); } if (rexp.isString()) { String[] pathsArray = rexp.asStrings(); return Arrays.stream(pathsArray).map(Paths::get).collect(Collectors.toList()); } throw new REXPMismatchException(rexp, "Unsupported return type"); } /** * Creates a local repository in the specified path. * <p> * Creates a CRAN folder structure in the specified destination folder and then * creates the PACKAGES index file. Since the folder structure mimics the * required structure and files of a CRAN repository, it supports functions like * <i>install.packages()</i>. * * @param path Destination download path. This path is the root folder of the * repository. * @throws RException * * @see <a href= * "https://cran.r-project.org/web/packages/miniCRAN/miniCRAN.pdf"> * miniCRAN documentation</a> */ void makeRepo(final Path path) throws RException { String cmd = "makeRepo(c(), '" + _path2String(path) + "', repos = '" + MIRROR + "', type = '" + type + "')"; controller.eval(cmd, false); } /** * Retrieve package dependencies. * <p> * Perform recursive retrieve for Depends, Imports and LinkLibrary. Performs * non-recursive retrieve for Suggests. * * @param pkgs List of names of packages. * @return the dependencies of the specified packages * @throws RException * @throws REXPMismatchException * * @see <a href= * "https://cran.r-project.org/web/packages/miniCRAN/miniCRAN.pdf"> * miniCRAN documentation</a> */ List<String> pkgDep(final List<String> pkgs) throws RException, REXPMismatchException { String cmd = "pkgDep(" + _pkgList(pkgs) + ", type = '" + type + "', repos = '" + MIRROR +"')"; REXP rexp = controller.eval(cmd, true); return Arrays.asList(rexp.asStrings()); } // Utility method. Should not be used outside of RCommandBuilder. String _pkgList(final List<String> pkgs) { return "c(" + pkgs.stream().map(pkg -> "'" + pkg + "'").collect(Collectors.joining(", ")) + ")"; } // Utility method. Should not be used outside of RCommandBuilder. String _path2String(final Path path) { return FilenameUtils.separatorsToUnix(path.toString()); } }}