/* * FindBugs - Find Bugs in Java programs * Copyright (C) 2003-2007 University of Maryland * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package edu.umd.cs.findbugs.workflow; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import javax.annotation.Nonnull; import javax.annotation.WillClose; import org.apache.bcel.classfile.ClassParser; import org.apache.bcel.classfile.JavaClass; import edu.umd.cs.findbugs.FindBugs; import edu.umd.cs.findbugs.charsets.UTF8; import edu.umd.cs.findbugs.charsets.UserTextFile; import edu.umd.cs.findbugs.config.CommandLine; import edu.umd.cs.findbugs.util.ClassName; /** * @author William Pugh */ public class RejarClassesForAnalysis { static class RejarClassesForAnalysisCommandLine extends CommandLine { static class PatternMatcher { final Pattern[] pattern; PatternMatcher(String arg) { String[] p = arg.split(","); this.pattern = new Pattern[p.length]; for (int i = 0; i < p.length; i++) pattern[i] = Pattern.compile(p[i]); } public boolean matches(String arg) { for (Pattern p : pattern) if (p.matcher(arg).find()) return true; return false; } } static class PrefixMatcher { final String[] prefixes; PrefixMatcher(String arg) { this.prefixes = arg.split(","); } PrefixMatcher() { this.prefixes = new String[0]; } public boolean matches(String arg) { for (String p : prefixes) if (arg.startsWith(p)) return true; return false; } public boolean matchesEverything() { for (String p : prefixes) if (p.length() == 0) return true; return false; } } PrefixMatcher prefix = new PrefixMatcher(""); PrefixMatcher exclude = new PrefixMatcher(); PatternMatcher excludePatterns = null; int maxClasses = 29999; long maxAge = Long.MIN_VALUE; public String inputFileList; public String auxFileList; boolean onlyAnalyze = false; boolean ignoreTimestamps = false; RejarClassesForAnalysisCommandLine() { addSwitch("-analyzeOnly", "only read the jars files and analyze them; don't produce new jar files"); addOption("-maxAge", "days", "maximum age in days (ignore jar files older than this)"); addOption("-inputFileList", "filename", "text file containing names of jar files"); addOption("-auxFileList", "filename", "text file containing names of jar files for aux class path"); addOption("-maxClasses", "num", "maximum number of classes per analysis*.jar file"); addOption("-outputDir", "dir", "directory for the generated jar files"); addSwitch("-ignoreTimestamps", "ignore timestamps on zip entries; use first version found"); addOption("-prefix", "class name prefix", "comma separated list of class name prefixes that should be analyzed (e.g., edu.umd.cs.)"); addOption("-exclude", "class name prefix", "comma separated list of class name prefixes that should be excluded from both analyze and auxilary jar files (e.g., java.)"); addOption("-excludePattern", "class name pattern(s)", "comma separated list of regular expressions; all classes matching them are excluded"); } File outputDir = new File("."); /* * (non-Javadoc) * * @see * edu.umd.cs.findbugs.config.CommandLine#handleOption(java.lang.String, * java.lang.String) */ @Override protected void handleOption(String option, String optionExtraPart) throws IOException { if (option.equals("-analyzeOnly")) { onlyAnalyze = true; } else if (option.equals("-ignoreTimestamps")) { ignoreTimestamps = true; }else throw new IllegalArgumentException("Unknown option : " + option); } /* * (non-Javadoc) * * @see * edu.umd.cs.findbugs.config.CommandLine#handleOptionWithArgument(java * .lang.String, java.lang.String) */ @Override protected void handleOptionWithArgument(String option, String argument) throws IOException { if (option.equals("-prefix")) prefix = new PrefixMatcher(argument); else if (option.equals("-exclude")) exclude = new PrefixMatcher(argument); else if (option.equals("-inputFileList")) inputFileList = argument; else if (option.equals("-auxFileList")) auxFileList = argument; else if (option.equals("-maxClasses")) maxClasses = Integer.parseInt(argument); else if (option.equals("-maxAge")) maxAge = System.currentTimeMillis() - (24 * 60 * 60 * 1000L) * Integer.parseInt(argument); else if (option.equals("-outputDir")) outputDir = new File(argument); else if (option.equals("-excludePattern")) excludePatterns = new PatternMatcher(argument); else throw new IllegalArgumentException("Unknown option : " + option); } boolean skip(ZipEntry ze) { return ze.getSize() > 1000000; } } final RejarClassesForAnalysisCommandLine commandLine; final int argCount; final String[] args; public RejarClassesForAnalysis(RejarClassesForAnalysisCommandLine commandLine, int argCount, String[] args) { this.commandLine = commandLine; this.argCount = argCount; this.args = args; } public static void readFromStandardInput(Collection<String> result) throws IOException { readFrom(result, UserTextFile.bufferedReader(System.in)); } SortedMap<String, ZipOutputStream> analysisOutputFiles = new TreeMap<String, ZipOutputStream>(); public @Nonnull ZipOutputStream getZipOutputFile(String path) { ZipOutputStream result = analysisOutputFiles.get(path); if (result != null) return result; SortedMap<String, ZipOutputStream> head = analysisOutputFiles.headMap(path); String matchingPath = head.lastKey(); result = analysisOutputFiles.get(matchingPath); if (result == null) throw new IllegalArgumentException("No zip output file for " + path); return result; } public static void readFrom(Collection<String> result, @WillClose Reader r) throws IOException { BufferedReader in = new BufferedReader(r); while (true) { String s = in.readLine(); if (s == null) { in.close(); return; } result.add(s); } } int analysisCount = 1; int auxilaryCount = 1; String getNextAuxilaryFileOutput() { String result; if (auxilaryCount == 1) result = "auxilary.jar"; else result = "auxilary" + (auxilaryCount) + ".jar"; auxilaryCount++; System.out.println("Starting " + result); return result; } String getNextAnalyzeFileOutput() { String result; if (analysisCount == 1) result = "analyze.jar"; else result = "analyze" + (analysisCount) + ".jar"; analysisCount++; System.out.println("Starting " + result); return result; } /** For each file, give the latest timestamp */ Map<String, Long> copied = new HashMap<String, Long>(); /** While file should we copy it from */ Map<String, File> copyFrom = new HashMap<String, File>(); Set<String> excluded = new HashSet<String>(); TreeSet<String> filesToAnalyze = new TreeSet<String>(); int numFilesToAnalyze = 0; public static void main(String args[]) throws Exception { FindBugs.setNoAnalysis(); RejarClassesForAnalysisCommandLine commandLine = new RejarClassesForAnalysisCommandLine(); int argCount = commandLine.parse(args, 0, Integer.MAX_VALUE, "Usage: " + RejarClassesForAnalysis.class.getName() + " [options] [<jarFile>+] "); RejarClassesForAnalysis doit = new RejarClassesForAnalysis(commandLine, argCount, args); doit.execute(); } int auxilaryClassCount = 0; ZipOutputStream auxilaryOut; final byte buffer[] = new byte[8192]; private boolean exclude(String dottedName) { if (!Character.isJavaIdentifierStart(dottedName.charAt(0))) return true; if (commandLine.excludePatterns != null && commandLine.excludePatterns.matches(dottedName) || commandLine.exclude.matches(dottedName)) { excluded.add(dottedName); return true; } return false; } boolean classFileFound; public void execute() throws IOException { ArrayList<String> fileList = new ArrayList<String>(); if (commandLine.inputFileList != null) readFrom(fileList, UTF8.fileReader(commandLine.inputFileList)); else if (argCount == args.length) readFromStandardInput(fileList); else fileList.addAll(Arrays.asList(args).subList(argCount, args.length)); ArrayList<String> auxFileList = new ArrayList<String>(); if (commandLine.auxFileList != null) { readFrom(auxFileList, UTF8.fileReader(commandLine.auxFileList)); auxFileList.removeAll(fileList); } List<File> inputZipFiles = new ArrayList<File>(fileList.size()); List<File> auxZipFiles = new ArrayList<File>(auxFileList.size()); for (String fInName : fileList) { final File f = new File(fInName); if (f.lastModified() < commandLine.maxAge) { System.err.println("Skipping " + fInName + ", too old (" + new Date(f.lastModified()) + ")"); continue; } int oldSize = copied.size(); classFileFound = false; if (processZipEntries(f, new ZipElementHandler() { boolean checked = false; public void handle(ZipFile file, ZipEntry ze) throws IOException { if (commandLine.skip(ze)) return; String name = ze.getName(); String dottedName = name.replace('/', '.'); if (exclude(dottedName)) return; if (!checked) { checked = true; if (embeddedNameMismatch(file, ze)) { System.out.println("Class name mismatch for " + name + " in " + file.getName()); throw new ClassFileNameMismatch(); } } if (!commandLine.prefix.matches(dottedName)) return; classFileFound = true; long timestamp = ze.getTime(); Long oldTimestamp = copied.get(name); if (oldTimestamp == null) { copied.put(name, timestamp); copyFrom.put(name, f); filesToAnalyze.add(name); numFilesToAnalyze++; } else if (!commandLine.ignoreTimestamps && oldTimestamp < timestamp) { System.out.printf("Found later version of %s; switching from %s to %s%n", name, copyFrom.get(name), f); copied.put(name, timestamp); copyFrom.put(name, f); } } /** * @param dottedName * @return */ }) && oldSize < copied.size()) inputZipFiles.add(f); else if (classFileFound) System.err.println("Skipping " + fInName + ", no new classes found"); else System.err.println("Skipping " + fInName + ", no classes found"); } for (String fInName : auxFileList) { final File f = new File(fInName); if (f.lastModified() < commandLine.maxAge) { System.err.println("Skipping " + fInName + ", too old (" + new Date(f.lastModified()) + ")"); continue; } int oldSize = copied.size(); classFileFound = false; if (processZipEntries(f, new ZipElementHandler() { public void handle(ZipFile file, ZipEntry ze) { if (commandLine.skip(ze)) return; String name = ze.getName(); String dottedName = name.replace('/', '.'); if (!exclude(dottedName)) { classFileFound = true; long timestamp = ze.getTime(); Long oldTimestamp = copied.get(name); if (oldTimestamp == null) { copied.put(name, timestamp); copyFrom.put(name, f); } else if (!commandLine.ignoreTimestamps && oldTimestamp < timestamp) { copied.put(name, timestamp); copyFrom.put(name, f); } } } }) && oldSize < copied.size()) auxZipFiles.add(f); else if (classFileFound) System.err.println("Skipping aux file " + fInName + ", no new classes found"); else System.err.println("Skipping aux file" + fInName + ", no classes found"); } System.out.printf(" # Zip/jar files: %2d%n", inputZipFiles.size()); System.out.printf("# aux Zip/jar files: %2d%n", auxZipFiles.size()); System.out.printf("Unique class files: %6d%n", copied.size()); if (numFilesToAnalyze != copied.size()) System.out.printf(" files to analyze: %6d%n", numFilesToAnalyze); if (!excluded.isEmpty()) System.out.printf(" excluded files: %6d%n", excluded.size()); if (commandLine.onlyAnalyze) return; if (numFilesToAnalyze < copied.size() || numFilesToAnalyze > commandLine.maxClasses) auxilaryOut = createZipFile(getNextAuxilaryFileOutput()); int count = Integer.MAX_VALUE; String oldBaseClass = "x x"; String oldPackage = "x x"; for (String path : filesToAnalyze) { int lastSlash = path.lastIndexOf('/'); String packageName = lastSlash <= 0 ? "" : path.substring(0, lastSlash - 1); int firstDollar = path.indexOf('$', lastSlash); String baseClass = firstDollar < 0 ? path : path.substring(0, firstDollar - 1); boolean switchOutput; if (count > commandLine.maxClasses) switchOutput = true; else if (count + 50 > commandLine.maxClasses && !baseClass.equals(oldBaseClass)) switchOutput = true; else if (count + 250 > commandLine.maxClasses && !packageName.equals(oldPackage)) switchOutput = true; else switchOutput = false; if (switchOutput) { // advance String zipFileName = getNextAnalyzeFileOutput(); analysisOutputFiles.put(path, createZipFile(zipFileName)); System.out.printf("%s%n -> %s%n", path, zipFileName); count = 0; } count++; oldPackage = packageName; oldBaseClass = baseClass; } for (File f : inputZipFiles) { System.err.println("Reading " + f); final File ff = f; processZipEntries(f, new ZipElementHandler() { public void handle(ZipFile zipInputFile, ZipEntry ze) throws IOException { if (commandLine.skip(ze)) return; String name = ze.getName(); String dottedName = name.replace('/', '.'); if (exclude(dottedName)) return; if (!ff.equals(copyFrom.get(name))) { return; } if (name.contains("DefaultProblem.class")) System.out.printf("%40s %40s%n", name, ff); boolean writeToAnalyzeOut = false; boolean writeToAuxilaryOut = false; if (commandLine.prefix.matches(dottedName)) { writeToAnalyzeOut = true; if (numFilesToAnalyze > commandLine.maxClasses) writeToAuxilaryOut = true; } else writeToAuxilaryOut = auxilaryOut != null; ZipOutputStream out = null; if (writeToAnalyzeOut) { out = getZipOutputFile(name); out.putNextEntry(newZipEntry(ze)); } if (writeToAuxilaryOut) { auxilaryClassCount++; if (auxilaryClassCount > 29999) { auxilaryClassCount = 0; advanceAuxilaryOut(); } auxilaryOut.putNextEntry(newZipEntry(ze)); } copyEntry(zipInputFile, ze, writeToAnalyzeOut, out, writeToAuxilaryOut, auxilaryOut); } }); } for (File f : auxZipFiles) { final File ff = f; System.err.println("Opening aux file " + f); processZipEntries(f, new ZipElementHandler() { public void handle(ZipFile zipInputFile, ZipEntry ze) throws IOException { if (commandLine.skip(ze)) return; String name = ze.getName(); String dottedName = name.replace('/', '.'); if (exclude(dottedName)) return; if (!ff.equals(copyFrom.get(name))) { return; } auxilaryClassCount++; if (auxilaryClassCount > 29999) { auxilaryClassCount = 0; advanceAuxilaryOut(); } auxilaryOut.putNextEntry(newZipEntry(ze)); copyEntry(zipInputFile, ze, false, null, true, auxilaryOut); } }); } if (auxilaryOut != null) auxilaryOut.close(); for (ZipOutputStream out : analysisOutputFiles.values()) out.close(); System.out.println("All done"); } /** * @param fileName * @return * @throws FileNotFoundException */ private ZipOutputStream createZipFile(String fileName) throws FileNotFoundException { File newFile = new File(commandLine.outputDir, fileName); return new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(newFile))); } private boolean embeddedNameMismatch(ZipFile zipInputFile, ZipEntry ze) throws IOException { InputStream zipIn = zipInputFile.getInputStream(ze); String name = ze.getName(); JavaClass j = new ClassParser(zipIn, name).parse(); zipIn.close(); String className = j.getClassName(); String computedFileName = ClassName.toSlashedClassName(className)+".class"; if (name.charAt(0) == '1') { System.out.println(name); System.out.println(" " + className); } if (computedFileName.equals(name)) return false; System.out.println("In " + name + " found " + className); return true; } private void copyEntry(ZipFile zipInputFile, ZipEntry ze, boolean writeToAnalyzeOut, ZipOutputStream analyzeOut1, boolean writeToAuxilaryOut, ZipOutputStream auxilaryOut1) throws IOException { InputStream zipIn = zipInputFile.getInputStream(ze); while (true) { int bytesRead = zipIn.read(buffer); if (bytesRead < 0) break; if (writeToAnalyzeOut) analyzeOut1.write(buffer, 0, bytesRead); if (writeToAuxilaryOut) auxilaryOut1.write(buffer, 0, bytesRead); } if (writeToAnalyzeOut) analyzeOut1.closeEntry(); if (writeToAuxilaryOut) auxilaryOut1.closeEntry(); zipIn.close(); } private void advanceAuxilaryOut() throws IOException, FileNotFoundException { auxilaryOut.close(); auxilaryOut = createZipFile(getNextAuxilaryFileOutput()); } boolean processZipEntries(File f, ZipElementHandler handler) { if (!f.exists()) { System.out.println("file not found: '" + f + "'"); return false; } if (!f.canRead() || f.isDirectory()) { System.out.println("not readable: '" + f + "'"); return false; } if (!f.canRead() || f.isDirectory()) { System.out.println("not readable: '" + f + "'"); return false; } if (f.length() == 0) { System.out.println("empty zip file: '" + f + "'"); return false; } ZipFile zipInputFile; try { zipInputFile = new ZipFile(f); for (Enumeration<? extends ZipEntry> e = zipInputFile.entries(); e.hasMoreElements();) { ZipEntry ze = e.nextElement(); if (!ze.isDirectory() && ze.getName().endsWith(".class") && ze.getSize() != 0) handler.handle(zipInputFile, ze); } zipInputFile.close(); } catch (ClassFileNameMismatch e) { return false; } catch (IOException e) { System.out.println("Error processing '" + f + "'"); return false; } return true; } /** * @param ze * @return */ public ZipEntry newZipEntry(ZipEntry ze) { ZipEntry ze2 = new ZipEntry(ze.getName()); ze2.setComment(ze.getComment()); ze2.setTime(ze.getTime()); ze2.setExtra(ze.getExtra()); return ze2; } static class ClassFileNameMismatch extends IOException { } interface ZipElementHandler { void handle(ZipFile file, ZipEntry ze) throws IOException; } }