package japicmp.cmp; import com.google.common.base.Optional; import japicmp.compat.CompatibilityChanges; import japicmp.exception.JApiCmpException; import japicmp.exception.JApiCmpException.Reason; import japicmp.filter.AnnotationFilterBase; import japicmp.filter.Filter; import japicmp.filter.Filters; import japicmp.filter.JavadocLikePackageFilter; import japicmp.model.JApiClass; import japicmp.model.JavaObjectSerializationCompatibility; import japicmp.output.OutputFilter; import japicmp.util.AnnotationHelper; import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException; import java.io.File; import java.io.IOException; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import static japicmp.util.FileHelper.toFileList; /** * This class provides the basic methods to compare the classes within to jar archives. */ public class JarArchiveComparator { private static final Logger LOGGER = Logger.getLogger(JarArchiveComparator.class.getName()); private ClassPool commonClassPool; private ClassPool oldClassPool; private ClassPool newClassPool; private String commonClassPathAsString = ""; private String oldClassPathAsString = ""; private String newClassPathAsString = ""; private JarArchiveComparatorOptions options; /** * Constructs an instance of this class and performs a setup of the classpath * * @param options the options used in the further processing */ public JarArchiveComparator(JarArchiveComparatorOptions options) { this.options = options; setupClasspaths(); } /** * Compares the two given archives. * * @param oldArchive the old version of the archive * @param newArchive the new version of the archive * @return a list which contains one instance of {@link japicmp.model.JApiClass} for each class found in one of the two archives * @throws JApiCmpException if the comparison fails */ public List<JApiClass> compare(JApiCmpArchive oldArchive, JApiCmpArchive newArchive) { return compare(Collections.singletonList(oldArchive), Collections.singletonList(newArchive)); } /** * Compares the two given lists of archives. * * @param oldArchives the old versions of the archives * @param newArchives the new versions of the archives * @return a list which contains one instance of {@link japicmp.model.JApiClass} for each class found in one of the archives * @throws JApiCmpException if the comparison fails */ public List<JApiClass> compare(List<JApiCmpArchive> oldArchives, List<JApiCmpArchive> newArchives) { return createAndCompareClassLists(toFileList(oldArchives), toFileList(newArchives)); } private void checkJavaObjectSerializationCompatibility(List<JApiClass> jApiClasses) { JavaObjectSerializationCompatibility javaObjectSerializationCompatibility = new JavaObjectSerializationCompatibility(); javaObjectSerializationCompatibility.evaluate(jApiClasses); } private void setupClasspaths() { if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) { commonClassPool = new ClassPool(); commonClassPathAsString = setupClasspath(commonClassPool, this.options.getClassPathEntries()); } else if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.TWO_SEPARATE_CLASSPATHS) { oldClassPool = new ClassPool(); oldClassPathAsString = setupClasspath(oldClassPool, this.options.getOldClassPath()); newClassPool = new ClassPool(); newClassPathAsString = setupClasspath(newClassPool, this.options.getNewClassPath()); } else { throw new JApiCmpException(Reason.IllegalState, "Unknown classpath mode: " + this.options.getClassPathMode()); } } private String setupClasspath(ClassPool classPool, List<String> classPathEntries) { String classPathAsString = appendUserDefinedClassPathEntries(classPool, classPathEntries); return appendSystemClassPath(classPool, classPathAsString); } private String appendSystemClassPath(ClassPool classPool, String classPathAsString) { String retVal = classPathAsString; classPool.appendSystemPath(); if (retVal.length() > 0 && !retVal.endsWith(File.pathSeparator)) { retVal += File.pathSeparator; } return retVal; } private String appendUserDefinedClassPathEntries(ClassPool classPool, List<String> classPathEntries) { String classPathAsString = ""; for (String classPathEntry : classPathEntries) { try { classPool.appendClassPath(classPathEntry); if (!classPathAsString.endsWith(File.pathSeparator)) { classPathAsString += File.pathSeparator; } classPathAsString += classPathEntry; } catch (NotFoundException e) { throw JApiCmpException.forClassLoading(e, classPathEntry, this); } } return classPathAsString; } /** * Returns the common classpath used by {@link japicmp.cmp.JarArchiveComparator} * * @return the common classpath as String */ public String getCommonClasspathAsString() { return commonClassPathAsString; } /** * Returns the classpath for the old version as String. * * @return the classpath for the old version */ public String getOldClassPathAsString() { return oldClassPathAsString; } /** * Returns the classpath for the new version as String. * * @return the classpath for the new version */ public String getNewClassPathAsString() { return newClassPathAsString; } private void checkBinaryCompatibility(List<JApiClass> classList) { CompatibilityChanges compatibilityChanges = new CompatibilityChanges(this); compatibilityChanges.evaluate(classList); } private List<JApiClass> createAndCompareClassLists(List<File> oldArchives, List<File> newArchives) { List<CtClass> oldClasses; List<CtClass> newClasses; if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) { oldClasses = createListOfCtClasses(oldArchives, commonClassPool); newClasses = createListOfCtClasses(newArchives, commonClassPool); return compareClassLists(options, oldClasses, newClasses); } else if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.TWO_SEPARATE_CLASSPATHS) { oldClasses = createListOfCtClasses(oldArchives, oldClassPool); newClasses = createListOfCtClasses(newArchives, newClassPool); return compareClassLists(options, oldClasses, newClasses); } else { throw new JApiCmpException(Reason.IllegalState, "Unknown classpath mode: " + this.options.getClassPathMode()); } } /** * Compares the two lists with CtClass objects using the provided options instance. * * @param options the options to use * @param oldClasses a list of CtClasses that represent the old version * @param newClasses a list of CtClasses that represent the new version * @return a list of {@link japicmp.model.JApiClass} that represent the changes */ List<JApiClass> compareClassLists(JarArchiveComparatorOptions options, List<CtClass> oldClasses, List<CtClass> newClasses) { List<CtClass> oldClassesFiltered = applyFilter(options, oldClasses); List<CtClass> newClassesFiltered = applyFilter(options, newClasses); ClassesComparator classesComparator = new ClassesComparator(this, options); classesComparator.compare(oldClassesFiltered, newClassesFiltered); List<JApiClass> classList = classesComparator.getClasses(); if (LOGGER.isLoggable(Level.FINE)) { for (JApiClass jApiClass : classList) { LOGGER.fine(jApiClass.toString()); } } checkBinaryCompatibility(classList); checkJavaObjectSerializationCompatibility(classList); OutputFilter.sortClassesAndMethods(classList); return classList; } private List<CtClass> applyFilter(JarArchiveComparatorOptions options, List<CtClass> ctClasses) { List<CtClass> newList = new ArrayList<>(ctClasses.size()); for (CtClass ctClass : ctClasses) { if (options.getFilters().includeClass(ctClass)) { newList.add(ctClass); } } return newList; } private List<CtClass> createListOfCtClasses(List<File> archives, ClassPool classPool) { List<CtClass> 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<JarEntry> 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)); } } } } catch (IOException e) { throw new JApiCmpException(Reason.IoException, String.format("Processing of jar file %s failed: %s", archive.getAbsolutePath(), e.getMessage()), e); } } return classes; } private void updatePackageFilter(CtClass ctClass) { Filters filters = options.getFilters(); List<Filter> newFilters = new LinkedList<>(); for (Filter filter : filters.getIncludes()) { if (filter instanceof AnnotationFilterBase) { String className = ((AnnotationFilterBase) filter).getClassName(); if (AnnotationHelper.hasAnnotation(ctClass.getClassFile(), className)) { newFilters.add(new JavadocLikePackageFilter(ctClass.getPackageName())); } } } if (newFilters.size() > 0) { filters.getIncludes().addAll(newFilters); newFilters.clear(); } for (Filter filter : filters.getExcludes()) { if (filter instanceof AnnotationFilterBase) { String className = ((AnnotationFilterBase) filter).getClassName(); if (AnnotationHelper.hasAnnotation(ctClass.getClassFile(), className)) { newFilters.add(new JavadocLikePackageFilter(ctClass.getPackageName())); } } } if (newFilters.size() > 0) { filters.getExcludes().addAll(newFilters); newFilters.clear(); } } /** * Returns the instance of {@link japicmp.cmp.JarArchiveComparatorOptions} that is used. * * @return an instance of {@link japicmp.cmp.JarArchiveComparatorOptions} */ public JarArchiveComparatorOptions getJarArchiveComparatorOptions() { return this.options; } /** * Returns the javassist ClassPool instance that is used by this instance. This can be used in unit tests to define * artificial CtClass instances for the same ClassPool. * * @return an instance of ClassPool */ public ClassPool getCommonClassPool() { return commonClassPool; } /** * Returns the javassist ClassPool that is used for the old version. * * @return an instance of ClassPool */ public ClassPool getOldClassPool() { return oldClassPool; } /** * Returns the javassist ClassPool that is used for the new version. * * @return an instance of ClassPool */ public ClassPool getNewClassPool() { return newClassPool; } public enum ArchiveType { OLD, NEW } /** * Loads a class either from the old, new or common classpath. * @param archiveType specify if this class should be loaded from the old or new class path * @param name the name of the class (FQN) * @return the loaded class (if options are not set to ignore missing classes) * @throws japicmp.exception.JApiCmpException if loading the class fails */ public Optional<CtClass> loadClass(ArchiveType archiveType, String name) { Optional<CtClass> loadedClass = Optional.absent(); if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) { try { loadedClass = Optional.of(commonClassPool.get(name)); } catch (NotFoundException e) { if (!options.getIgnoreMissingClasses().ignoreClass(e.getMessage())) { throw JApiCmpException.forClassLoading(e, name, this); } } } else if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.TWO_SEPARATE_CLASSPATHS) { if (archiveType == ArchiveType.OLD) { try { loadedClass = Optional.of(oldClassPool.get(name)); } catch (NotFoundException e) { if (!options.getIgnoreMissingClasses().ignoreClass(e.getMessage())) { throw JApiCmpException.forClassLoading(e, name, this); } } } else if (archiveType == ArchiveType.NEW) { try { loadedClass = Optional.of(newClassPool.get(name)); } catch (NotFoundException e) { if (!options.getIgnoreMissingClasses().ignoreClass(e.getMessage())) { throw JApiCmpException.forClassLoading(e, name, this); } } } else { throw new JApiCmpException(Reason.IllegalState, "Unknown archive type: " + archiveType); } } else { throw new JApiCmpException(Reason.IllegalState, "Unknown classpath mode: " + this.options.getClassPathMode()); } return loadedClass; } }