From 9ae1c987a08af842978b9d53dd2847493922f624 Mon Sep 17 00:00:00 2001 From: Chesnay Schepler Date: Mon, 31 Jan 2022 11:02:48 +0100 Subject: [PATCH] Eagerly filter classes --- .../japicmp/cmp/JarArchiveComparator.java | 207 +++++++++++++----- .../java/japicmp/cmp/ReducibleClassPool.java | 14 ++ .../test/java/japicmp/cmp/ClassesHelper.java | 8 +- 3 files changed, 175 insertions(+), 54 deletions(-) create mode 100644 japicmp/src/main/java/japicmp/cmp/ReducibleClassPool.java diff --git a/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java b/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java index b36012ef1..96fa03955 100644 --- a/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java +++ b/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java @@ -13,15 +13,17 @@ import japicmp.model.JavaObjectSerializationCompatibility; import japicmp.output.OutputFilter; import japicmp.util.AnnotationHelper; +import java.util.function.Supplier; import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException; import java.io.File; import java.io.IOException; -import java.util.ArrayList; +import java.io.InputStream; import java.util.Collections; import java.util.Enumeration; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.jar.JarEntry; @@ -36,9 +38,9 @@ */ public class JarArchiveComparator { private static final Logger LOGGER = Logger.getLogger(JarArchiveComparator.class.getName()); - private ClassPool commonClassPool; - private ClassPool oldClassPool; - private ClassPool newClassPool; + private ReducibleClassPool commonClassPool; + private ReducibleClassPool oldClassPool; + private ReducibleClassPool newClassPool; private String commonClassPathAsString = ""; private String oldClassPathAsString = ""; private String newClassPathAsString = ""; @@ -102,12 +104,12 @@ private void checkJavaObjectSerializationCompatibility(List jApiClass private void setupClasspaths() { if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) { - commonClassPool = new ClassPool(); + commonClassPool = new ReducibleClassPool(); commonClassPathAsString = setupClasspath(commonClassPool, this.options.getClassPathEntries()); } else if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.TWO_SEPARATE_CLASSPATHS) { - oldClassPool = new ClassPool(); + oldClassPool = new ReducibleClassPool(); oldClassPathAsString = setupClasspath(oldClassPool, this.options.getOldClassPath()); - newClassPool = new ClassPool(); + newClassPool = new ReducibleClassPool(); newClassPathAsString = setupClasspath(newClassPool, this.options.getNewClassPath()); } else { throw new JApiCmpException(Reason.IllegalState, "Unknown classpath mode: " + this.options.getClassPathMode()); @@ -201,10 +203,8 @@ private List createAndCompareClassLists(List oldArchives, List< * @return a list of {@link japicmp.model.JApiClass} that represent the changes */ List compareClassLists(JarArchiveComparatorOptions options, List oldClasses, List newClasses) { - List oldClassesFiltered = applyFilter(options, oldClasses); - List newClassesFiltered = applyFilter(options, newClasses); ClassesComparator classesComparator = new ClassesComparator(this, options); - classesComparator.compare(oldClassesFiltered, newClassesFiltered); + classesComparator.compare(oldClasses, newClasses); List classList = classesComparator.getClasses(); if (LOGGER.isLoggable(Level.FINE)) { for (JApiClass jApiClass : classList) { @@ -217,55 +217,53 @@ List compareClassLists(JarArchiveComparatorOptions options, List applyFilter(JarArchiveComparatorOptions options, List ctClasses) { - List newList = new ArrayList<>(ctClasses.size()); - for (CtClass ctClass : ctClasses) { - if (options.getFilters().includeClass(ctClass)) { - newList.add(ctClass); - } - } - return newList; + private List createListOfCtClasses(List archives, ReducibleClassPool classPool) { + return createListOfCtClasses(() -> new JarsCtClassIterable(archives, classPool), classPool); + } + + List createListOfCtClasses(Supplier> ctClasses, ReducibleClassPool classPool) { + return loadAndFilterClasses(ctClasses, classPool, false); } - private List createListOfCtClasses(List archives, ClassPool classPool) { + private List loadAndFilterClasses(Supplier> ctClasses, ReducibleClassPool classPool, boolean ignorePackageFilters) { + // marks whether any package was found + // if so we need to go over _all_ classes again + boolean packageFilterEncountered = false; + List classes = new LinkedList<>(); - for (File archive : archives) { - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Loading classes from jar file '" + archive.getAbsolutePath() + "'"); - } - try (JarFile jarFile = new JarFile(archive)) { - Enumeration entryEnumeration = jarFile.entries(); - while (entryEnumeration.hasMoreElements()) { - JarEntry jarEntry = entryEnumeration.nextElement(); - String name = jarEntry.getName(); - if (name.endsWith(".class")) { - CtClass ctClass; - try { - ctClass = classPool.makeClass(jarFile.getInputStream(jarEntry)); - } catch (Exception e) { - throw new JApiCmpException(Reason.IoException, String.format("Failed to load file from jar '%s' as class file: %s.", name, e.getMessage()), e); - } - classes.add(ctClass); - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine(String.format("Adding class '%s' with jar name '%s' to list.", ctClass.getName(), name)); - } - if (name.endsWith("package-info.class")) { - updatePackageFilter(ctClass); - } - } else { - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine(String.format("Skipping file '%s' because filename does not end with '.class'.", name)); - } + for (CtClass ctClass : ctClasses.get()) { + if (!packageFilterEncountered) { + if (options.getFilters().includeClass(ctClass)) { + classes.add(ctClass); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format("Adding class '%s' with jar name '%s' to list.", ctClass.getName(), ctClass.getName())); + } + } else { + classPool.remove(ctClass); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format("Ignoring class '%s' with jar name '%s'.", ctClass.getName(), ctClass.getName())); } } - } catch (IOException e) { - throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e); + } + if (!ignorePackageFilters && ctClass.getName().endsWith("package-info")) { + packageFilterEncountered |= updatePackageFilter(ctClass); + if (packageFilterEncountered) { + // we found a package filter, so any filtering we did so far may be invalid + // reset everything and restart after having read all remaining filters + classes.forEach(classPool::remove); + classes.clear(); + } } } - return classes; + + return packageFilterEncountered + ? loadAndFilterClasses(ctClasses, classPool, true) + : classes; } - private void updatePackageFilter(CtClass ctClass) { + + private boolean updatePackageFilter(CtClass ctClass) { + boolean filtersUpdated = false; Filters filters = options.getFilters(); List newFilters = new LinkedList<>(); for (Filter filter : filters.getIncludes()) { @@ -279,6 +277,7 @@ private void updatePackageFilter(CtClass ctClass) { if (newFilters.size() > 0) { filters.getIncludes().addAll(newFilters); newFilters.clear(); + filtersUpdated = true; } for (Filter filter : filters.getExcludes()) { if (filter instanceof AnnotationFilterBase) { @@ -291,7 +290,9 @@ private void updatePackageFilter(CtClass ctClass) { if (newFilters.size() > 0) { filters.getExcludes().addAll(newFilters); newFilters.clear(); + filtersUpdated = true; } + return filtersUpdated; } /** @@ -309,7 +310,7 @@ public JarArchiveComparatorOptions getJarArchiveComparatorOptions() { * * @return an instance of ClassPool */ - public ClassPool getCommonClassPool() { + public ReducibleClassPool getCommonClassPool() { return commonClassPool; } @@ -377,4 +378,108 @@ public Optional loadClass(ArchiveType archiveType, String name) { } return loadedClass; } + + private static class JarsCtClassIterable implements Iterable, Iterator { + private final Iterator archives; + private final ClassPool classPool; + + private Iterator currentIterator = null; + + public JarsCtClassIterable(List archives, ClassPool classPool) { + this.archives = archives.iterator(); + this.classPool = classPool; + } + + @Override + public boolean hasNext() { + if (currentIterator != null) { + if (currentIterator.hasNext()) { + return true; + } else { + currentIterator = null; + } + } + if (archives.hasNext()) { + final File archive = archives.next(); + currentIterator = new JarCtClassIterator(archive, classPool); + return hasNext(); + } + return false; + } + + @Override + public CtClass next() { + return currentIterator.next(); + } + + @Override + public Iterator iterator() { + return this; + } + } + + private static class JarCtClassIterator implements Iterator { + + private final File archive; + private final JarFile jarFile; + private final Enumeration entryEnumeration; + private final ClassPool classPool; + + private CtClass next = null; + + public JarCtClassIterator(File archive, ClassPool classPool) { + this.archive = archive; + try { + this.jarFile = new JarFile(archive); + } catch (IOException e) { + throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e); + } + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Loading classes from jar file '" + archive.getAbsolutePath() + "'"); + } + this.entryEnumeration = jarFile.entries(); + this.classPool = classPool; + } + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + + while (entryEnumeration.hasMoreElements()) { + JarEntry jarEntry = entryEnumeration.nextElement(); + String name = jarEntry.getName(); + if (name.endsWith(".class")) { + CtClass ctClass; + try (InputStream classFile = jarFile.getInputStream(jarEntry)) { + ctClass = classPool.makeClass(classFile); + } catch (Exception e) { + throw new JApiCmpException(Reason.IoException, String.format("Failed to load file from jar '%s' as class file: %s.", name, e.getMessage()), e); + } + next = ctClass; + return true; + } else { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format("Skipping file '%s' because filename does not end with '.class'.", name)); + } + } + } + try { + jarFile.close(); + } catch (IOException e) { + throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e); + } + return false; + } + + @Override + public CtClass next() { + try { + return next; + } finally { + next = null; + } + } + } } diff --git a/japicmp/src/main/java/japicmp/cmp/ReducibleClassPool.java b/japicmp/src/main/java/japicmp/cmp/ReducibleClassPool.java new file mode 100644 index 000000000..2adef3ba7 --- /dev/null +++ b/japicmp/src/main/java/japicmp/cmp/ReducibleClassPool.java @@ -0,0 +1,14 @@ +package japicmp.cmp; + +import javassist.ClassPool; +import javassist.CtClass; + +/** + * A {@link ClassPool} that allows to remove a class from the pool. + */ +public class ReducibleClassPool extends ClassPool { + public void remove(CtClass ctClass) { + removeCached(ctClass.getName()); + } +} + diff --git a/japicmp/src/test/java/japicmp/cmp/ClassesHelper.java b/japicmp/src/test/java/japicmp/cmp/ClassesHelper.java index 7c5a4fb7a..005d351d1 100644 --- a/japicmp/src/test/java/japicmp/cmp/ClassesHelper.java +++ b/japicmp/src/test/java/japicmp/cmp/ClassesHelper.java @@ -16,10 +16,12 @@ public interface ClassesGenerator { public static List compareClasses(JarArchiveComparatorOptions options, ClassesGenerator classesGenerator) throws Exception { JarArchiveComparator jarArchiveComparator = new JarArchiveComparator(options); - ClassPool classPool = jarArchiveComparator.getCommonClassPool(); - List oldClasses = classesGenerator.createOldClasses(classPool); + ReducibleClassPool classPool = jarArchiveComparator.getCommonClassPool(); + final List oldClasses = classesGenerator.createOldClasses(classPool); List newClasses = classesGenerator.createNewClasses(classPool); - return jarArchiveComparator.compareClassLists(options, oldClasses, newClasses); + List filteredOldClasses = jarArchiveComparator.createListOfCtClasses(() -> oldClasses, classPool); + List filteredNewClasses = jarArchiveComparator.createListOfCtClasses(() -> newClasses, classPool); + return jarArchiveComparator.compareClassLists(options, filteredOldClasses, filteredNewClasses); } public static class CompareClassesResult {