/*=============================================================================#
# Copyright (c) 2005-2016 RoSuDa (www.rosuda.org) and others.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the GNU Lesser General Public License
# v2.1 or newer, which accompanies this distribution, and is available at
# http://www.gnu.org/licenses/lgpl.html
#
# Contributors:
# RoSuDa, University Augsburg - initial API and implementation
# Stephan Wahlbrink - adjustments to RJ
#=============================================================================*/
package de.walware.rj.server.jri.loader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import de.walware.rj.server.srvImpl.RJClassLoader;
import de.walware.rj.server.srvext.ServerUtil;
public class JRIClassLoader extends RJClassLoader {
private static JRIClassLoader instance;
public static JRIClassLoader getRJavaClassLoader() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
synchronized (JRIClassLoader.class) {
if (instance != null) {
return instance;
}
instance = (JRIClassLoader) Class.forName("RJavaClassLoader").newInstance();
return instance;
}
}
private static final Logger LOGGER = Logger.getLogger("de.walware.rj.server.jri");
public static boolean verbose = false;
public static void setDebug(final int level) {
verbose = (level > 0);
if (verbose) {
LOGGER.setLevel(Level.ALL);
}
}
private static final Pattern PATH_SPLITTER = Pattern.compile(Pattern.quote(File.pathSeparator));
private static String getNonEmpty(final String... strings) {
for (final String string : strings) {
if (string != null && string.length() > 0) {
return string;
}
}
return null;
}
/**
* Light extension of File that handles file separators and updates
*/
private static class UnixFile extends java.io.File {
private static final long serialVersionUID = 6120112960030467453L;
/**
* cached "last time modified" stamp
*/
private long lastModStamp;
public Object cache;
/**
* Constructor. Modifies the path so that the proper path separator is used (most useful on windows)
*/
public UnixFile(final String fn) {
super((separatorChar != '/') ? fn.replace('/', separatorChar) : fn);
this.lastModStamp = 0;
}
/**
* @return whether the file modified since last time the update method was called
*/
public boolean hasChanged() {
final long curMod = lastModified();
return (curMod != this.lastModStamp);
}
/**
* Cache the result of the lastModified stamp
*/
public void update() {
this.lastModStamp = lastModified();
}
}
/**
* Specialization of UnixFile that deals with jar files
*/
private static class UnixJarFile extends UnixFile {
private static final long serialVersionUID = -956832260008070610L;
/**
* The cached jar file
*/
private ZipFile zfile ;
/**
* common prefix for all URLs within this jar file
*/
private String urlPrefix ;
public UnixJarFile(final String filename) {
super(filename);
}
@Override
public void update() {
try {
if (this.zfile != null) {
this.zfile.close();
}
this.zfile = new ZipFile(this) ;
} catch (final Exception tryCloseX) {}
/* time stamp */
super.update() ;
}
/**
* Get an input stream for a resource contained in the jar file
*
* @param name file name of the resource within the jar file
* @return an input stream representing the resouce if it exists or null
*/
public InputStream getResourceAsStream(final String name) {
if (this.zfile==null || hasChanged()) {
update();
}
try {
if (this.zfile == null) {
return null;
}
final ZipEntry e = this.zfile.getEntry(name);
if (e != null) {
return this.zfile.getInputStream(e);
}
} catch (final Exception e) {
if (verbose) {
LOGGER.log(Level.WARNING, "Failed to create resource stream for JAR file.", e);
}
}
return null;
}
public URL getResource(final String name) {
if(this.zfile == null || this.zfile.getEntry(name) == null) {
return null;
}
URL u = null;
if (this.urlPrefix == null) {
try{
this.urlPrefix = "jar:" + toURL().toString() + "!";
} catch(final java.net.MalformedURLException ex) {
}
}
try{
u = new URL(this.urlPrefix + name) ;
} catch (final java.net.MalformedURLException ex) {
}
return u ;
}
}
/**
* Specialization of UnixFile representing a directory
*/
private static class UnixDirectory extends UnixFile {
private static final long serialVersionUID = -5697105404215366546L;
public UnixDirectory(final String dirname) {
super(dirname);
}
}
private final String r_home;
private final String r_arch;
private final List<String> r_libs_site;
private final List<String> r_libs_user;
private final List<String> r_libs;
private final int os;
/**
* Path of rj R package
*/
private String rjPath;
/**
* Map of native libraries (name, path)
*/
private final HashMap<String, UnixFile> libMap;
/**
* The class path list for alternative class loading
*/
private final Vector<UnixFile> classPath;
private final Set<String> defaultLibPath;
/**
* Should the system class loader be used to resolve classes as well as this class loader
*/
private final boolean useSystem = !"false".equalsIgnoreCase(System.getProperty("rjava.classloader.system")); // default true
private final boolean useSecond = "true".equalsIgnoreCase(System.getProperty("rjava.classloader.alternative")); // default false
protected JRIClassLoader() {
super(new URL[0], getSystemClassLoader());
this.libMap = new HashMap<>();
this.classPath = new Vector<>();
this.defaultLibPath = new LinkedHashSet<>();
if (verbose) {
LOGGER.log(Level.CONFIG, "System URL classloader: " + (this.useSystem ? "enabled" : "disabled"));
LOGGER.log(Level.CONFIG, "Alternative classloader: " + (this.useSecond ? "enabled" : "disabled"));
}
this.r_home = checkDirPath(System.getenv("R_HOME"));
this.r_arch = getNonEmpty(System.getenv("R_ARCH"), System.getProperty("r.arch"));
this.r_libs_site = checkDirPathList(System.getenv("R_LIBS_SITE"));
this.r_libs_user = checkDirPathList(System.getenv("R_LIBS_USER"));
this.r_libs = checkDirPathList(System.getenv("R_LIBS"));
final String osname = System.getProperty("os.name").toLowerCase();
if (osname.contains("win")) {
this.os = OS_WIN;
}
else if (osname.contains("mac")) {
this.os = OS_MAC;
}
else {
this.os = OS_NIX;
}
initRLibs();
this.rjPath = System.getProperty("de.walware.rj.rpkg.path");
if (this.rjPath == null) {
this.rjPath = searchPackageInLibrary("rj");
}
if (this.rjPath == null) {
final String message = "Path to rj package not found. Use R_LIBS or java property 'de.walware.rj.rpkg.path' to specify the location.";
LOGGER.log(Level.SEVERE, message);
throw new RuntimeException(message);
}
final String rJavaClassPath = System.getProperty("rjava.class.path");
if (rJavaClassPath != null) {
addClassPath(checkDirPathList(rJavaClassPath));
}
// rJava library
String rJavaPath = System.getProperty("rjava.path");
if (rJavaPath == null) {
rJavaPath = searchPackageInLibrary("rJava");
}
if (rJavaPath != null) {
String rJavaLibPath = System.getProperty("rjava.rjavalibs");
if (rJavaLibPath == null) {
rJavaLibPath = rJavaPath + "/java";
}
addClassPath(rJavaPath + "/java");
final String rJavaDynlibName;
switch (this.os) {
case OS_WIN:
rJavaDynlibName = "rJava.dll";
break;
default:
rJavaDynlibName = "rJava.so";
break;
}
final UnixFile rJavaDynlibFile = new UnixFile(rJavaLibPath + '/' + rJavaDynlibName);
if (rJavaDynlibFile.exists()) {
this.libMap.put("rJava", rJavaDynlibFile);
}
}
// jri library
String jriLibPath = System.getProperty("rjava.jrilibs");
if (jriLibPath == null) {
jriLibPath = this.rjPath + "/jri";
}
final String jriDynlibName;
switch (this.os) {
case OS_WIN:
jriDynlibName = "jri.dll";
break;
case OS_MAC:
jriDynlibName = "libjri.jnilib";
break;
default:
jriDynlibName = "libjri.so";
break;
}
final UnixFile jriDynlibFile = searchFile(getRArchNames(jriLibPath, jriDynlibName));
if (jriDynlibFile != null) {
this.libMap.put("jri", jriDynlibFile);
if (verbose) {
LOGGER.log(Level.CONFIG, "registered JRI: " + jriDynlibFile);
}
}
else {
if (verbose) {
LOGGER.log(Level.WARNING, jriDynlibName + " not found");
}
}
final UnixFile jriJarFile = searchFile(new String[] {
jriLibPath + "/JRI.jar",
this.rjPath + "/jri/JRI.jar",
});
if (jriJarFile != null) {
addClassPath(jriJarFile);
}
else {
logEntries();
final String message = "JRI.jar not found.";
LOGGER.log(Level.SEVERE, message);
throw new RuntimeException(message);
}
final UnixFile rjJarFile = searchFile(new String[] {
this.rjPath + "/server/rj.jar",
});
if (rjJarFile != null) {
addClassPath(rjJarFile);
}
else {
logEntries();
final String message = "rj.jar not found.";
LOGGER.log(Level.SEVERE, message);
throw new RuntimeException(message);
}
if (verbose) {
logEntries();
}
}
protected void logEntries() {
final StringBuilder sb = new StringBuilder();
sb.append("RJ/R-Java ClassLoader / Registery native libraries:");
ServerUtil.prettyPrint(this.libMap, sb);
LOGGER.log(Level.INFO, sb.toString());
sb.setLength(0);
sb.append("RJ/R-Java ClassLoader / Registery class paths:");
if (this.useSystem) {
ServerUtil.prettyPrint(Arrays.asList(getURLs()), sb);
}
else {
ServerUtil.prettyPrint(this.classPath, sb);
}
LOGGER.log(Level.INFO, sb.toString());
}
private String checkDirPath(String path) {
if (path != null) {
path = path.trim();
if (path.length() > 0) {
path = path.replace('\\', '/');
int end = path.length();
while (end > 0 && path.charAt(end-1) == '/') {
end--;
}
if (end != path.length()) {
path = path.substring(0, end);
}
if (path.length() > 0) {
return path;
}
}
}
return null;
}
private List<String> checkDirPathList(String pathList) {
if (pathList != null) {
pathList = pathList.trim();
if (pathList.length() > 0) {
final String[] split = PATH_SPLITTER.split(pathList);
final ArrayList<String> list = new ArrayList<>(split.length);
for (int i = 0; i < split.length; i++) {
final String path = checkDirPath(split[i]);
if (path != null) {
list.add(path);
}
}
return list;
}
}
return null;
}
private String[] getRArchNames(final String prefix, final String postfix) {
final String[] names;
int i = 0;
if (this.r_arch != null) {
names = new String[2];
names[i++] = prefix + this.r_arch + '/' + postfix;
}
else {
names = new String[1];
}
names[i++] = prefix + '/' + postfix;
return names;
}
@Override
public int getOSType() {
return this.os;
}
private UnixFile searchFile(final String[] search) {
for (final String path : search) {
final UnixFile file = new UnixFile(path);
if (file.exists()) {
return file;
}
}
return null;
}
private void initRLibs() {
synchronized (this.defaultLibPath) {
this.defaultLibPath.clear();
// R other libraries (R_LIBS)
if (this.r_libs != null) {
for (final String l : this.r_libs) {
this.defaultLibPath.add(l);
}
}
// R user libraries (R_LIBS_USER)
if (this.r_libs_user != null) {
for (final String l : this.r_libs_user) {
this.defaultLibPath.add(l);
}
}
// R site libraries (R_LIBS_SITE)
if (this.r_libs_site != null) {
for (final String l : this.r_libs_site) {
this.defaultLibPath.add(l);
}
}
else if (this.r_home != null) {
if (this.r_home.startsWith("/usr/lib")) {
this.defaultLibPath.add("/usr/local/lib"+this.r_home.substring(8)+"/site-library");
}
}
// R default library
if (this.r_home != null) {
this.defaultLibPath.add(this.r_home+"/library");
}
if (verbose) {
final StringBuilder sb = new StringBuilder((1+this.defaultLibPath.size())*32);
sb.append("RJ/R-Java ClassLoader / R library path:");
ServerUtil.prettyPrint(this.defaultLibPath, sb);
LOGGER.log(Level.CONFIG, sb.toString());
}
}
}
private String searchPackageInLibrary(final String name) {
synchronized (this.defaultLibPath) {
for (final String l : this.defaultLibPath) {
try {
final String p = l+'/'+name;
final File dir = new File(p, "DESCRIPTION");
if (dir.exists() && dir.isFile()) {
return p;
}
}
catch (final Exception e) {}
}
return null;
}
}
private String classNameToFile(final String cls) {
// convert . to /
return cls.replace('.', '/') + ".class";
}
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
if (verbose) {
LOGGER.entering("RJavaClassLoader", "findClass", name);
}
Class cl = null;
if ("RJavaClassLoader".equals(name)) {
return getClass();
}
if (this.useSystem) {
try {
cl = super.findClass(name);
if (cl != null) {
if (verbose) {
LOGGER.log(Level.FINE, "Found class '" + name + "' using URL loader");
}
return cl;
}
}
catch (final Exception e) {
if (verbose) {
LOGGER.log(Level.FINE, "URL Loader could not find class", e);
}
if (!this.useSecond && e instanceof ClassNotFoundException) {
throw (ClassNotFoundException) e;
}
}
}
if (this.useSecond) {
if (verbose) {
System.out.println("RJavaClassLoader.findClass(\"" + name + "\")");
}
final String classFileName = classNameToFile(name);
InputStream ins = null;
for (final Enumeration<UnixFile> e = this.classPath.elements(); e.hasMoreElements();) {
final UnixFile cp = e.nextElement();
if (verbose) {
System.out.println(" - trying class path \"" + cp + "\"");
}
try {
ins = null;
if (cp instanceof UnixJarFile) {
ins = ((UnixJarFile) cp).getResourceAsStream(classFileName);
if (verbose) {
System.out.println(" JAR file, can get '" + classFileName + "'? " + ((ins != null) ? "YES" : "NO"));
}
} else if (cp instanceof UnixDirectory) {
final UnixFile class_f = new UnixFile(cp.getPath()+'/'+classFileName);
if (class_f.isFile()) {
ins = new FileInputStream(class_f);
}
if (verbose) {
System.out.println(" Directory, can get '" + classFileName + "'? " + ((ins != null) ? "YES" : "NO"));
}
}
if (ins != null) {
int al = 128 * 1024;
byte fc[] = new byte[al];
int n = ins.read(fc);
int rp = n;
// System.out.println(" loading class file, initial n =
// "+n);
while (n > 0) {
if (rp == al) {
int nexa = al * 2;
if (nexa < 512 * 1024) {
nexa = 512 * 1024;
}
final byte la[] = new byte[nexa];
System.arraycopy(fc, 0, la, 0, al);
fc = la;
al = nexa;
}
n = ins.read(fc, rp, fc.length - rp);
// System.out.println(" next n = "+n+" (rp="+rp+",
// al="+al+")");
if (n > 0) {
rp += n;
}
}
ins.close();
n = rp;
if (verbose) {
System.out.println("RJavaClassLoader: loaded class " + name + ", " + n + " bytes");
}
try {
cl = defineClass(name, fc, 0, n);
} catch (final Exception dce) {
throw new ClassNotFoundException("Class not found - candidate class binary found but could not be loaded", dce);
}
if (verbose) {
System.out.println(" defineClass('" + name +"') returned " + cl);
}
}
} catch (final ClassNotFoundException ex) {
throw ex;
} catch (final Exception ex) {
// System.out.println(" * won't work: "+ex.getMessage());
}
}
}
// System.out.println("=== giving up");
if (cl == null) {
throw (new ClassNotFoundException(name));
}
return cl;
}
@Override
public URL findResource(final String name) {
if (verbose) {
LOGGER.entering("RJavaClassLoader", "findResource", name);
}
if (this.useSystem) {
try {
final URL u = super.findResource(name);
if (u != null) {
if (verbose) {
LOGGER.log(Level.FINE, "Found resource '"+name+"' at '"+ u + "' using URL loader.");
}
return u;
}
}
catch (final Exception e) {
}
}
if (this.useSecond) {
for (final Enumeration<UnixFile> e = this.classPath.elements(); e.hasMoreElements();) {
final UnixFile cp = e.nextElement();
try {
if (cp instanceof UnixJarFile) {
final URL u = ((UnixJarFile) cp).getResource(name) ;
if (u != null) {
if (verbose) {
System.out.println(" - found in a JAR file, URL " + u);
}
return u;
}
} else if (cp instanceof UnixDirectory) {
final UnixFile res_f = new UnixFile(cp.getPath() + "/" + name);
if (res_f.isFile()) {
if (verbose) {
System.out.println(" - find as a file: "+res_f);
}
return res_f.toURL();
}
}
} catch (final Exception iox) {
}
}
}
return null;
}
/** add a library to path mapping for a native library */
@Override
public void addRLibrary(final String name, final String path) {
this.libMap.put(name, new UnixFile(path));
}
@Override
public void addClassPath(final String cp) {
final UnixFile f = new UnixFile(cp);
addClassPath(f);
}
public void addClassPath(final UnixFile f) {
if (this.useSystem) {
try {
addURL(f.toURI().toURL());
if (verbose) {
LOGGER.log(Level.FINE, "Added '"+f.getPath()+"' to classpath of URL loader");
}
// return; // we need to add it anyway
}
catch (final Exception e) {
}
}
if (this.useSecond) {
UnixFile g = null;
if (f.isFile() && (f.getName().endsWith(".jar") || f.getName().endsWith(".JAR"))) {
g = new UnixJarFile(f.getPath());
if (verbose) {
System.out.println("RJavaClassLoader: adding Java archive file '"+g+"' to the internal class path");
}
} else if (f.isDirectory()) {
g = new UnixDirectory(f.getPath());
if (verbose) {
System.out.println("RJavaClassLoader: adding class directory '"+g+"' to the internal class path");
}
} else if (verbose) {
System.err.println(f.exists() ?
("WARNING: the path '"+f+"' is neither a directory nor a JAR file, it will NOT be added to the internal class path!") :
("WARNING: the path '"+f+"' does NOT exist, it will NOT be added to the internal class path!") );
}
if (g != null && !this.classPath.contains(g)) {
this.classPath.add(g);
System.setProperty("java.class.path",
System.getProperty("java.class.path")+File.pathSeparator+g.getPath());
}
}
}
/**
* Adds multiple entries to the class path
*/
public void addClassPath(final String[] cp) {
int i = 0;
while (i < cp.length) {
addClassPath(cp[i++]);
}
}
public void addClassPath(final List<String> cpList) {
for (final String path : cpList) {
addClassPath(path);
}
}
/**
* @return the array of class paths used by this class loader
*/
public String[] getClassPath() {
final List<String> list = new ArrayList<>();
if (this.useSystem) {
final URL[] urls = getURLs();
for (final URL url : urls) {
list.add(url.toString());
}
}
if (this.useSecond) {
final UnixFile[] files = this.classPath.toArray(new UnixFile[list.size()]);
for (final UnixFile file : files) {
list.add(file.getPath());
}
}
return list.toArray(new String[list.size()]);
}
@Override
protected String findLibrary(final String name) {
if (verbose) {
LOGGER.entering("RJavaClassLoader", "findLibrary", name);
}
// if (name.equals("rJava"))
// return rJavaLibPath+"/"+name+".so";
final UnixFile u = this.libMap.get(name);
String s = null;
if (u != null && u.exists()) {
s = u.getPath();
}
if (verbose) {
LOGGER.log(Level.FINE, "Mapping to " + ((s == null) ? "<none>" : s));
}
return s;
}
public Class<?> loadRJavaClass(final String name) throws ClassNotFoundException {
final Class<?> clazz = findClass(name);
resolveClass(clazz);
return clazz;
}
}