/** * Copyright 2012-2014 Julien Eluard and contributors * This project includes software developed by Julien Eluard: https://github.com/jeluard/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.osjava.jardiff; import java.io.*; import java.net.URL; import java.net.URLClassLoader; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Opcodes; /* import javax.xml.transform.ErrorListener; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; */ /** * A class to perform a diff between two jar files. * * @author Antony Riley */ public class JarDiff { /** * A map containing information about classes which are dependencies. * Keys are internal class names. * Values are instances of ClassInfo. */ protected Map depClassInfo = new HashMap(); /** * A map containing information about classes in the old jar file. * Keys are internal class names. * Values are instances of ClassInfo. */ protected Map oldClassInfo = new TreeMap(); /** * A map containing information about classes in the new jar file. * Keys are internal class names. * Values are instances of ClassInfo. */ protected Map newClassInfo = new TreeMap(); /** * An array of dependencies which are jar files, or urls. */ private URL[] deps; /** * A class loader used for loading dependency classes. */ private URLClassLoader depLoader; /** * The name of the old version. */ private String oldVersion; /** * The name of the new version. */ private String newVersion; /** * Class info visitor, used to load information about classes. */ private final ClassInfoVisitor infoVisitor = new ClassInfoVisitor(); /** * Create a new JarDiff object. */ public JarDiff() { } /** * Set the name of the old version. * * @param oldVersion the name */ public void setOldVersion(final String oldVersion) { this.oldVersion = oldVersion; } /** * Get the name of the old version. * * @return the name */ public String getOldVersion() { return oldVersion; } /** * Set the name of the new version. * * @param newVersion the version */ public void setNewVersion(final String newVersion) { this.newVersion = newVersion; } /** * Get the name of the new version. * * @return the name */ public String getNewVersion() { return newVersion; } /** * Set the dependencies. * * @param deps an array of urls pointing to jar files or directories * containing classes which are required dependencies. */ public void setDependencies(final URL[] deps) { this.deps = deps; } /** * Get the dependencies. * * @return the dependencies as an array of URLs */ public URL[] getDependencies() { return deps; } /** * Load classinfo given a ClassReader. * * @param reader the ClassReader * @return the ClassInfo */ public synchronized ClassInfo loadClassInfo(final ClassReader reader) throws IOException { infoVisitor.reset(); reader.accept(infoVisitor, 0); return infoVisitor.getClassInfo(); } /** * Load all the classes from the specified URL and store information * about them in the specified map. * This currently only works for jar files, not directories * which contain classes in subdirectories or in the current directory. * * @param infoMap the map to store the ClassInfo in. * @throws DiffException if there is an exception reading info about a * class. */ private void loadClasses(final Map infoMap, final URL path) throws DiffException { try { File jarFile = null; if(!"file".equals(path.getProtocol()) || path.getHost() != null) { // If it's not a local file, store it as a temporary jar file. // java.util.jar.JarFile requires a local file handle. jarFile = File.createTempFile("jardiff","jar"); // Mark it to be deleted on exit. jarFile.deleteOnExit(); final InputStream in = path.openStream(); final OutputStream out = new FileOutputStream(jarFile); final byte[] buffer = new byte[4096]; int i; while( (i = in.read(buffer,0,buffer.length)) != -1) { out.write(buffer, 0, i); } in.close(); out.close(); } else { // Else it's a local file, nothing special to do. jarFile = new File(path.getPath()); } loadClasses(infoMap, jarFile); } catch (final IOException ioe) { throw new DiffException(ioe); } } /** * Load all the classes from the specified URL and store information * about them in the specified map. * This currently only works for jar files, not directories * which contain classes in subdirectories or in the current directory. * * @param infoMap the map to store the ClassInfo in. * @param file the jarfile to load classes from. * @throws IOException if there is an IOException reading info about a * class. */ private void loadClasses(final Map infoMap, final File file) throws DiffException { try { final JarFile jar = new JarFile(file); final Enumeration e = jar.entries(); while (e.hasMoreElements()) { final JarEntry entry = (JarEntry) e.nextElement(); final String name = entry.getName(); if (!entry.isDirectory() && name.endsWith(".class")) { final ClassReader reader = new ClassReader(jar.getInputStream(entry)); final ClassInfo ci = loadClassInfo(reader); infoMap.put(ci.getName(), ci); } } } catch (final IOException ioe) { throw new DiffException(ioe); } } /** * Load old classes from the specified URL. * * @param loc The location of a jar file to load classes from. * @throws DiffException if there is an IOException. */ public void loadOldClasses(final URL loc) throws DiffException { loadClasses(oldClassInfo, loc); } /** * Load new classes from the specified URL. * * @param loc The location of a jar file to load classes from. * @throws DiffException if there is an IOException. */ public void loadNewClasses(final URL loc) throws DiffException { loadClasses(newClassInfo, loc); } /** * Load old classes from the specified File. * * @param file The location of a jar file to load classes from. * @throws DiffException if there is an IOException */ public void loadOldClasses(final File file) throws DiffException { loadClasses(oldClassInfo, file); } /** * Load new classes from the specified File. * * @param file The location of a jar file to load classes from. * @throws DiffException if there is an IOException */ public void loadNewClasses(final File file) throws DiffException { loadClasses(newClassInfo, file); } /** * Perform a diff sending the output to the specified handler, using * the specified criteria to select diffs. * * @param handler The handler to receive and handle differences. * @param criteria The criteria we use to select differences. * @throws DiffException when there is an underlying exception, e.g. * writing to a file caused an IOException */ public void diff(final DiffHandler handler, final DiffCriteria criteria) throws DiffException { diff(handler, criteria, oldVersion, newVersion, oldClassInfo, newClassInfo); } public void diff(final DiffHandler handler, final DiffCriteria criteria, final String oldVersion, final String newVersion, final Map oldClassInfo, final Map newClassInfo) throws DiffException { // TODO: Build the name from the MANIFEST rather than the filename handler.startDiff(oldVersion, newVersion); handler.startOldContents(); for (final ClassInfo ci : oldClassInfo.values()) { if (criteria.validClass(ci)) { handler.contains(ci); } } handler.endOldContents(); handler.startNewContents(); for (final ClassInfo ci : newClassInfo.values()) { if (criteria.validClass(ci)) { handler.contains(ci); } } handler.endNewContents(); final Set onlyOld = new TreeSet(oldClassInfo.keySet()); final Set onlyNew = new TreeSet(newClassInfo.keySet()); final Set both = new TreeSet(oldClassInfo.keySet()); onlyOld.removeAll(newClassInfo.keySet()); onlyNew.removeAll(oldClassInfo.keySet()); both.retainAll(newClassInfo.keySet()); handler.startRemoved(); for (final String s : onlyOld) { final ClassInfo ci = oldClassInfo.get(s); if (criteria.validClass(ci)) { handler.classRemoved(ci); } } handler.endRemoved(); handler.startAdded(); for (final String s : onlyNew) { final ClassInfo ci = newClassInfo.get(s); if (criteria.validClass(ci)) { handler.classAdded(ci); } } handler.endAdded(); final Set removedMethods = new TreeSet(); final Set removedFields = new TreeSet(); final Set addedMethods = new TreeSet(); final Set addedFields = new TreeSet(); final Set changedMethods = new TreeSet(); final Set changedFields = new TreeSet(); handler.startChanged(); for (final String s : both) { final ClassInfo oci = oldClassInfo.get(s); final ClassInfo nci = newClassInfo.get(s); if (criteria.validClass(oci) || criteria.validClass(nci)) { final Map oldMethods = oci.getMethodMap(); final Map oldFields = oci.getFieldMap(); final Map newMethods = nci.getMethodMap(); final Map newFields = nci.getFieldMap(); final Map extNewMethods = new HashMap(newMethods); final Map extNewFields = new HashMap(newFields); String superClass = nci.getSupername(); while (superClass != null && newClassInfo.containsKey(superClass)) { final ClassInfo sci = newClassInfo.get(superClass); for (final Map.Entry entry : sci.getFieldMap().entrySet()) { if (!(entry.getValue()).isPrivate() && !extNewFields.containsKey(entry.getKey())) { extNewFields.put(entry.getKey(), entry.getValue()); } } for (final Map.Entry entry : sci.getMethodMap().entrySet()) { if (!(entry.getValue()).isPrivate() && !extNewMethods.containsKey(entry.getKey())) { extNewMethods.put(entry.getKey(), entry.getValue()); } } superClass = sci.getSupername(); } for (final Map.Entry entry : oldMethods.entrySet()) { if (criteria.validMethod(entry.getValue())) removedMethods.add(entry.getKey()); } for (final Map.Entry entry : oldFields.entrySet()) { if (criteria.validField(entry.getValue())) removedFields.add(entry.getKey()); } for (final Map.Entry entry : newMethods.entrySet()) { if (criteria.validMethod(entry.getValue())) addedMethods.add(entry.getKey()); } for (final Map.Entry entry : newFields.entrySet()) { if (criteria.validField(entry.getValue())) addedFields.add(entry.getKey()); } // We add all the old methods that match the criteria changedMethods.addAll(removedMethods); // We keep the intersection of these with all the new methods // to detect as changed a method that no longer match the // criteria (i.e. a method that was public and is now private) changedMethods.retainAll(newMethods.keySet()); removedMethods.removeAll(changedMethods); removedMethods.removeAll(extNewMethods.keySet()); addedMethods.removeAll(changedMethods); changedFields.addAll(removedFields); changedFields.retainAll(newFields.keySet()); removedFields.removeAll(changedFields); removedFields.removeAll(extNewFields.keySet()); addedFields.removeAll(changedFields); Iterator j = changedMethods.iterator(); while (j.hasNext()) { final String desc = j.next(); final MethodInfo oldInfo = oldMethods.get(desc); final MethodInfo newInfo = newMethods.get(desc); if (!criteria.differs(oldInfo, newInfo)) j.remove(); } j = changedFields.iterator(); while (j.hasNext()) { final String desc = j.next(); final FieldInfo oldInfo = oldFields.get(desc); final FieldInfo newInfo = newFields.get(desc); if (!criteria.differs(oldInfo, newInfo)) j.remove(); } final boolean classchanged = criteria.differs(oci, nci); if (classchanged || !removedMethods.isEmpty() || !removedFields.isEmpty() || !addedMethods.isEmpty() || !addedFields.isEmpty() || !changedMethods.isEmpty() || !changedFields.isEmpty()) { handler.startClassChanged(s); handler.startRemoved(); for (final String field : removedFields) { handler.fieldRemoved(oldFields.get(field)); } for (final String method : removedMethods) { handler.methodRemoved(oldMethods.get(method)); } handler.endRemoved(); handler.startAdded(); for (final String field : addedFields) { handler.fieldAdded(newFields.get(field)); } for (final String method : addedMethods) { handler.methodAdded(newMethods.get(method)); } handler.endAdded(); handler.startChanged(); if (classchanged) { // Was only deprecated? if (wasDeprecated(oci, nci) && !criteria.differs(cloneDeprecated(oci), nci)) { handler.classDeprecated(oci, nci); } else { handler.classChanged(oci, nci); } } for (final String field : changedFields) { final FieldInfo oldFieldInfo = oldFields.get(field); final FieldInfo newFieldInfo = newFields.get(field); // Was only deprecated? if (wasDeprecated(oldFieldInfo, newFieldInfo) && !criteria.differs( cloneDeprecated(oldFieldInfo), newFieldInfo)) { handler.fieldDeprecated(oldFieldInfo, newFieldInfo); } else if( !criteria.differsBinary(oldFieldInfo, newFieldInfo)) { handler.fieldChangedCompat(oldFieldInfo, newFieldInfo); } else { handler.fieldChanged(oldFieldInfo, newFieldInfo); } } for (final String method : changedMethods) { final MethodInfo oldMethodInfo = oldMethods.get(method); final MethodInfo newMethodInfo = newMethods.get(method); // Was only deprecated? if (wasDeprecated(oldMethodInfo, newMethodInfo) && !criteria.differs( cloneDeprecated(oldMethodInfo), newMethodInfo)) { handler.methodDeprecated(oldMethodInfo, newMethodInfo); } else if ( !criteria.differsBinary(oldMethodInfo, newMethodInfo) ) { handler.methodChangedCompat(oldMethodInfo, newMethodInfo); } else { handler.methodChanged(oldMethodInfo, newMethodInfo); } } handler.endChanged(); handler.endClassChanged(); removedMethods.clear(); removedFields.clear(); addedMethods.clear(); addedFields.clear(); changedMethods.clear(); changedFields.clear(); } } } handler.endChanged(); handler.endDiff(); } /** * Determines if an {@link AbstractInfo} was deprecated. (Shortcut to avoid * creating cloned deprecated infos). */ private static boolean wasDeprecated(final AbstractInfo oldInfo, final AbstractInfo newInfo) { return !oldInfo.isDeprecated() && newInfo.isDeprecated(); } /** * Determines if an {@link AbstractInfo} was deprecated. (Shortcut to avoid * creating cloned deprecated infos). */ private static boolean throwClauseDiffers(final AbstractInfo oldInfo, final AbstractInfo newInfo) { return !oldInfo.isDeprecated() && newInfo.isDeprecated(); } /** * Clones the class info, but changes access, setting deprecated flag. * * @param classInfo * the original class info * @return the cloned and deprecated info. */ private static ClassInfo cloneDeprecated(final ClassInfo classInfo) { return new ClassInfo(classInfo.getVersion(), classInfo.getAccess() | Opcodes.ACC_DEPRECATED, classInfo.getName(), classInfo.getSignature(), classInfo.getSupername(), classInfo.getInterfaces(), classInfo.getMethodMap(), classInfo.getFieldMap()); } /** * Clones the method, but changes access, setting deprecated flag. * * @param methodInfo * the original method info * @return the cloned and deprecated method info. */ private static MethodInfo cloneDeprecated(final MethodInfo methodInfo) { return new MethodInfo(methodInfo.getAccess() | Opcodes.ACC_DEPRECATED, methodInfo.getName(), methodInfo.getDesc(), methodInfo.getSignature(), methodInfo.getExceptions()); } /** * Clones the field info, but changes access, setting deprecated flag. * * @param fieldInfo * the original field info * @return the cloned and deprecated field info. */ private static FieldInfo cloneDeprecated(final FieldInfo fieldInfo) { return new FieldInfo(fieldInfo.getAccess() | Opcodes.ACC_DEPRECATED, fieldInfo.getName(), fieldInfo.getDesc(), fieldInfo.getSignature(), fieldInfo.getValue()); } }