package japicmp.compat;
import com.google.common.base.Optional;
import japicmp.cmp.JarArchiveComparator;
import japicmp.cmp.JarArchiveComparatorOptions;
import japicmp.exception.JApiCmpException;
import japicmp.model.*;
import japicmp.util.ClassHelper;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import java.util.*;
import java.util.logging.Logger;
import static japicmp.util.ModifierHelper.hasModifierLevelDecreased;
import static japicmp.util.ModifierHelper.isNotPrivate;
import static japicmp.util.ModifierHelper.isSynthetic;
public class CompatibilityChanges {
private final JarArchiveComparator jarArchiveComparator;
public CompatibilityChanges(JarArchiveComparator jarArchiveComparator) {
this.jarArchiveComparator = jarArchiveComparator;
}
public void evaluate(List<JApiClass> classes) {
Map<String, JApiClass> classMap = buildClassMap(classes);
for (JApiClass clazz : classes) {
evaluateBinaryCompatibility(clazz, classMap);
}
}
private Map<String, JApiClass> buildClassMap(List<JApiClass> classes) {
Map<String, JApiClass> classMap = new HashMap<>();
for (JApiClass clazz : classes) {
classMap.put(clazz.getFullyQualifiedName(), clazz);
}
return classMap;
}
private void evaluateBinaryCompatibility(JApiClass jApiClass, Map<String, JApiClass> classMap) {
if (jApiClass.getChangeStatus() == JApiChangeStatus.REMOVED) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.CLASS_REMOVED);
} else if (jApiClass.getChangeStatus() == JApiChangeStatus.MODIFIED) {
// section 13.4.1 of "Java Language Specification" SE7
if (jApiClass.getAbstractModifier().hasChangedFromTo(AbstractModifier.NON_ABSTRACT, AbstractModifier.ABSTRACT)) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.CLASS_NOW_ABSTRACT);
}
// section 13.4.2 of "Java Language Specification" SE7
if (jApiClass.getFinalModifier().hasChangedFromTo(FinalModifier.NON_FINAL, FinalModifier.FINAL)) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.CLASS_NOW_FINAL);
}
// section 13.4.3 of "Java Language Specification" SE7
if (jApiClass.getAccessModifier().hasChangedFrom(AccessModifier.PUBLIC)) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.CLASS_NO_LONGER_PUBLIC);
}
}
// section 13.4.4 of "Java Language Specification" SE7
checkIfSuperclassesOrInterfacesChangedIncompatible(jApiClass, classMap);
checkIfMethodsHaveChangedIncompatible(jApiClass, classMap);
checkIfConstructorsHaveChangedIncompatible(jApiClass, classMap);
checkIfFieldsHaveChangedIncompatible(jApiClass, classMap);
if (jApiClass.getClassType().getChangeStatus() == JApiChangeStatus.MODIFIED) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.CLASS_TYPE_CHANGED);
}
}
private void checkIfFieldsHaveChangedIncompatible(JApiClass jApiClass, Map<String, JApiClass> classMap) {
for (final JApiField field : jApiClass.getFields()) {
// section 13.4.6 of "Java Language Specification" SE7
if (isNotPrivate(field) && field.getChangeStatus() == JApiChangeStatus.REMOVED) {
ArrayList<Integer> returnValues = new ArrayList<>();
forAllSuperclasses(jApiClass, classMap, returnValues, new OnSuperclassCallback<Integer>() {
@Override
public Integer callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
int movedToSuperclass = 0;
for (JApiField superclassField : superclass.getFields()) {
if (superclassField.getName().equals(field.getName()) && fieldTypeMatches(superclassField, field) && isNotPrivate(superclassField)) {
movedToSuperclass = 1;
}
}
return movedToSuperclass;
}
});
boolean movedToSuperclass = false;
for (Integer returnValue : returnValues) {
if (returnValue == 1) {
movedToSuperclass = true;
}
}
if (!movedToSuperclass) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_REMOVED);
}
}
// section 13.4.7 of "Java Language Specification" SE7
if (hasModifierLevelDecreased(field)) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_LESS_ACCESSIBLE);
}
// section 13.4.8 of "Java Language Specification" SE7
if (isNotPrivate(field) && field.getChangeStatus() == JApiChangeStatus.NEW) {
ArrayList<Integer> returnValues = new ArrayList<>();
forAllSuperclasses(jApiClass, classMap, returnValues, new OnSuperclassCallback<Integer>() {
@Override
public Integer callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
int changedIncompatible = 0;
for (JApiField superclassField : superclass.getFields()) {
if (superclassField.getName().equals(field.getName()) && fieldTypeMatches(superclassField, field)) {
boolean superclassFieldIsStatic = false;
boolean subclassFieldIsStatic = false;
boolean accessModifierSubclassLess = false;
if (field.getStaticModifier().getNewModifier().isPresent() && field.getStaticModifier().getNewModifier().get() == StaticModifier.STATIC) {
subclassFieldIsStatic = true;
}
if (superclassField.getStaticModifier().getNewModifier().isPresent() && superclassField.getStaticModifier().getNewModifier().get() == StaticModifier.STATIC && superclassField.getChangeStatus() != JApiChangeStatus.NEW) {
superclassFieldIsStatic = true;
}
if (field.getAccessModifier().getNewModifier().isPresent() && superclassField.getAccessModifier().getNewModifier().isPresent()) {
if (field.getAccessModifier().getNewModifier().get().getLevel() < superclassField.getAccessModifier().getNewModifier().get().getLevel() && superclassField.getChangeStatus() != JApiChangeStatus.NEW) {
accessModifierSubclassLess = true;
}
}
if (superclassFieldIsStatic) {
if (subclassFieldIsStatic) {
changedIncompatible = 1;
}
}
if (accessModifierSubclassLess) {
changedIncompatible = 2;
}
}
}
return changedIncompatible;
}
});
for (int returnValue : returnValues) {
if (returnValue == 1) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_STATIC_AND_OVERRIDES_STATIC);
} else if (returnValue == 2) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_LESS_ACCESSIBLE_THAN_IN_SUPERCLASS);
}
}
}
// section 13.4.9 of "Java Language Specification" SE7
if (isNotPrivate(field) && field.getFinalModifier().hasChangedFromTo(FinalModifier.NON_FINAL, FinalModifier.FINAL)) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_NOW_FINAL);
}
// section 13.4.10 of "Java Language Specification" SE7
if (isNotPrivate(field)) {
if (field.getStaticModifier().hasChangedFromTo(StaticModifier.NON_STATIC, StaticModifier.STATIC)) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_NOW_STATIC);
}
if (field.getStaticModifier().hasChangedFromTo(StaticModifier.STATIC, StaticModifier.NON_STATIC)) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_NO_LONGER_STATIC);
}
}
if (isNotPrivate(field) && field.getType().hasChanged()) {
addCompatibilityChange(field, JApiCompatibilityChange.FIELD_TYPE_CHANGED);
}
}
}
private interface OnSuperclassCallback<T> {
T callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass);
}
private interface OnImplementedInterfaceCallback<T> {
T callback(JApiClass implementedInterface, Map<String, JApiClass> classMap);
}
private <T> void forAllSuperclasses(JApiClass jApiClass, Map<String, JApiClass> classMap, List<T> returnValues, OnSuperclassCallback<T> onSuperclassCallback) {
JApiSuperclass superclass = jApiClass.getSuperclass();
if (superclass.getNewSuperclassName().isPresent()) {
String newSuperclassName = superclass.getNewSuperclassName().get();
JApiClass foundClass = classMap.get(newSuperclassName);
if (foundClass == null) {
Optional<JApiClass> superclassJApiClassOptional = superclass.getJApiClass();
if (superclassJApiClassOptional.isPresent()) {
foundClass = superclassJApiClassOptional.get();
} else {
foundClass = loadClass(newSuperclassName);
evaluate(Collections.singletonList(foundClass));
}
classMap.put(foundClass.getFullyQualifiedName(), foundClass);
}
T returnValue = onSuperclassCallback.callback(foundClass, classMap, superclass.getChangeStatus());
returnValues.add(returnValue);
forAllSuperclasses(foundClass, classMap, returnValues, onSuperclassCallback);
}
}
private JApiClass loadClass(String newSuperclassName) {
JApiClass foundClass;
Optional<CtClass> oldClassOptional = Optional.absent();
Optional<CtClass> newClassOptional = Optional.absent();
JarArchiveComparatorOptions.ClassPathMode classPathMode = this.jarArchiveComparator.getJarArchiveComparatorOptions().getClassPathMode();
if (classPathMode == JarArchiveComparatorOptions.ClassPathMode.ONE_COMMON_CLASSPATH) {
ClassPool classPool = this.jarArchiveComparator.getCommonClassPool();
try {
oldClassOptional = Optional.of(classPool.get(newSuperclassName));
} catch (NotFoundException e) {
if (!this.jarArchiveComparator.getJarArchiveComparatorOptions().getIgnoreMissingClasses().ignoreClass(e.getMessage())) {
throw JApiCmpException.forClassLoading(e, newSuperclassName, this.jarArchiveComparator);
}
}
try {
newClassOptional = Optional.of(classPool.get(newSuperclassName));
} catch (NotFoundException e) {
if (!this.jarArchiveComparator.getJarArchiveComparatorOptions().getIgnoreMissingClasses().ignoreClass(e.getMessage())) {
throw JApiCmpException.forClassLoading(e, newSuperclassName, this.jarArchiveComparator);
}
}
} else {
ClassPool oldClassPool = this.jarArchiveComparator.getOldClassPool();
ClassPool newClassPool = this.jarArchiveComparator.getNewClassPool();
try {
oldClassOptional = Optional.of(oldClassPool.get(newSuperclassName));
} catch (NotFoundException e) {
if (!this.jarArchiveComparator.getJarArchiveComparatorOptions().getIgnoreMissingClasses().ignoreClass(e.getMessage())) {
throw JApiCmpException.forClassLoading(e, newSuperclassName, this.jarArchiveComparator);
}
}
try {
newClassOptional = Optional.of(newClassPool.get(newSuperclassName));
} catch (NotFoundException e) {
if (!this.jarArchiveComparator.getJarArchiveComparatorOptions().getIgnoreMissingClasses().ignoreClass(e.getMessage())) {
throw JApiCmpException.forClassLoading(e, newSuperclassName, this.jarArchiveComparator);
}
}
}
JApiClassType classType;
JApiChangeStatus changeStatus = JApiChangeStatus.UNCHANGED;
if (oldClassOptional.isPresent() && newClassOptional.isPresent()) {
classType = new JApiClassType(Optional.of(ClassHelper.getType(oldClassOptional.get())), Optional.of(ClassHelper.getType(newClassOptional.get())), JApiChangeStatus.UNCHANGED);
} else if (oldClassOptional.isPresent() && !newClassOptional.isPresent()) {
classType = new JApiClassType(Optional.of(ClassHelper.getType(oldClassOptional.get())), Optional.<JApiClassType.ClassType>absent(), JApiChangeStatus.REMOVED);
} else if (!oldClassOptional.isPresent() && newClassOptional.isPresent()) {
classType = new JApiClassType(Optional.<JApiClassType.ClassType>absent(), Optional.of(ClassHelper.getType(newClassOptional.get())), JApiChangeStatus.NEW);
} else {
classType = new JApiClassType(Optional.<JApiClassType.ClassType>absent(), Optional.<JApiClassType.ClassType>absent(), JApiChangeStatus.UNCHANGED);
}
foundClass = new JApiClass(this.jarArchiveComparator, newSuperclassName, oldClassOptional, newClassOptional, changeStatus, classType);
return foundClass;
}
private boolean fieldTypeMatches(JApiField field1, JApiField field2) {
boolean matches = true;
JApiType type1 = field1.getType();
JApiType type2 = field2.getType();
if (type1.getNewTypeOptional().isPresent() && type2.getNewTypeOptional().isPresent()) {
if (!type1.getNewTypeOptional().get().equals(type2.getNewTypeOptional().get())) {
matches = false;
}
}
return matches;
}
private void checkIfConstructorsHaveChangedIncompatible(JApiClass jApiClass, Map<String, JApiClass> classMap) {
for (JApiConstructor constructor : jApiClass.getConstructors()) {
// section 13.4.6 of "Java Language Specification" SE7
if (isNotPrivate(constructor) && constructor.getChangeStatus() == JApiChangeStatus.REMOVED) {
addCompatibilityChange(constructor, JApiCompatibilityChange.CONSTRUCTOR_REMOVED);
}
// section 13.4.7 of "Java Language Specification" SE7
if (hasModifierLevelDecreased(constructor)) {
addCompatibilityChange(constructor, JApiCompatibilityChange.CONSTRUCTOR_LESS_ACCESSIBLE);
}
}
}
private void checkIfMethodsHaveChangedIncompatible(JApiClass jApiClass, Map<String, JApiClass> classMap) {
for (final JApiMethod method : jApiClass.getMethods()) {
// section 13.4.6 of "Java Language Specification" SE7
if (isNotPrivate(method) && method.getChangeStatus() == JApiChangeStatus.REMOVED) {
final List<Integer> returnValues = new ArrayList<>();
forAllSuperclasses(jApiClass, classMap, returnValues, new OnSuperclassCallback<Integer>() {
@Override
public Integer callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
for (JApiMethod superMethod : superclass.getMethods()) {
if (superMethod.getName().equals(method.getName()) && superMethod.hasSameParameter(method) && superMethod.hasSameReturnType(method)) {
return 1;
}
}
return 0;
}
});
checkIfMethodHasBeenPulledUp(jApiClass, classMap, method, returnValues);
boolean superclassHasSameMethod = false;
for (Integer returnValue : returnValues) {
if (returnValue == 1) {
superclassHasSameMethod = true;
}
}
if (!superclassHasSameMethod) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_REMOVED);
}
}
// section 13.4.7 of "Java Language Specification" SE7
if (hasModifierLevelDecreased(method)) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_LESS_ACCESSIBLE);
}
// section 13.4.12 of "Java Language Specification" SE7
if (isNotPrivate(method) && method.getChangeStatus() == JApiChangeStatus.NEW) {
List<Integer> returnValues = new ArrayList<>();
forAllSuperclasses(jApiClass, classMap, returnValues, new OnSuperclassCallback<Integer>() {
@Override
public Integer callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
for (JApiMethod superMethod : superclass.getMethods()) {
if (superMethod.getName().equals(method.getName()) && superMethod.hasSameParameter(method) && superMethod.hasSameReturnType(method)) {
if (superMethod.getAccessModifier().getNewModifier().isPresent() && method.getAccessModifier().getNewModifier().isPresent()) {
if (superMethod.getAccessModifier().getNewModifier().get().getLevel() > method.getAccessModifier().getNewModifier().get().getLevel()) {
return 1;
}
}
if (superMethod.getStaticModifier().getNewModifier().isPresent() && method.getStaticModifier().getNewModifier().isPresent()) {
if (superMethod.getStaticModifier().getNewModifier().get() == StaticModifier.NON_STATIC
&& method.getStaticModifier().getNewModifier().get() == StaticModifier.STATIC) {
return 2;
}
}
}
}
return 0;
}
});
for (Integer returnValue : returnValues) {
if (returnValue == 1) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_LESS_ACCESSIBLE_THAN_IN_SUPERCLASS);
} else if (returnValue == 2) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_IS_STATIC_AND_OVERRIDES_NOT_STATIC);
}
}
}
// section 13.4.15 of "Java Language Specification" SE7 (Method Result Type)
if (method.getReturnType().getChangeStatus() == JApiChangeStatus.MODIFIED) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_RETURN_TYPE_CHANGED);
}
// section 13.4.16 of "Java Language Specification" SE7
if (isNotPrivate(method) && method.getAbstractModifier().hasChangedFromTo(AbstractModifier.NON_ABSTRACT, AbstractModifier.ABSTRACT)) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_NOW_ABSTRACT);
}
// section 13.4.17 of "Java Language Specification" SE7
if (isNotPrivate(method) && method.getFinalModifier().hasChangedFromTo(FinalModifier.NON_FINAL, FinalModifier.FINAL)) {
if (!(method.getStaticModifier().getOldModifier().isPresent() && method.getStaticModifier().getOldModifier().get() == StaticModifier.STATIC)) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_NOW_FINAL);
}
}
// section 13.4.18 of "Java Language Specification" SE7
if (isNotPrivate(method)) {
if (method.getStaticModifier().hasChangedFromTo(StaticModifier.NON_STATIC, StaticModifier.STATIC)) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_NOW_STATIC);
}
if (method.getStaticModifier().hasChangedFromTo(StaticModifier.STATIC, StaticModifier.NON_STATIC)) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_NO_LONGER_STATIC);
}
}
checkAbstractMethod(jApiClass, classMap, method);
checkIfExceptionIsNowChecked(method);
}
}
private void checkAbstractMethod(JApiClass jApiClass, Map<String, JApiClass> classMap, JApiMethod method) {
if (isInterface(jApiClass)) {
if (jApiClass.getChangeStatus() != JApiChangeStatus.NEW) {
if (method.getChangeStatus() == JApiChangeStatus.NEW && !isSynthetic(method)) {
List<JApiMethod> implementedMethods = getImplementedMethods(jApiClass, classMap, method);
if (implementedMethods.size() == 0) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_ADDED_TO_INTERFACE);
} else {
boolean allNew = true;
for (JApiMethod jApiMethod : implementedMethods) {
if (jApiMethod.getChangeStatus() != JApiChangeStatus.NEW) {
allNew = false;
}
}
if (allNew) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_ADDED_TO_INTERFACE);
}
}
}
}
} else {
if (isAbstract(method)) {
if (jApiClass.getChangeStatus() != JApiChangeStatus.NEW) {
if (method.getChangeStatus() == JApiChangeStatus.NEW && !isSynthetic(method)) {
List<JApiMethod> overriddenMethods = getOverriddenMethods(jApiClass, classMap, method);
boolean overridesAbstract = false;
for (JApiMethod jApiMethod : overriddenMethods) {
if (isAbstract(jApiMethod) && jApiMethod.getChangeStatus() != JApiChangeStatus.NEW) {
overridesAbstract = true;
}
}
List<JApiMethod> implementedMethods = getImplementedMethods(jApiClass, classMap, method);
if (implementedMethods.size() > 0) {
overridesAbstract = true;
}
if (!overridesAbstract) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_ABSTRACT_ADDED_TO_CLASS);
}
}
}
}
}
}
private List<JApiMethod> getOverriddenMethods(final JApiClass jApiClass, final Map<String, JApiClass> classMap, final JApiMethod method) {
ArrayList<JApiMethod> jApiMethods = new ArrayList<>();
forAllSuperclasses(jApiClass, classMap, jApiMethods, new OnSuperclassCallback<JApiMethod>() {
@Override
public JApiMethod callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
for (JApiMethod jApiMethod : superclass.getMethods()) {
if (isAbstract(jApiMethod)) {
if (jApiMethod.getName().equals(method.getName()) && jApiMethod.hasSameSignature(method)) {
return jApiMethod;
}
}
}
return null;
}
});
return removeNullValues(jApiMethods);
}
private List<JApiMethod> removeNullValues(ArrayList<JApiMethod> jApiMethods) {
ArrayList<JApiMethod> returnValues = new ArrayList<>();
for (JApiMethod jApiMethod : jApiMethods) {
if (jApiMethod != null) {
returnValues.add(jApiMethod);
}
}
return returnValues;
}
private List<JApiMethod> getImplementedMethods(final JApiClass jApiClass, final Map<String, JApiClass> classMap, final JApiMethod method) {
ArrayList<JApiMethod> jApiMethods = new ArrayList<>();
forAllImplementedInterfaces(jApiClass, classMap, jApiMethods, new OnImplementedInterfaceCallback<JApiMethod>() {
@Override
public JApiMethod callback(JApiClass implementedInterface, Map<String, JApiClass> classMap) {
for (JApiMethod jApiMethod : implementedInterface.getMethods()) {
if (isAbstract(jApiMethod)) {
if (jApiMethod.getName().equals(method.getName()) && jApiMethod.hasSameSignature(method)) {
return jApiMethod;
}
}
}
return null;
}
});
return removeNullValues(jApiMethods);
}
private boolean isAbstract(JApiHasAbstractModifier jApiHasAbstractModifier) {
boolean isAbstract = false;
if (jApiHasAbstractModifier.getAbstractModifier().hasChangedTo(AbstractModifier.ABSTRACT)) {
isAbstract = true;
}
return isAbstract;
}
private void checkIfExceptionIsNowChecked(JApiMethod method) {
for (JApiException exception : method.getExceptions()) {
if (exception.getChangeStatus() == JApiChangeStatus.NEW && exception.isCheckedException() && method.getChangeStatus() != JApiChangeStatus.NEW) {
addCompatibilityChange(method, JApiCompatibilityChange.METHOD_NOW_THROWS_CHECKED_EXCEPTION);
}
}
}
private boolean isInterface(JApiClass jApiClass) {
return jApiClass.getClassType().getNewTypeOptional().isPresent() && jApiClass.getClassType().getNewTypeOptional().get() == JApiClassType.ClassType.INTERFACE;
}
private void checkIfMethodHasBeenPulledUp(JApiClass jApiClass, Map<String, JApiClass> classMap, final JApiMethod method, List<Integer> returnValues) {
forAllImplementedInterfaces(jApiClass, classMap, returnValues, new OnImplementedInterfaceCallback<Integer>() {
@Override
public Integer callback(JApiClass implementedInterface, Map<String, JApiClass> classMap) {
for (JApiMethod superMethod : implementedInterface.getMethods()) {
if (superMethod.getName().equals(method.getName()) && superMethod.hasSameParameter(method) && superMethod.hasSameReturnType(method)) {
return 1;
}
}
return 0;
}
});
}
private <T> void forAllImplementedInterfaces(JApiClass jApiClass, Map<String, JApiClass> classMap, List<T> returnValues, OnImplementedInterfaceCallback<T> onImplementedInterfaceCallback) {
List<JApiImplementedInterface> interfaces = jApiClass.getInterfaces();
for (JApiImplementedInterface implementedInterface : interfaces) {
String fullyQualifiedName = implementedInterface.getFullyQualifiedName();
JApiClass foundClass = classMap.get(fullyQualifiedName);
if (foundClass != null) {
T returnValue = onImplementedInterfaceCallback.callback(foundClass, classMap);
returnValues.add(returnValue);
forAllImplementedInterfaces(foundClass, classMap, returnValues, onImplementedInterfaceCallback);
} else {
//TODO
}
}
}
private void checkIfSuperclassesOrInterfacesChangedIncompatible(final JApiClass jApiClass, Map<String, JApiClass> classMap) {
final JApiSuperclass superclass = jApiClass.getSuperclass();
// section 13.4.4 of "Java Language Specification" SE7
if (superclass.getChangeStatus() == JApiChangeStatus.UNCHANGED
|| superclass.getChangeStatus() == JApiChangeStatus.MODIFIED
|| superclass.getChangeStatus() == JApiChangeStatus.REMOVED) {
final List<JApiMethod> implementedMethods = new ArrayList<>();
final List<JApiMethod> removedAndNotOverriddenMethods = new ArrayList<>();
final List<JApiField> fields = new ArrayList<>();
final List<JApiField> removedAndNotOverriddenFields = new ArrayList<>();
for (JApiMethod jApiMethod : jApiClass.getMethods()) {
if (!isAbstract(jApiMethod) && jApiMethod.getChangeStatus() != JApiChangeStatus.REMOVED && isNotPrivate(jApiMethod)) {
implementedMethods.add(jApiMethod);
}
}
for (JApiField jApiField : jApiClass.getFields()) {
if (jApiField.getChangeStatus() != JApiChangeStatus.REMOVED && isNotPrivate(jApiField)) {
fields.add(jApiField);
}
}
forAllSuperclasses(jApiClass, classMap, new ArrayList<Integer>(), new OnSuperclassCallback<Integer>() {
@Override
public Integer callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
for (JApiMethod jApiMethod : superclass.getMethods()) {
if (!isAbstract(jApiMethod) && jApiMethod.getChangeStatus() != JApiChangeStatus.REMOVED && isNotPrivate(jApiMethod)) {
implementedMethods.add(jApiMethod);
}
}
for (JApiField jApiField : superclass.getFields()) {
if (jApiField.getChangeStatus() != JApiChangeStatus.REMOVED && isNotPrivate(jApiField)) {
fields.add(jApiField);
}
}
for (JApiMethod jApiMethod : superclass.getMethods()) {
if (jApiMethod.getChangeStatus() == JApiChangeStatus.REMOVED) {
boolean implemented = false;
for (JApiMethod implementedMethod : implementedMethods) {
if (jApiMethod.getName().equals(implementedMethod.getName()) && jApiMethod.hasSameSignature(implementedMethod)) {
implemented = true;
break;
}
}
if (!implemented) {
removedAndNotOverriddenMethods.add(jApiMethod);
}
}
}
for (JApiField jApiField : superclass.getFields()) {
if (jApiField.getChangeStatus() == JApiChangeStatus.REMOVED) {
boolean overridden = false;
for (JApiField field : fields) {
if (field.getName().equals(jApiField.getName()) && hasSameType(jApiField, field)) {
overridden = true;
}
}
if (!overridden) {
removedAndNotOverriddenFields.add(jApiField);
}
}
}
return 0;
}
});
if (removedAndNotOverriddenMethods.size() > 0) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.METHOD_REMOVED_IN_SUPERCLASS);
}
if (removedAndNotOverriddenFields.size() > 0) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.FIELD_REMOVED_IN_SUPERCLASS);
}
if (superclass.getOldSuperclassName().isPresent() && superclass.getNewSuperclassName().isPresent()) {
if (!superclass.getOldSuperclassName().get().equals(superclass.getNewSuperclassName().get())) {
boolean superClassChangedToObject = false;
boolean superClassChangedFromObject = false;
if (!superclass.getOldSuperclassName().get().equals("java.lang.Object") && superclass.getNewSuperclassName().get().equals("java.lang.Object")) {
superClassChangedToObject = true;
}
if (superclass.getOldSuperclassName().get().equals("java.lang.Object") && !superclass.getNewSuperclassName().get().equals("java.lang.Object")) {
superClassChangedFromObject = true;
}
if (superClassChangedToObject) {
addCompatibilityChange(superclass, JApiCompatibilityChange.SUPERCLASS_REMOVED);
} else if (superClassChangedFromObject) {
addCompatibilityChange(superclass, JApiCompatibilityChange.SUPERCLASS_ADDED);
} else {
// check if the old superclass is still an ancestor of the new superclass
List<JApiSuperclass> ancestors = new ArrayList<>();
final List<JApiSuperclass> matchingAncestors = new ArrayList<>();
forAllSuperclasses(jApiClass, classMap, ancestors, new OnSuperclassCallback<JApiSuperclass>() {
@Override
public JApiSuperclass callback(JApiClass clazz, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
JApiSuperclass ancestor = clazz.getSuperclass();
if (ancestor.getNewSuperclassName().isPresent() && ancestor.getNewSuperclassName().get().equals(superclass.getOldSuperclassName().get())) {
matchingAncestors.add(ancestor);
}
return ancestor;
}
});
if (matchingAncestors.isEmpty()) {
addCompatibilityChange(superclass, JApiCompatibilityChange.SUPERCLASS_REMOVED);
} else {
// really, superclass(es) inserted - but the old superclass is still an ancestor
addCompatibilityChange(superclass, JApiCompatibilityChange.SUPERCLASS_ADDED);
}
}
}
} else {
if (superclass.getOldSuperclassName().isPresent()) {
addCompatibilityChange(superclass, JApiCompatibilityChange.SUPERCLASS_REMOVED);
} else if (superclass.getNewSuperclassName().isPresent()) {
addCompatibilityChange(superclass, JApiCompatibilityChange.SUPERCLASS_ADDED);
}
}
}
// section 13.4.4 of "Java Language Specification" SE7
for (JApiImplementedInterface implementedInterface : jApiClass.getInterfaces()) {
if (implementedInterface.getChangeStatus() == JApiChangeStatus.REMOVED) {
addCompatibilityChange(implementedInterface, JApiCompatibilityChange.INTERFACE_REMOVED);
} else {
JApiClass interfaceClass = classMap.get(implementedInterface.getFullyQualifiedName());
if (interfaceClass == null) {
interfaceClass = loadClass(implementedInterface.getFullyQualifiedName());
}
if (implementedInterface.getChangeStatus() == JApiChangeStatus.MODIFIED || implementedInterface.getChangeStatus() == JApiChangeStatus.UNCHANGED) {
implementedInterface.setJApiClass(interfaceClass);
checkIfMethodsHaveChangedIncompatible(interfaceClass, classMap);
checkIfFieldsHaveChangedIncompatible(interfaceClass, classMap);
} else if (implementedInterface.getChangeStatus() == JApiChangeStatus.NEW) {
if (interfaceClass.getMethods().size() > 0) { //no marker interface
boolean allInterfaceMethodsImplemented = true;
for (JApiMethod interfaceMethod : interfaceClass.getMethods()) {
boolean interfaceMethodImplemented = false;
if (isSynthetic(interfaceMethod)) {
continue;
}
for (JApiMethod classMethod : jApiClass.getMethods()) {
if (classMethod.getName().equals(interfaceMethod.getName()) && classMethod.hasSameSignature(interfaceMethod)) {
interfaceMethodImplemented = true;
break;
}
}
if (!interfaceMethodImplemented) {
allInterfaceMethodsImplemented = false;
break;
}
}
if (!allInterfaceMethodsImplemented) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.INTERFACE_ADDED);
}
}
}
}
}
checkIfClassNowCheckedException(jApiClass);
checkIfAbstractMethodAddedInSuperclass(jApiClass, classMap);
checkIfAbstraceMethodAdded(jApiClass, classMap);
}
private boolean hasSameType(JApiField field, JApiField otherField) {
boolean hasSameNewType = false;
if (field.getType().getNewTypeOptional().isPresent() && otherField.getType().getNewTypeOptional().isPresent()) {
hasSameNewType = field.getType().getNewTypeOptional().get().equals(otherField.getType().getNewTypeOptional().get());
} else if (field.getType().getOldTypeOptional().isPresent() && otherField.getType().getNewTypeOptional().isPresent()) {
hasSameNewType = field.getType().getOldTypeOptional().get().equals(otherField.getType().getNewTypeOptional().get());
} else if (field.getType().getOldTypeOptional().isPresent() && otherField.getType().getOldTypeOptional().isPresent()) {
hasSameNewType = field.getType().getOldTypeOptional().get().equals(otherField.getType().getOldTypeOptional().get());
}
return hasSameNewType;
}
private void checkIfAbstraceMethodAdded(JApiClass jApiClass, Map<String, JApiClass> classMap) {
if (jApiClass.getChangeStatus() != JApiChangeStatus.NEW && !isAbstract(jApiClass)) {
//TODO compute hull of all methods and check if there are any abstract methods not implemented
}
}
private void checkIfAbstractMethodAddedInSuperclass(final JApiClass jApiClass, Map<String, JApiClass> classMap) {
if (jApiClass.getChangeStatus() != JApiChangeStatus.NEW) {
final List<JApiMethod> abstractMethods = new ArrayList<>();
final List<JApiMethod> implementedMethods = new ArrayList<>();
final List<JApiImplementedInterface> implementedInterfaces = new ArrayList<>();
for (JApiMethod jApiMethod : jApiClass.getMethods()) {
if (!isAbstract(jApiMethod)) {
implementedMethods.add(jApiMethod);
}
}
forAllSuperclasses(jApiClass, classMap, new ArrayList<Integer>(), new OnSuperclassCallback<Integer>() {
@Override
public Integer callback(JApiClass superclass, Map<String, JApiClass> classMap, JApiChangeStatus changeStatusOfSuperclass) {
for (JApiMethod jApiMethod : superclass.getMethods()) {
if (!isAbstract(jApiMethod)) {
implementedMethods.add(jApiMethod);
}
}
for (JApiMethod jApiMethod : superclass.getMethods()) {
if (isAbstract(jApiMethod)) {
boolean isImplemented = false;
for (JApiMethod implementedMethod : implementedMethods) {
if (jApiMethod.getName().equals(implementedMethod.getName()) && jApiMethod.hasSameSignature(implementedMethod)) {
isImplemented = true;
break;
}
}
if (!isImplemented) {
if (jApiMethod.getChangeStatus() == JApiChangeStatus.NEW || changeStatusOfSuperclass == JApiChangeStatus.NEW || changeStatusOfSuperclass == JApiChangeStatus.MODIFIED) {
abstractMethods.add(jApiMethod);
}
}
}
}
for (JApiImplementedInterface jApiImplementedInterface : superclass.getInterfaces()) {
implementedInterfaces.add(jApiImplementedInterface);
}
return 0;
}
});
if (abstractMethods.size() > 0) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.METHOD_ABSTRACT_ADDED_IN_SUPERCLASS);
}
abstractMethods.clear();
for (JApiImplementedInterface jApiImplementedInterface : implementedInterfaces) {
String fullyQualifiedName = jApiImplementedInterface.getFullyQualifiedName();
JApiClass foundClass = classMap.get(fullyQualifiedName);
if (foundClass == null) {
foundClass = loadClass(fullyQualifiedName);
}
for (JApiMethod method : foundClass.getMethods()) {
boolean isImplemented = false;
for (JApiMethod implementedMethod : implementedMethods) {
if (method.getName().equals(implementedMethod.getName()) && method.hasSameSignature(implementedMethod)) {
isImplemented = true;
break;
}
}
if (!isImplemented) {
if (method.getChangeStatus() == JApiChangeStatus.NEW || jApiImplementedInterface.getChangeStatus() == JApiChangeStatus.NEW) {
abstractMethods.add(method);
}
}
}
}
if (abstractMethods.size() > 0) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.METHOD_ABSTRACT_ADDED_IN_IMPLEMENTED_INTERFACE);
}
}
}
private void checkIfClassNowCheckedException(JApiClass jApiClass) {
JApiSuperclass jApiClassSuperclass = jApiClass.getSuperclass();
if (jApiClassSuperclass.getChangeStatus() == JApiChangeStatus.MODIFIED) {
if (jApiClassSuperclass.getNewSuperclassName().isPresent()) {
String fqn = jApiClassSuperclass.getNewSuperclassName().get();
if ("java.lang.Exception".equals(fqn)) {
addCompatibilityChange(jApiClass, JApiCompatibilityChange.CLASS_NOW_CHECKED_EXCEPTION);
}
}
}
}
private void addCompatibilityChange(JApiCompatibility binaryCompatibility, JApiCompatibilityChange compatibilityChange) {
List<JApiCompatibilityChange> compatibilityChanges = binaryCompatibility.getCompatibilityChanges();
if (!compatibilityChanges.contains(compatibilityChange)) {
compatibilityChanges.add(compatibilityChange);
}
}
}