/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.aries.versioning.utils; import java.io.IOException; import java.lang.reflect.Modifier; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; public class ClassDeclaration extends GenericDeclaration { // Binary Compatibility - deletion of package-level access field/method/constructors of classes and interfaces in the package // will not break binary compatibility when an entire package is updated. // Assumptions: // 1. This tool assumes that the deletion of package-level fields/methods/constructors is not break binary compatibility // based on the assumption of the entire package is updated. // private final String superName; private final String[] interfaces; private final Map<String, FieldDeclaration> fields; private final Map<String, Set<MethodDeclaration>> methods; private final Map<String, Set<MethodDeclaration>> methodsInUpperChain = new HashMap<String, Set<MethodDeclaration>>(); private final Map<String, FieldDeclaration> fieldsInUpperChain = new HashMap<String, FieldDeclaration>(); private final Collection<String> supers = new ArrayList<String>(); private final URLClassLoader jarsLoader; private final SerialVersionClassVisitor serialVisitor; public Map<String, FieldDeclaration> getFields() { return fields; } public Map<String, FieldDeclaration> getAllFields() { Map<String, FieldDeclaration> allFields = new HashMap<String, FieldDeclaration>(getFields()); Map<String, FieldDeclaration> fieldsFromSupers = getFieldsInUpperChain(); putIfAbsent(allFields, fieldsFromSupers); return allFields; } private void putIfAbsent(Map<String, FieldDeclaration> allFields, Map<String, FieldDeclaration> fieldsFromSupers) { for (Map.Entry<String, FieldDeclaration> superFieldEntry : fieldsFromSupers.entrySet()) { String fieldName = superFieldEntry.getKey(); FieldDeclaration fd = superFieldEntry.getValue(); if (allFields.get(fieldName) == null) { allFields.put(fieldName, fd); } } } /** * Get the methods in the current class plus the methods in the upper chain * * @return map of method name to set of method declarations */ public Map<String, Set<MethodDeclaration>> getAllMethods() { Map<String, Set<MethodDeclaration>> methods = new HashMap<String, Set<MethodDeclaration>>(getMethods()); Map<String, Set<MethodDeclaration>> methodsFromSupers = getMethodsInUpperChain(); for (Map.Entry<String, Set<MethodDeclaration>> superMethodsEntry : methodsFromSupers.entrySet()) { Set<MethodDeclaration> overloadingMethods = methods.get(superMethodsEntry.getKey()); if (overloadingMethods != null) { overloadingMethods.addAll(superMethodsEntry.getValue()); } else { methods.put(superMethodsEntry.getKey(), superMethodsEntry.getValue()); } } return methods; } public Map<String, Set<MethodDeclaration>> getMethods() { return methods; } // public ClassDeclaration(int access, String name, String signature, String superName, // String[] interfaces, URLClassLoader loader) { // super(access, name, signature); // this.superName = superName; // this.interfaces = interfaces; // this.fields = new HashMap<String, FieldDeclaration>(); // this.methods = new HashMap<String, Set<MethodDeclaration>>(); // this.jarsLoader = loader; // this.serialVisitor = null; // } public ClassDeclaration(int access, String name, String signature, String superName, String[] interfaces, URLClassLoader loader, SerialVersionClassVisitor cv) { super(access, name, signature); this.superName = superName; this.interfaces = interfaces; this.fields = new HashMap<String, FieldDeclaration>(); this.methods = new HashMap<String, Set<MethodDeclaration>>(); this.jarsLoader = loader; this.serialVisitor = cv; } private void getFieldsRecursively(String superClass) { if ((superClass != null)) { // load the super class of the cd try { SerialVersionClassVisitor cv = new SerialVersionClassVisitor(null); SemanticVersioningClassVisitor svc = new SemanticVersioningClassVisitor(jarsLoader, cv); ClassReader cr = new ClassReader(jarsLoader.getResourceAsStream(superClass + SemanticVersioningUtils.classExt)); cr.accept(svc, 0); ClassDeclaration cd = svc.getClassDeclaration(); if (cd != null) { addFieldInUpperChain(cd.getFields()); getFieldsRecursively(cd.getSuperName()); for (String iface : cd.getInterfaces()) { getFieldsRecursively(iface); } } } catch (IOException ioe) { // not a problem } } } private void getMethodsRecursively(String superClass) { if ((superClass != null)) { // load the super class of the cd SerialVersionClassVisitor cv = new SerialVersionClassVisitor(null); SemanticVersioningClassVisitor svc = new SemanticVersioningClassVisitor(jarsLoader, cv); // use URLClassLoader to load the class try { ClassReader cr = new ClassReader(jarsLoader.getResourceAsStream(superClass + SemanticVersioningUtils.classExt)); cr.accept(svc, 0); ClassDeclaration cd = svc.getClassDeclaration(); if (cd != null) { addMethodsInUpperChain(cd.getMethods()); getMethodsRecursively(cd.getSuperName()); for (String iface : cd.getInterfaces()) { getMethodsRecursively(iface); } } } catch (IOException ioe) { // not a deal } } } public Map<String, FieldDeclaration> getFieldsInUpperChain() { if (fieldsInUpperChain.isEmpty()) { getFieldsRecursively(getSuperName()); for (String ifs : getInterfaces()) { getFieldsRecursively(ifs); } } return fieldsInUpperChain; } private void addFieldInUpperChain(Map<String, FieldDeclaration> fields) { putIfAbsent(fieldsInUpperChain, fields); } public Map<String, Set<MethodDeclaration>> getMethodsInUpperChain() { if (methodsInUpperChain.isEmpty()) { getMethodsRecursively(getSuperName()); for (String ifs : getInterfaces()) { getMethodsRecursively(ifs); } } return methodsInUpperChain; } private void addMethodsInUpperChain(Map<String, Set<MethodDeclaration>> methods) { for (Map.Entry<String, Set<MethodDeclaration>> method : methods.entrySet()) { String methodName = method.getKey(); Set<MethodDeclaration> mds = new HashSet<MethodDeclaration>(); if (methodsInUpperChain.get(methodName) != null) { mds.addAll(methodsInUpperChain.get(methodName)); } mds.addAll(method.getValue()); methodsInUpperChain.put(methodName, mds); } } public Collection<String> getUpperChainRecursively(String className) { Collection<String> clazz = new HashSet<String>(); if (className != null) { // load the super class of the cd SerialVersionClassVisitor cv = new SerialVersionClassVisitor(null); SemanticVersioningClassVisitor svc = new SemanticVersioningClassVisitor(jarsLoader, cv); try { ClassReader cr = new ClassReader(jarsLoader.getResourceAsStream(className + SemanticVersioningUtils.classExt)); cr.accept(svc, 0); clazz.add(className); if (svc.getClassDeclaration() != null) { String superName = svc.getClassDeclaration().getSuperName(); clazz.addAll(getUpperChainRecursively(superName)); if (svc.getClassDeclaration().getInterfaces() != null) { for (String iface : svc.getClassDeclaration().getInterfaces()) { clazz.addAll(getUpperChainRecursively(iface)); } } } } catch (IOException ioe) { // not to worry about this. terminate. } } return clazz; } public Collection<String> getAllSupers() { if (supers.isEmpty()) { supers.addAll(getUpperChainRecursively(getSuperName())); for (String iface : getInterfaces()) { supers.addAll(getUpperChainRecursively(iface)); } } return supers; } public String getSuperName() { return superName; } public String[] getInterfaces() { return interfaces; } public void addFields(FieldDeclaration fd) { fields.put(fd.getName(), fd); } public void addMethods(MethodDeclaration md) { String key = md.getName(); Set<MethodDeclaration> overloaddingMethods = methods.get(key); if (overloaddingMethods != null) { overloaddingMethods.add(md); methods.put(key, overloaddingMethods); } else { Set<MethodDeclaration> mds = new HashSet<MethodDeclaration>(); mds.add(md); methods.put(key, mds); } } public BinaryCompatibilityStatus getBinaryCompatibleStatus(ClassDeclaration old) { // check class signature, fields, methods BinaryCompatibilityStatus reasons = new BinaryCompatibilityStatus(); if (old == null) { return reasons; } getClassSignatureBinaryCompatibleStatus(old, reasons); getAllMethodsBinaryCompatibleStatus(old, reasons); getAllFieldsBinaryCompatibleStatus(old, reasons); getAllSuperPresentStatus(old, reasons); getSerializableBackCompatable(old, reasons); return reasons; } public boolean isAbstract() { return Modifier.isAbstract(getAccess()); } private void getClassSignatureBinaryCompatibleStatus(ClassDeclaration originalClass, List<String> reasons) { // if a class was not abstract but changed to abstract // not final changed to final // public changed to non-public String prefix = " The class " + getName(); if (!!!originalClass.isAbstract() && isAbstract()) { reasons.add(prefix + " was not abstract but is changed to be abstract."); } if (!!!originalClass.isFinal() && isFinal()) { reasons.add(prefix + " was not final but is changed to be final."); } if (originalClass.isPublic() && !!!isPublic()) { reasons.add(prefix + " was public but is changed to be non-public."); } } private void getAllFieldsBinaryCompatibleStatus(ClassDeclaration originalClass, List<String> reasons) { // for each field to see whether the same field has changed // not final -> final // static <-> nonstatic Map<String, FieldDeclaration> oldFields = originalClass.getAllFields(); Map<String, FieldDeclaration> newFields = getAllFields(); areFieldsBinaryCompatible(oldFields, newFields, reasons); } private void areFieldsBinaryCompatible(Map<String, FieldDeclaration> oldFields, Map<String, FieldDeclaration> currentFields, List<String> reasons) { for (Map.Entry<String, FieldDeclaration> entry : oldFields.entrySet()) { FieldDeclaration bef_fd = entry.getValue(); FieldDeclaration cur_fd = currentFields.get(entry.getKey()); isFieldBinaryCompatible(reasons, bef_fd, cur_fd); } } private boolean isFieldBinaryCompatible(List<String> reasons, FieldDeclaration bef_fd, FieldDeclaration cur_fd) { String fieldName = bef_fd.getName(); //only interested in the public or protected fields boolean compatible = true; if (bef_fd.isPublic() || bef_fd.isProtected()) { String prefix = "The " + (bef_fd.isPublic() ? "public" : "protected") + " field " + fieldName; if (cur_fd == null) { reasons.add(prefix + " has been deleted."); compatible = false; } else { if ((!!!bef_fd.isFinal()) && (cur_fd.isFinal())) { // make sure it has not been changed to final reasons.add(prefix + " was not final but has been changed to be final."); compatible = false; } if (bef_fd.isStatic() != cur_fd.isStatic()) { // make sure it the static signature has not been changed reasons.add(prefix + " was static but is changed to be non static or vice versa."); compatible = false; } // check to see the field type is the same if (!isFieldTypeSame(bef_fd, cur_fd)) { reasons.add(prefix + " has changed its type."); compatible = false; } if (SemanticVersioningUtils.isLessAccessible(bef_fd, cur_fd)) { // check whether the new field is less accessible than the old one reasons.add(prefix + " becomes less accessible."); compatible = false; } } } return compatible; } /** * Return whether the serializable class is binary compatible. The serial verison uid change breaks binary compatibility. * * * @param old Old class declaration * @param reasons list of binary compatibility problems */ private void getSerializableBackCompatable(ClassDeclaration old, List<String> reasons) { // It does not matter one of them is not serializable. if ((getAllSupers().contains(SemanticVersioningUtils.SERIALIZABLE_CLASS_IDENTIFIER)) && (old.getAllSupers().contains(SemanticVersioningUtils.SERIALIZABLE_CLASS_IDENTIFIER))) { // check to see whether the serializable id is the same //ignore if it is enum if ((!getAllSupers().contains(SemanticVersioningUtils.ENUM_CLASS) && (!old.getAllSupers().contains(SemanticVersioningUtils.ENUM_CLASS)))) { long oldValue = getSerialVersionUID(old); long curValue = getSerialVersionUID(this); if ((oldValue != curValue)) { reasons.add("The serializable class is no longer back compatible as the value of SerialVersionUID has changed from " + oldValue + " to " + curValue + "."); } } } } private long getSerialVersionUID(ClassDeclaration cd) { FieldDeclaration serialID = cd.getAllFields().get(SemanticVersioningUtils.SERIAL_VERSION_UTD); if (serialID != null) { if (serialID.isFinal() && serialID.isStatic() && Type.LONG_TYPE.equals(Type.getType(serialID.getDesc()))) { if (serialID.getValue() != null) { return (Long)serialID.getValue(); } else { return 0; } } } // get the generated value return cd.getSerialVisitor().getComputeSerialVersionUID(); } private boolean isFieldTypeSame(FieldDeclaration bef_fd, FieldDeclaration cur_fd) { boolean descSame = bef_fd.getDesc().equals(cur_fd.getDesc()); if (descSame) { return true; } return false; } private void getAllMethodsBinaryCompatibleStatus(ClassDeclaration originalClass, List<String> reasons) { // for all methods // no methods should have deleted // method return type has not changed // method changed from not abstract -> abstract Map<String, Set<MethodDeclaration>> oldMethods = originalClass.getAllMethods(); Map<String, Set<MethodDeclaration>> newMethods = getAllMethods(); areMethodsBinaryCompatible(oldMethods, newMethods, reasons); } private void areMethodsBinaryCompatible( Map<String, Set<MethodDeclaration>> oldMethods, Map<String, Set<MethodDeclaration>> newMethods, List<String> reasons) { boolean compatible = true; Map<String, Collection<MethodDeclaration>> extraMethods = new HashMap<String, Collection<MethodDeclaration>>(); for (Map.Entry<String, Set<MethodDeclaration>> me : newMethods.entrySet()) { Collection<MethodDeclaration> mds = new ArrayList<MethodDeclaration>(me.getValue()); extraMethods.put(me.getKey(), mds); } for (Map.Entry<String, Set<MethodDeclaration>> methods : oldMethods.entrySet()) { // all overloading methods, check against the current class String methodName = methods.getKey(); Collection<MethodDeclaration> oldMDSigs = methods.getValue(); // If the method cannot be found in the current class, it means that it has been deleted. Collection<MethodDeclaration> newMDSigs = newMethods.get(methodName); // for each overloading methods outer: for (MethodDeclaration md : oldMDSigs) { String mdName = md.getName(); String prefix = "The " + SemanticVersioningUtils.getReadableMethodSignature(mdName, md.getDesc()); if (md.isProtected() || md.isPublic()) { boolean found = false; if (newMDSigs != null) { // try to find it in the current class for (MethodDeclaration new_md : newMDSigs) { // find the method with the same return type, parameter list if ((md.equals(new_md))) { found = true; // If the old method is final but the new one is not or vice versa // If the old method is static but the new one is non static // If the old method is not abstract but the new is if (!!!Modifier.isFinal(md.getAccess()) && !!!Modifier.isStatic(md.getAccess()) && Modifier.isFinal(new_md.getAccess())) { compatible = false; reasons.add(prefix + " was not final but has been changed to be final."); } if (Modifier.isStatic(md.getAccess()) != Modifier.isStatic(new_md.getAccess())) { compatible = false; reasons.add(prefix + " has changed from static to non-static or vice versa."); } if ((Modifier.isAbstract(new_md.getAccess())) && (!Modifier.isAbstract(md.getAccess()))) { compatible = false; reasons.add(prefix + " has changed from non abstract to abstract."); } if (SemanticVersioningUtils.isLessAccessible(md, new_md)) { compatible = false; reasons.add(prefix + " is less accessible."); } if (compatible) { // remove from the extra map Collection<MethodDeclaration> mds = extraMethods.get(methodName); mds.remove(new_md); extraMethods.put(methodName, mds); continue outer; } } } } // // if we are here, it means that we have not found the method with the same description and signature // which means that the method has been deleted. Let's make sure it is not moved to its upper chain. if (!found) { if (!isMethodInSuperClass(md)) { compatible = false; reasons.add(prefix + " has been deleted or its return type or parameter list has changed."); } else { if (newMDSigs != null) { for (MethodDeclaration new_md : newMDSigs) { // find the method with the same return type, parameter list if ((md.equals(new_md))) { Collection<MethodDeclaration> mds = extraMethods.get(methodName); mds.remove(new_md); extraMethods.put(methodName, mds); } } } } } } } } // Check the newly added method has not caused binary incompatibility for (Map.Entry<String, Collection<MethodDeclaration>> extraMethodSet : extraMethods.entrySet()) { for (MethodDeclaration md : extraMethodSet.getValue()) { String head = "The " + SemanticVersioningUtils.getReadableMethodSignature(md.getName(), md.getDesc()); isNewMethodSpecialCase(md, head, reasons); } } } /** * Return the newly added fields * * @param old old class declaration * @return FieldDeclarations for fields added to new class */ public Collection<FieldDeclaration> getExtraFields(ClassDeclaration old) { Map<String, FieldDeclaration> oldFields = old.getAllFields(); Map<String, FieldDeclaration> newFields = getAllFields(); Map<String, FieldDeclaration> extraFields = new HashMap<String, FieldDeclaration>(newFields); for (String key : oldFields.keySet()) { extraFields.remove(key); } return extraFields.values(); } /** * Return the extra non-private methods * * @param old old class declaration * @return method declarations for methods added to new class */ public Collection<MethodDeclaration> getExtraMethods(ClassDeclaration old) { // Need to find whether there are new methods added. Collection<MethodDeclaration> extraMethods = new HashSet<MethodDeclaration>(); Map<String, Set<MethodDeclaration>> currMethodsMap = getAllMethods(); Map<String, Set<MethodDeclaration>> oldMethodsMap = old.getAllMethods(); for (Map.Entry<String, Set<MethodDeclaration>> currMethod : currMethodsMap.entrySet()) { String methodName = currMethod.getKey(); Collection<MethodDeclaration> newMethods = currMethod.getValue(); // for each method, we look for whether it exists in the old class Collection<MethodDeclaration> oldMethods = oldMethodsMap.get(methodName); for (MethodDeclaration new_md : newMethods) { if (!new_md.isPrivate()) { if (oldMethods == null) { extraMethods.add(new_md); } else { if (!oldMethods.contains(new_md)) { extraMethods.add(new_md); } } } } } return extraMethods; } public boolean isMethodInSuperClass(MethodDeclaration md) { // scan the super class and interfaces String methodName = md.getName(); Collection<MethodDeclaration> overloaddingMethods = getMethodsInUpperChain().get(methodName); if (overloaddingMethods != null) { for (MethodDeclaration value : overloaddingMethods) { // method signature and name same and also the method should not be less accessible if (md.equals(value) && (!!!SemanticVersioningUtils.isLessAccessible(md, value)) && (value.isStatic() == md.isStatic())) { return true; } } } return false; } /** * The newly added method is less accessible than the old one in the super or is a static (respectively instance) method. * * * @param md method declaration * @param prefix beginning of incompatibility message * @param reasons list of binary incompatibility reasons * @return whether new method is less accessible or changed static-ness compared to old class */ private boolean isNewMethodSpecialCase(MethodDeclaration md, String prefix, List<String> reasons) { // scan the super class and interfaces String methodName = md.getName(); boolean special = false; Collection<MethodDeclaration> overloaddingMethods = getMethodsInUpperChain().get(methodName); if (overloaddingMethods != null) { for (MethodDeclaration value : overloaddingMethods) { // method signature and name same and also the method should not be less accessible if (!SemanticVersioningUtils.CONSTRUTOR.equals(md.getName())) { if (md.equals(value)) { if (SemanticVersioningUtils.isLessAccessible(value, md)) { special = true; reasons.add(prefix + " is less accessible than the same method in its parent."); } if (value.isStatic()) { if (!md.isStatic()) { special = true; reasons.add(prefix + " is non-static but the same method in its parent is static."); } } else { if (md.isStatic()) { special = true; reasons.add(prefix + " is static but the same method is its parent is not static."); } } } } } } return special; } private void getAllSuperPresentStatus(ClassDeclaration old, List<String> reasons) { Collection<String> oldSupers = old.getAllSupers(); boolean containsAll = getAllSupers().containsAll(oldSupers); if (!!!containsAll) { oldSupers.removeAll(getAllSupers()); reasons.add("The superclasses or superinterfaces have stopped being super: " + oldSupers.toString() + "."); } } public SerialVersionClassVisitor getSerialVisitor() { return serialVisitor; } }