//
// Copyright (C) 2012 United States Government as represented by the
// Administrator of the National Aeronautics and Space Administration
// (NASA). All Rights Reserved.
//
// This software is distributed under the NASA Open Source Agreement
// (NOSA), version 1.3. The NOSA has been approved by the Open Source
// Initiative. See the file NOSA-1.3-JPF at the top of the distribution
// directory tree for the complete NOSA document.
//
// THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY
// KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT
// LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL CONFORM TO
// SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR
// A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT
// THE SUBJECT SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT
// DOCUMENTATION, IF PROVIDED, WILL CONFORM TO THE SUBJECT SOFTWARE.
//
package gov.nasa.jpf.conformanceChecker.providers;
import static gov.nasa.jpf.conformanceChecker.util.MaybeVersion.*;
import gov.nasa.jpf.Config;
import gov.nasa.jpf.JPF;
import gov.nasa.jpf.classfile.ClassFile;
import gov.nasa.jpf.classfile.ClassFileException;
import gov.nasa.jpf.conformanceChecker.util.MaybeVersion;
import gov.nasa.jpf.jvm.ClassInfo;
import gov.nasa.jpf.jvm.JVM;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class gives access to standard library classes located in
* system jar files.
*
* The entry point of this class is method {@link #loadClassInfo(int, String)}.
* With this method a {@link ClassInfo} object is loaded for the
* specified java version of the class.
*
* This class uses the following properties in the project's
* jpf configuration file:
*
* - conformance-checker.java5path
* - conformance-checker.java6path
* - conformance-checker.java7path
* - conformance-checker.libJars
*
* that list the path to the directory containing the system
* library jars for java 5, 6 ant 7 respectively and a list
* of third party libraries.
*
* ClassInfo objects returned by this class are not registered
* to the JPF JVM.
*
* @author Matteo Ceccarello <matteo.ceccarello AT gmail.com>
*
*/
public class StandardClassProvider {
static {
// FIXME this is the hack needed to have static fields in JVM class
// initialized, so that ClassInfo does not throw NullPointer
// when initialized.
if(JVM.getVM() == null)
new JVM(null, new Config(new String[0]));
}
@SuppressWarnings("serial")
public static class UnsupportedJavaVersionException extends RuntimeException {
public UnsupportedJavaVersionException() {
super();
}
public UnsupportedJavaVersionException(String message){
super(message);
}
}
public static final String JAVA_5_PATH_PROP = "conformance-checker.java5path";
public static final String JAVA_6_PATH_PROP = "conformance-checker.java6path";
public static final String JAVA_7_PATH_PROP = "conformance-checker.java7path";
public static final String EXT_LIBS_PROP = "conformance-checker.libJars";
public static final Logger logger = JPF.getLogger(StandardClassProvider.class.getName());
private JarFile[] java5jars = {};
private JarFile[] java6jars = {};
private JarFile[] java7jars = {};
private JarFile[] extLibsJars = {};
/* caches */
private Map<String, ClassInfo> java5cache = new HashMap<String, ClassInfo>();
private Map<String, ClassInfo> java6cache = new HashMap<String, ClassInfo>();
private Map<String, ClassInfo> java7cache = new HashMap<String, ClassInfo>();
private Map<String, ClassInfo> extLibscache = new HashMap<String, ClassInfo>();
/**
* Builds a new {@link StandardClassProvider} that tries
* to guess the location of all available system
* libraries, for all installed Java versions.
*
* If there is a jpf config file available, then
* properties are read from there.
*/
public StandardClassProvider() {
this(new Config(new String[]{}));
}
/**
* Builds a new {@link StandardClassProvider} that tries
* to guess the location of all available system
* libraries, for all installed Java versions.
*
* If the given {@link Config} object contains
* the appropriate options (see the class description)
* then java libraries are loaded from that paths.
*/
public StandardClassProvider(Config config) {
List<Short> remainingVersions = loadConfiguredVersions(config);
loadOtherVersionsJars(remainingVersions);
loadThirdPartyLibs(config);
}
/**
* Loads a {@link ClassInfo} object from the given java version.
*
* If the version is is {@link Maybe#NOTHING}, then the method
* looks into the external library array.
*
* If the class is not found, this method will return null. This may be
* caused by IO exceptions, {@link ClassFileException}s and other error
* conditions that generally means that the class is not located in the
* given jars. This should be interpreted as the fact that the requested
* class is not present in the java version requested.
*
* @param javaVersion the java version desired for the class
* @param className the class to look for
* @return a {@link ClassInfo} object representing the requested class if
* the class is found in the requested version, null otherwise.
*/
public ClassInfo loadClassInfo(MaybeVersion javaVersion, String className){
ClassInfo cached = null;
if(javaVersion.isNothing()) {
cached = extLibscache.get(className);
if(cached != null)
return cached;
cached = loadClassFromJarFiles(extLibsJars, className);
extLibscache.put(className, cached);
return cached;
}
switch(javaVersion.getJust()) {
case 5:
cached = java5cache.get(className);
if(cached != null)
return cached;
cached = loadClassFromJarFiles(java5jars, className);
java5cache.put(className, cached);
return cached;
case 6:
cached = java6cache.get(className);
if (cached != null)
return cached;
cached = loadClassFromJarFiles(java6jars, className);
java6cache.put(className, cached);
return cached;
case 7:
cached = java7cache.get(className);
if (cached != null)
return cached;
cached = loadClassFromJarFiles(java7jars, className);
java7cache.put(className, cached);
return cached;
default:
throw new UnsupportedJavaVersionException("Supported Java versions are 5, 6 and 7");
}
}
/**
* Loads a {@link ClassInfo} object, for all java versions.
* This method looks also into the external library paths
*
* If the class is not found, this method will return null. This may be
* caused by IO exceptions, {@link ClassFileException}s and other error
* conditions that generally means that the class is not located in the
* given jars. This should be interpreted as the fact that the requested
* class is not present in the java version requested.
*
* @param javaVersion the java version desired for the class
* @param className the class to look for
* @return a {@link Map} between java versions and
* {@link ClassInfo} objects.
*/
public Map<MaybeVersion, ClassInfo> loadClassInfo(String className){
Map<MaybeVersion, ClassInfo> map = new HashMap<MaybeVersion, ClassInfo>();
ClassInfo ext = loadClassInfo(nothing(), className);
if(ext != null) {
map.put(nothing(), ext);
}
ClassInfo ver5 = loadClassInfo(just(5), className);
if(ver5 != null)
map.put(just(5), ver5);
ClassInfo ver6 = loadClassInfo(just(6), className);
if(ver6 != null)
map.put(just(6), ver6);
ClassInfo ver7 = loadClassInfo(just(7), className);
if(ver7 != null)
map.put(just(7), ver7);
return map;
}
void loadThirdPartyLibs(Config config) {
File[] jarPaths = config.getPathArray(EXT_LIBS_PROP);
ArrayList<JarFile> jars = new ArrayList<JarFile>();
for (File file : jarPaths) {
try {
jars.add(new JarFile(file));
} catch (IOException e) {
logger.warning("failed to load jar file from " +
file.getAbsolutePath() + ", skipping. \n\t" + e.getMessage());
}
}
extLibsJars = jars.toArray(extLibsJars);
}
/**
* Loads the java versions explicitly specified in the
* given config and return a list of versions to load
* without the config help.
*
* If the option is configured as the empty string, then the program
* tries to find it itself, if it is null it is skipped
*/
List<Short> loadConfiguredVersions(Config config) {
List<Short> remainingVersions =
new ArrayList<Short>(Arrays.asList((short) 5, (short) 6, (short) 7));
String dir = config.getString(JAVA_5_PATH_PROP);
if(dir != null) {
if(!"".equals(dir)){
java5jars = loadFromDirIntoArray(dir);
if(java5jars.length > 0){
logger.fine("Successfully loaded Java 5 from "+dir);
remainingVersions.remove(new Short((short) 5));
}
}
} else remainingVersions.remove(new Short((short) 5)); // the version should be ignored
dir = config.getString(JAVA_6_PATH_PROP);
if(dir != null) {
if(!"".equals(dir)){
java6jars = loadFromDirIntoArray(dir);
if(java6jars.length > 0){
logger.fine("Successfully loaded Java 6 from "+dir);
remainingVersions.remove(new Short((short) 6));
}
}
} else remainingVersions.remove(new Short((short) 6)); // the version should be ignored
dir = config.getString(JAVA_7_PATH_PROP);
if(dir != null) {
if (!"".equals(dir)){
java7jars = loadFromDirIntoArray(dir);
if(java7jars.length > 0){
logger.fine("Successfully loaded Java 7 from "+dir);
remainingVersions.remove(new Short((short) 7));
}
}
} else remainingVersions.remove(new Short((short) 7)); // the version should be ignored
return remainingVersions;
}
JarFile[] loadFromDirIntoArray(String dir) {
JarFile[] jars;
File libDir = new File(dir);
if(libDir.exists()) {
File[] jarFiles = libDir.listFiles(
new FileFilter() {
public boolean accept(File pathname) {
return pathname.getName().endsWith(".jar");
}
});
if(jarFiles.length == 0) {
logger.warning("The directory " + dir + " does not contain" +
"jar files");
return new JarFile[0];
}
jars = new JarFile[jarFiles.length];
for (int i = 0; i < jars.length; i++) {
try {
jars[i] = new JarFile(jarFiles[i]);
} catch (IOException e) {
logger.warning("Error loading jar file: " +
jarFiles[i]);
}
}
return jars;
} else {
logger.warning("The directory " + dir + " does not exists");
return new JarFile[0];
}
}
/**
* Loads the libraries for other java version, specified by versionInUse and
* otherVersions.
*
* @param versionInUseDir
* @param versionInUse
* @param otherVersions
*/
void loadOtherVersionsJars(List<Short> otherVersions) {
logger.finer("Trying to load the following java versions: "+otherVersions);
String currentLibraryBaseDir = currentLibraryBaseDir();
short versionInUse = detectJavaVersion();
for (short ver : otherVersions) {
String dir = inferLibraryBaseDir(currentLibraryBaseDir, versionInUse, ver);
boolean success;
switch (ver) {
case 5:
java5jars = loadFromDirIntoArray(dir);
success = java5jars.length > 0;
break;
case 6:
java6jars = loadFromDirIntoArray(dir);
success = java6jars.length > 0;
break;
case 7:
java7jars = loadFromDirIntoArray(dir);
success = java7jars.length > 0;
break;
default:
throw new UnsupportedJavaVersionException("Supported Java versions are 5, 6 and 7");
}
if(success) {
logger.finer("Successfully loaded Java " + ver + " from " + dir);
} else {
logger.warning("Failed loading Java " + ver +" library. " +
"Maybe it is not installed on you system");
}
}
}
/**
* Loads a {@link ClassInfo} object from the given list of jar files.
*
* If the class is not found, this method will return null. This may be
* caused by IO exceptions, {@link ClassFileException}s and other error
* conditions that generally means that the class is not located in the
* given jars. This should be interpreted as the fact that the requested
* class is not present in the java version represented by the given
* jar files.
*
* @param jars the jar files to look into.
* @param className the class to look for
* @return a {@link ClassInfo} object representing the requested class if
* the class is found, null otherwise.
*/
ClassInfo loadClassFromJarFiles(JarFile[] jars, String className) {
// FIXME: check if on Windows the file separator inside jar files
// is / or \
String fileName = className.replace('.', '/') + ".class";
for (JarFile jar : jars) {
JarEntry entry = jar.getJarEntry(fileName);
if (entry != null) {
try {
logger.fine("Loaded " + className + " from jar file " + jar.getName());
byte[] classBytes = Util.getClassBytes(jar.getInputStream(entry));
ClassFile cf = new ClassFile(className, classBytes);
return new UnregisteredClassInfo(cf);
} catch (IOException e) {
logger.warning(e.getMessage());
return null;
} catch (ClassFileException e) {
logger.warning(e.getMessage());
return null;
}
}
}
return null;
}
public static short detectJavaVersion() {
String javaVersion = System.getProperty("java.version");
String[] versionNumber = javaVersion.split("\\.");
logger.finer("Java version currently in use is: " + javaVersion);
return Short.parseShort(versionNumber[1]);
}
/**
* Infers the path to the set of libraries for the
* Java version currently in use.
*
* Includes the last slash in the returned value
*/
static String currentLibraryBaseDir() {
String aJar = System.getProperty("sun.boot.class.path").split(":")[0];
String inferred = aJar.substring(0, aJar.lastIndexOf(File.separator)+1);
return inferred;
}
static String inferLibraryBaseDir(String versionInUseDir, short versionInUse, short wantedVersion) {
// FIXME in Sabayon Linux (Icedtea) this does not work.
// Java versions are in directories like /opt/icedtea-bin-7.2.1/jre/lib/
return versionInUseDir.replace("."+versionInUse+".", "."+wantedVersion+".");
}
/** small test main */
public static void main(String[] args) {
logger.setLevel(Level.ALL);
Map<MaybeVersion, ClassInfo> classes = new StandardClassProvider().loadClassInfo("java.io.File");
System.out.println(classes);
}
}