All Downloads are FREE. Search and download functionalities are using the official Maven repository.

japicmp.cmp.JarArchiveComparator Maven / Gradle / Ivy

Go to download

japicmp is a library that computes the differences between two versions of a jar file/artifact in order to ease the API documentation for clients/customers.

There is a newer version: 0.23.1
Show newest version
package japicmp.cmp;

import com.google.common.base.Optional;
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.compat.CompatibilityChanges;
import japicmp.model.JApiClass;
import japicmp.model.JApiException;
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;

/**
 * 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 jar 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 compare(File oldArchive, File newArchive) {
		return compare(Collections.singletonList(oldArchive), Collections.singletonList(newArchive));
	}

	/**
	 * Compares the two given list of jar 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 compare(List oldArchives, List newArchives) {
		return createAndCompareClassLists(oldArchives, newArchives);
	}

	private void checkJavaObjectSerializationCompatibility(List 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 classPathEntries) {
		String classPathAsString = appendUserDefinedClassPathEntries(classPool, classPathEntries);
		return appendSystemClassPath(classPool, classPathAsString);
	}

	private String appendSystemClassPath(ClassPool classPool, String classPathAsString) {
		classPool.appendSystemPath();
		if (classPathAsString.length() > 0 && !classPathAsString.endsWith(File.pathSeparator)) {
			classPathAsString += File.pathSeparator;
		}
		return classPathAsString;
	}

	private String appendUserDefinedClassPathEntries(ClassPool classPool, List 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 classList) {
		CompatibilityChanges compatibilityChanges = new CompatibilityChanges(this);
		compatibilityChanges.evaluate(classList);
	}

	private List createAndCompareClassLists(List oldArchives, List newArchives) {
		List oldClasses;
		List 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 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);
		List 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 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, ClassPool classPool) {
		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));
						}
					}
				}
			} 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 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 loadClass(ArchiveType archiveType, String name) {
		Optional loadedClass = Optional.absent();
		if (this.options.getClassPathMode() == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) {
			try {
				loadedClass = Optional.of(commonClassPool.get(name));
			} catch (NotFoundException e) {
				if (!options.isIgnoreMissingClasses()) {
					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.isIgnoreMissingClasses()) {
						throw JApiCmpException.forClassLoading(e, name, this);
					}
				}
			} else if (archiveType == ArchiveType.NEW) {
				try {
					loadedClass = Optional.of(newClassPool.get(name));
				} catch (NotFoundException e) {
					if (!options.isIgnoreMissingClasses()) {
						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;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy