From 38596534bee2181c15eeecca78aaae059756f058 Mon Sep 17 00:00:00 2001 From: netmikey Date: Thu, 30 Aug 2012 14:13:04 +0200 Subject: [PATCH] Initial code commit. --- .project | 16 ++ build.gradle | 14 + .../cvscanner/ClassVersionScanner.java | 57 ++++ .../com/netmikey/cvscanner/JavaVersion.java | 61 +++++ .../java/com/netmikey/cvscanner/Scanner.java | 259 ++++++++++++++++++ 5 files changed, 407 insertions(+) create mode 100644 .project create mode 100644 build.gradle create mode 100644 src/main/java/com/netmikey/cvscanner/ClassVersionScanner.java create mode 100644 src/main/java/com/netmikey/cvscanner/JavaVersion.java create mode 100644 src/main/java/com/netmikey/cvscanner/Scanner.java diff --git a/.project b/.project new file mode 100644 index 0000000..ff24d17 --- /dev/null +++ b/.project @@ -0,0 +1,16 @@ + + + classversion-scanner + + + + org.eclipse.jdt.core.javanature + + + + org.eclipse.jdt.core.javabuilder + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..499b8ba --- /dev/null +++ b/build.gradle @@ -0,0 +1,14 @@ + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'application' + +mainClassName = "com.netmikey.cvscanner.ClassVersionScanner" + +repositories { + mavenCentral() +} + +dependencies { + compile (group: 'commons-cli', name: 'commons-cli', version: '1.2') +} diff --git a/src/main/java/com/netmikey/cvscanner/ClassVersionScanner.java b/src/main/java/com/netmikey/cvscanner/ClassVersionScanner.java new file mode 100644 index 0000000..bce1fe5 --- /dev/null +++ b/src/main/java/com/netmikey/cvscanner/ClassVersionScanner.java @@ -0,0 +1,57 @@ +package com.netmikey.cvscanner; + +import java.io.File; +import java.util.logging.Level; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.PosixParser; + +/** + * Main class for running the Class Version Scanner. + * + * @author netmikey + */ +public class ClassVersionScanner { + /** + * Main method. + * + * @param argv + * Command-Line arguments. + * @throws Exception + * Something went horribly wrong. + */ + public static void main(String[] argv) throws Exception { + // Command line stuff... + Options opt = new Options(); + opt.addOption("d", "dir", true, "the root directory from which to search for class files and java archives " + + "(if not set, the current working directory will be used)."); + opt.addOption("n", "newer", true, "only look for class files compiled for the specified JRE and newer"); + opt.addOption("o", "older", true, "only look for class files compiled for the specified JRE and older"); + // I know java logging can be configured in an "awesome" *cough* way, + // but srsly, nobody wants to do that for a little command line tool + // like this... + opt.addOption("v", "verbose", false, "display more processing info"); + opt.addOption("h", "help", false, "display this help info"); + + CommandLineParser parser = new PosixParser(); + CommandLine cmd = parser.parse(opt, argv); + + if (cmd.hasOption("help")) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(ClassVersionScanner.class.getName(), opt); + return; + } + + Scanner scanner = new Scanner(); + File baseDir = new File(cmd.hasOption("dir") ? cmd.getOptionValue("dir") : System.getProperty("user.dir")); + scanner.setMinVersion(Scanner.VERSIONS.get(cmd.getOptionValue("newer"))); + scanner.setMaxVersion(Scanner.VERSIONS.get(cmd.getOptionValue("older"))); + if (cmd.hasOption("verbose")) { + scanner.setFineLevel(Level.INFO); + } + scanner.scanForVersion(baseDir); + } +} diff --git a/src/main/java/com/netmikey/cvscanner/JavaVersion.java b/src/main/java/com/netmikey/cvscanner/JavaVersion.java new file mode 100644 index 0000000..37fb466 --- /dev/null +++ b/src/main/java/com/netmikey/cvscanner/JavaVersion.java @@ -0,0 +1,61 @@ +package com.netmikey.cvscanner; + +import java.util.Arrays; +import java.util.List; + +/** + * Wraps the required metadata of a java version/release. + * + * @author netmikey + */ +public class JavaVersion { + private int version; + + private List aliases; + + private long classFileVersion; + + /** + * Default constructor. + * + * @param version + * The numeric value of the java version. + * @param classFileVersion + * The class file version that corresponds to this java version. + * @param aliases + * A list of aliases that match this version. + */ + public JavaVersion(int version, long classFileVersion, String... aliases) { + this.version = version; + this.classFileVersion = classFileVersion; + this.aliases = Arrays.asList(aliases); + } + + /** + * Get the version. + * + * @return Returns the version. + */ + public int getVersion() { + return version; + } + + /** + * Get the aliases. + * + * @return Returns the aliases. + */ + public List getAliases() { + return aliases; + } + + /** + * Get the classFileVersion. + * + * @return Returns the classFileVersion. + */ + public long getClassFileVersion() { + return classFileVersion; + } + +} diff --git a/src/main/java/com/netmikey/cvscanner/Scanner.java b/src/main/java/com/netmikey/cvscanner/Scanner.java new file mode 100644 index 0000000..034c2aa --- /dev/null +++ b/src/main/java/com/netmikey/cvscanner/Scanner.java @@ -0,0 +1,259 @@ +package com.netmikey.cvscanner; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Encapsulates the actual class-version scanner logic. + * + * @author netmikey + */ +public class Scanner { + private static final Logger LOGGER = Logger.getLogger(Scanner.class.getName()); + + /* package */static final Map VERSIONS; + + private static final Map CLASS_VERSIONS; + + static { + JavaVersion[] jVersions = new JavaVersion[] { new JavaVersion(1, 45, "1", "1.1"), + new JavaVersion(2, 46, "2", "1.2"), new JavaVersion(3, 47, "3", "1.3"), + new JavaVersion(4, 48, "4", "1.4"), new JavaVersion(5, 49, "5", "1.5", "5.0"), + new JavaVersion(6, 50, "6", "1.6", "6.0"), new JavaVersion(7, 51, "7", "1.7", "7.0") }; + + CLASS_VERSIONS = new HashMap(); + VERSIONS = new HashMap(); + for (JavaVersion version : jVersions) { + CLASS_VERSIONS.put(version.getClassFileVersion(), version); + for (String versionAlias : version.getAliases()) { + VERSIONS.put(versionAlias, version); + } + } + } + + private JavaVersion minVersion; + + private JavaVersion maxVersion; + + private Level fineLevel = Level.FINE; + + /** + * Main entry method to start scanning according to the current scanner + * configuration. + * + * @param baseDir + * The basedir to start scanning from. + * @throws IOException + * Something went horribly wrong. + */ + public void scanForVersion(File baseDir) throws IOException { + LOGGER.log(fineLevel, "Scanning directory: " + baseDir); + + if (!baseDir.isDirectory()) { + return; + } + + File[] classFiles = baseDir.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return !file.isDirectory() && isClassFileName(file.getName()); + } + }); + + File[] archiveFiles = baseDir.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return !file.isDirectory() && isArchiveName(file.getName()); + } + }); + + for (File archiveFile : archiveFiles) { + this.processArchiveFile(archiveFile); + } + + for (File classFile : classFiles) { + this.processClassFile(classFile); + } + + File[] subDirs = baseDir.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return file.isDirectory(); + } + }); + + for (File subDir : subDirs) { + scanForVersion(subDir); + } + } + + private void processClassFile(InputStream inputStream, String filename) throws IOException { + byte[] versionBytes = new byte[2]; + inputStream.skip(6); + inputStream.read(versionBytes); + + long decimalVersion = 0; + for (int i = 0; i < versionBytes.length; i++) { + decimalVersion = (decimalVersion << 8) + (versionBytes[i] & 0xff); + } + + boolean filtered = false; + + if (minVersion != null && decimalVersion < minVersion.getClassFileVersion()) { + filtered = true; + } + if (maxVersion != null && decimalVersion > maxVersion.getClassFileVersion()) { + filtered = true; + } + + if (!filtered) { + JavaVersion jVersion = CLASS_VERSIONS.get(decimalVersion); + String version = jVersion == null ? "(Unknown Classfile Version " + decimalVersion + ")" : String + .valueOf(jVersion.getVersion()); + LOGGER.info("Found matching class file: " + filename + " compiled for Java version: " + version); + } + } + + private void processClassFile(File classFile) throws IOException { + LOGGER.log(fineLevel, "Processing class file: " + classFile); + + try { + InputStream fis = new FileInputStream(classFile); + this.processClassFile(fis, classFile.getPath()); + fis.close(); + } catch (IOException e) { + System.err.println("Error trying to process class file " + classFile + ": " + e.getMessage()); + throw e; + } + } + + private void processArchiveFile(File archiveFile) throws IOException { + LOGGER.log(fineLevel, "Processing archive file: " + archiveFile); + ZipInputStream zis = new ZipInputStream(new FileInputStream(archiveFile)); + processArchiveFile(zis, archiveFile.getPath()); + zis.close(); + } + + /** + * This method was inspired by Layton Smith's ZipReader posted on: + * http://stackoverflow + * .com/questions/5075615/java-searching-inside-zips-inside-zips + * + * @param zis + * @param archivePath + * @throws IOException + */ + private void processArchiveFile(final ZipInputStream zis, String archivePath) throws IOException { + InputStream zipReader = new InputStream() { + @Override + public int read() throws IOException { + if (zis.available() > 0) { + return zis.read(); + } else { + return -1; + } + } + + @Override + public void close() throws IOException { + zis.close(); + } + }; + + ZipEntry zipEntry; + while ((zipEntry = zis.getNextEntry()) != null) { + if (!zipEntry.isDirectory()) { + if (isClassFileName(zipEntry.getName())) { + LOGGER.log(fineLevel, "Processing class file: " + archivePath + "/" + zipEntry.getName()); + processClassFile(zipReader, archivePath + "/" + zipEntry.getName()); + } + + if (isArchiveName(zipEntry.getName())) { + LOGGER.log(fineLevel, "Recursing into: " + zipEntry.getName()); + ZipInputStream inner = new ZipInputStream(zipReader); + processArchiveFile(inner, archivePath + "/" + zipEntry.getName()); + } + } + } + } + + private boolean isClassFileName(String filename) { + return filename.toLowerCase().endsWith(".class"); + } + + private boolean isArchiveName(String filename) { + String[] suffixes = new String[] { ".jar", ".ear", ".war", ".sar", ".rar" }; + for (String suffix : suffixes) { + if (filename.toLowerCase().endsWith(suffix)) { + return true; + } + } + return false; + } + + /** + * Get the minVersion. + * + * @return Returns the minVersion. + */ + public JavaVersion getMinVersion() { + return minVersion; + } + + /** + * Set the minVersion. + * + * @param minVersion + * The minVersion to set. + */ + public void setMinVersion(JavaVersion minVersion) { + this.minVersion = minVersion; + } + + /** + * Get the maxVersion. + * + * @return Returns the maxVersion. + */ + public JavaVersion getMaxVersion() { + return maxVersion; + } + + /** + * Set the maxVersion. + * + * @param maxVersion + * The maxVersion to set. + */ + public void setMaxVersion(JavaVersion maxVersion) { + this.maxVersion = maxVersion; + } + + /** + * Get the fineLevel. + * + * @return Returns the fineLevel. + */ + public Level getFineLevel() { + return fineLevel; + } + + /** + * Set the fineLevel. + * + * @param fineLevel + * The fineLevel to set. + */ + public void setFineLevel(Level fineLevel) { + this.fineLevel = fineLevel; + } +}