JNI.java
package emissary.jni;
import emissary.config.ConfigUtil;
import emissary.config.Configurator;
import emissary.core.EmissaryException;
import emissary.core.Namespace;
import emissary.core.NamespaceException;
import emissary.directory.DirectoryEntry;
import emissary.directory.DirectoryPlace;
import emissary.directory.IDirectoryPlace;
import emissary.directory.KeyManipulator;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Provide methods for retrieving native libraries from the repository The main entry point is loadLibrary. Places
* wishing to loadLibraries from the JniRepositoryPlace should include this class via composition and invoke
* this.loadLibrary rather than System.loadLibrary()
*/
@SuppressWarnings("AvoidObjectArrays")
public class JNI implements Serializable {
static final long serialVersionUID = 3037911106823343480L;
/**
* A handle to the DirectoryPlace
*/
private transient IDirectoryPlace theDir;
/**
* The mappings for SharedPrefix on native libraries
*/
private final Map<String, String> sharedPrefix = new HashMap<>();
/**
* The mappings for SharedSuffixes on native libraries
*/
private final Map<String, String> sharedSuffix = new HashMap<>();
/**
* The mappings for the correct version of each native library
*/
private final Map<String, String> libVersions = new HashMap<>();
/**
* The location we all agree to save library files retrieved from the repository osname-dependently
*/
private final Map<String, String> savePath = new HashMap<>();
/**
* Handle to the configG gives access to the config file entries
*/
private final Configurator configG;
protected static final Logger logger = LoggerFactory.getLogger(JNI.class);
/**
* Public constructor when used as a utility class
*/
public JNI() throws IOException {
this.configG = ConfigUtil.getConfigInfo(JNI.class);
configurePlace();
}
/**
* Public constructor args are easy when called from ServiceProviderPlace
*/
public JNI(@Nullable final String theDir, final Configurator configG) {
if (theDir != null) {
try {
this.theDir = (IDirectoryPlace) Namespace.lookup(theDir);
} catch (NamespaceException ne) {
logger.debug("Cannot get directory using {}: {}", theDir, ne.getLocalizedMessage());
}
}
if (this.theDir == null) {
try {
this.theDir = DirectoryPlace.lookup();
} catch (EmissaryException ex) {
logger.debug("Unable to lookup default directory", ex);
}
}
this.configG = configG;
configurePlace();
}
/**
* Configure the place based on the current {@link #configG} setting.
*/
private void configurePlace() {
if (this.configG == null) {
return;
}
List<String> parms = this.configG.findEntries("SHARED_PREFIX");
for (final String entry : parms) {
final int ndx = entry.indexOf(':');
if (ndx == -1) {
logger.warn("Invalid SHARED_PREFIX: {}", entry);
continue;
}
final String arch = entry.substring(0, ndx);
String prefix = "";
if (ndx < entry.length() - 1) {
prefix = entry.substring(ndx + 1);
}
this.sharedPrefix.put(arch, prefix);
// Get the osname-dependent SAVE_PATH
final List<String> iparms = this.configG.findEntries(arch + "_LIBRARY_SAVE_PATH");
if (CollectionUtils.isNotEmpty(iparms)) {
this.savePath.put(arch, iparms.get(0));
}
}
parms = this.configG.findEntries("SHARED_SUFFIX");
for (final String entry : parms) {
final int ndx = entry.indexOf(':');
if (ndx == -1) {
logger.warn("Invalid SHARED_SUFFIX: {}", entry);
continue;
}
final String arch = entry.substring(0, ndx);
String suffix = "";
if (ndx < entry.length() - 1) {
suffix = entry.substring(ndx + 1);
}
this.sharedSuffix.put(arch, suffix);
}
parms = this.configG.findEntries("LIBRARY_VERSION");
for (final String entry : parms) {
final int ndx = entry.indexOf(':');
if (ndx == -1) {
logger.warn("Invalid LIBRARY_VERSION: {}", entry);
continue;
}
final String lib = entry.substring(0, ndx);
String ver = "";
if (ndx < entry.length() - 1) {
ver = entry.substring(ndx + 1);
}
this.libVersions.put(lib, ver);
}
if (logger.isDebugEnabled()) {
logger.debug("JNI config save paths {}", this.savePath);
logger.debug("JNI config prefixes {}", this.sharedPrefix);
logger.debug("JNI config suffixes {}", this.sharedSuffix);
logger.debug("JNI config versions {}", this.libVersions);
}
}
/**
* Expand the root name of the library by the architecture and version constants.
*/
private String expandLibraryName(final String name) {
final String osarch = System.getProperty("os.arch").replace(' ', '_');
final String osname = System.getProperty("os.name").replace(' ', '_');
// String osver = System.getProperty("os.version").replace(' ','_');
String libver = this.libVersions.get(name);
final String sep = "-";
if (libver == null) {
libver = "1.0";
}
return name + sep + // foo-
osname + sep + // Solaris-
osarch + sep + // sparc-
// osver+sep+ // 2.x-
libver; // v3.2
}
/**
* Expand the full library name with the way it is stored in the file system. This method expects something like
* foo-Solaris-sparc-2.x-v3.2 and returns libfoo-Solaris-sparc-2.x-v3.2.so
*/
private String filesystemLibraryName(final String name) {
final String osname = System.getProperty("os.name").replace(' ', '_');
String prefix = this.sharedPrefix.get(osname);
String suffix = this.sharedSuffix.get(osname);
if (prefix == null) {
prefix = "";
}
if (suffix == null) {
suffix = "";
}
return prefix + name + suffix;
}
/**
* Load a native library, finding it if it isn't already here. First try to load the library and catch any exception.
* Try to ask the directory place where the JniRepositoryPlace is and see if the repository has the library we are
* looking for. If it comes back, save it to disk in the agreed upon location and then load it in the regular way. Throw
* an UnsatisfiedLinkError if this doesn't work.
* <p>
* Note that System.loadLibrary expects the name as it comes back from expandLibraryName while the repository will
* respond to the name as it comes from filesystemLibraryName.
*
* @throws UnsatisfiedLinkError If it fails to load.
*/
public void loadLibrary(final String lib) {
final String libname = expandLibraryName(lib);
final String filename = filesystemLibraryName(libname);
final String osname = System.getProperty("os.name").replace(' ', '_');
final String theLocation = this.savePath.get(osname);
final String fullPathName = theLocation + File.separator + filename;
logger.debug("In JNI.loadLibrary({})", lib);
logger.debug("loading library: {}", libname);
// Try it on the LD_LIBRARY_PATH or equivalent
try {
System.loadLibrary(libname);
return;
} catch (UnsatisfiedLinkError e) {
final String syspath = System.getProperty("java.library.path", "<none>");
logger.debug("Unable to link local {} from incoming {} using system path {}", libname, lib, syspath, e);
}
// Try it in the save area, in case it's not on the LD_LIBRARY_PATH
try {
System.load(fullPathName);
return;
} catch (UnsatisfiedLinkError e) {
logger.debug("Unable to link abs path {} from incoming {}", fullPathName, lib, e);
}
// Retrieve the File and dependencies.
// errorMsg valid only when return is false
final String[] errorMsg = new String[1];
// Retrieve everything listed as a dependency of the requested library.
// If the library was obtained via System.loadLibrary() or System.load()
// above then we assume this has already been done (by a previous
// incantation of this routine) and not needed again. Note there is
// therefor NO versioning capability of the dependent libraries without
// jacking the version number for the actual library being requested.
if (!retrieveDependencies(lib, errorMsg)) {
logger.debug("Unable to retrieve dependencies:{}", errorMsg[0]);
throw new UnsatisfiedLinkError("Unable to retrieve dependencies for " + filename + " : " + errorMsg[0]);
}
if (!retrieveFile(filename, errorMsg)) {
logger.debug("Unable to retrieve:{}", errorMsg[0]);
throw new UnsatisfiedLinkError("Unable to retrieve " + filename + " : " + errorMsg[0]);
}
// Loadlib the file we just retrieved and saved
try {
System.load(fullPathName);
logger.debug("LINK SUCCESS for {}", fullPathName);
} catch (UnsatisfiedLinkError e) {
logger.debug("Unable to link retrieved {}:{}", fullPathName, e.getLocalizedMessage());
// We have done all we can. Throw an exception and return
throw new UnsatisfiedLinkError("Cannot link with retrieved library " + fullPathName + ":" + e);
}
}
/**
* Retrieve all the dependencies of a given library. For example if the incoming libname is foo, then the full path of
* the lib might be libfoo-Solaris-:-2.x-v1.2.so and the dependencies would be listed in the config file as DEP_foo =
* "libdep1.so" DEP_foo = "libdep2.so"
*/
private boolean retrieveDependencies(final String libname, final String[] errmsg) {
final List<String> deps = this.configG.findEntries("DEP_" + libname);
// See if anything needs to be retrieved
if ((deps == null) || deps.isEmpty()) {
return true;
}
// Use this string on error
final String myErr = libname + " dependencies are " + deps;
for (final String file : deps) {
logger.debug("JNI: Retrieving dependent file {}", file);
if (!retrieveFile(file, errmsg)) {
errmsg[0] = myErr + ": failed on " + file;
return false;
}
}
return true;
}
/**
* Actually retrieve the byteStream of the native library over the network and save it to a diskfile.
*/
public boolean retrieveFile(final String filename, final String[] errmsg) {
final String osname = System.getProperty("os.name").replace(' ', '_');
final String theLocation = this.savePath.get(osname);
final String fullPathName = theLocation + File.separator + filename;
final byte[] libContents = returnFile(filename, errmsg);
if (libContents == null) {
return false;
}
// Save the contents into the disk file
try (FileOutputStream fos = new FileOutputStream(fullPathName);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
bos.write(libContents, 0, libContents.length);
} catch (IOException ioe) {
errmsg[0] = "Cannot write retrieved JNI library to " + fullPathName + ": " + ioe;
return false;
}
return true;
}
/**
* Look up the repository using our directory and retrieve the bytestream over the network with a synchronous call.
*/
@Nullable
public byte[] returnFile(final String filename, final String[] errmsg) {
if (this.theDir == null) {
// Probably instantiated from a static main standalone constructor
// which should have called returnFile(String,String[],String)
// instead and specified the key
errmsg[0] = "No DirectoryPlace available";
return null;
}
try {
final List<DirectoryEntry> entries = this.theDir.nextKeys("JNI", null, null);
if (CollectionUtils.isEmpty(entries)) {
errmsg[0] = "No JNI place in directory for:" + filename;
return null;
}
// Just use the first one
final DirectoryEntry entry = entries.get(0);
String repositoryKey = entry.getKey();
// No related place, try a bootstrapping repository
if (repositoryKey == null) {
repositoryKey = System.getProperty("emissary.repository");
}
// No repository at all. Bail out
if (repositoryKey == null) {
errmsg[0] = "JNI.returnFile: cannot retrieve files " + "without a repository.";
return null;
}
// We have a repository of some sort, try using it
return returnFile(filename, errmsg, repositoryKey);
} catch (RuntimeException ve) {
errmsg[0] = "JNI.returnFile: " + ve;
return null;
}
}
/**
* Retrieve from the Repository with the specified Key. Can be used for bootstrapping when config files for directories
* and Repositories might not exist. A non-related repository can be specified in this case and the system will
* bootstrap from it.
*/
@Nullable
public byte[] returnFile(final String filename, final String[] errmsg, final String repositoryKey) {
final String repositoryAddrString = KeyManipulator.getServiceLocation(repositoryKey);
IJniRepositoryPlace repositoryProxy;
try {
final String look = repositoryAddrString.substring(repositoryAddrString.indexOf("//"));
repositoryProxy = (IJniRepositoryPlace) Namespace.lookup(look);
} catch (NamespaceException | RuntimeException e) {
errmsg[0] = "JNI.returnFile: " + e;
return null;
}
// Ask the repository to send the byte stream of the native
// library contents
final byte[] libContents;
try {
libContents = repositoryProxy.nativeLibraryDeliver(filename);
} catch (RemoteException | RuntimeException e) {
errmsg[0] = "Error calling nativeLibraryDeliver: " + e;
return null;
}
if (libContents == null) {
errmsg[0] = "Unsuccessful request to repository: " + "got zero bytes";
}
return libContents;
}
/**
* Provide access to this OS's default save path
*/
public String getSavePath() {
final String osname = System.getProperty("os.name").replace(' ', '_');
return this.savePath.get(osname);
}
/**
* Provide access to the remote file timestamp
*/
public long lastModified(final String filename, final String[] errmsg, final String repositoryKey) {
long stamp = 0L;
String repositoryAddrString = KeyManipulator.getServiceLocation(repositoryKey);
if (StringUtils.contains(repositoryAddrString, "//")) {
repositoryAddrString = repositoryAddrString.substring(repositoryAddrString.indexOf("//"));
}
IJniRepositoryPlace repositoryProxy;
try {
repositoryProxy = (IJniRepositoryPlace) Namespace.lookup(repositoryAddrString);
} catch (NamespaceException ne) {
errmsg[0] = "JNI.returnFile: " + ne;
return stamp;
}
// Ask the repository to send the timestamp
try {
stamp = repositoryProxy.lastModified(filename);
} catch (RuntimeException e) {
errmsg[0] = "Error calling lastModified: " + e;
}
return stamp;
}
/**
* Static main used from Makefiles to produce the correct expanded name of the library. Guaranteed to match when the
* calling program asks for it via the repository mechanism
*/
public static void main(final String[] args) throws IOException {
if (args.length < 1) {
logger.info("usage: java JNI [-L LEVEL] libname");
return;
}
int argpos = 0;
while ((argpos < args.length) && args[argpos].startsWith("-")) {
if ("--".equals(args[argpos])) {
argpos++;
break;
}
}
final JNI jni = new JNI();
String path = jni.getSavePath();
if (path != null && !path.endsWith("/")) {
path += "/";
}
for (int i = argpos; i < args.length; i++) {
logger.info("{}{}", path, jni.filesystemLibraryName(jni.expandLibraryName(args[i])));
}
}
}