DifferencesFactory.java
/*
* Copyright (c) 2001-2024, Jean Tessier
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Jean Tessier nor the names of his contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jeantessier.diff;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import org.apache.logging.log4j.*;
import com.jeantessier.classreader.*;
public class DifferencesFactory {
private Classfile newClass;
private final DifferenceStrategy strategy;
/**
* For tests only.
*/
DifferencesFactory() {
this(new APIDifferenceStrategy(new CodeDifferenceStrategy()));
}
public DifferencesFactory(DifferenceStrategy strategy) {
this.strategy = strategy;
}
public Differences createProjectDifferences(String name, String oldVersion, PackageMapper oldPackages, String newVersion, PackageMapper newPackages) {
LogManager.getLogger(getClass()).debug("Begin {} ({} -> {})", name, oldVersion, newVersion);
ProjectDifferences projectDifferences = new ProjectDifferences(name, oldVersion, newVersion);
LogManager.getLogger(getClass()).debug(" Collecting packages ...");
var packageNames = Stream.concat(
oldPackages.getPackageNames().stream(),
newPackages.getPackageNames().stream())
.distinct()
.sorted()
.toList();
LogManager.getLogger(getClass()).debug(" Diff'ing packages ...");
packageNames.forEach(packageName -> {
Map<String, Classfile> oldPackage = oldPackages.getPackage(packageName);
Map<String, Classfile> newPackage = newPackages.getPackage(packageName);
if (strategy.isPackageDifferent(oldPackage, newPackage)) {
projectDifferences.getPackageDifferences().add(createPackageDifferences(packageName, oldPackage, newPackage));
}
});
LogManager.getLogger(getClass()).debug("End {} ({} -> {})", name, oldVersion, newVersion);
return projectDifferences;
}
public Differences createPackageDifferences(String name, Map<String, Classfile> oldPackage, Map<String, Classfile> newPackage) {
LogManager.getLogger(getClass()).debug("Begin {}", name);
PackageDifferences packageDifferences = new PackageDifferences(name, oldPackage, newPackage);
if (oldPackage != null && !oldPackage.isEmpty() && newPackage != null && !newPackage.isEmpty()) {
LogManager.getLogger(getClass()).debug(" Diff'ing classes ...");
Stream.concat(
oldPackage.keySet().stream(),
newPackage.keySet().stream())
.distinct()
.forEach(className -> {
Classfile oldClass = oldPackage.get(className);
Classfile newClass = newPackage.get(className);
if (strategy.isClassDifferent(oldClass, newClass)) {
packageDifferences.getClassDifferences().add(createClassDifferences(className, oldClass, newClass));
}
});
LogManager.getLogger(getClass()).debug(" {} has {} class(es) that changed.", () -> name, () -> packageDifferences.getClassDifferences().size());
}
LogManager.getLogger(getClass()).debug("End {}", name);
return packageDifferences;
}
public Differences createClassDifferences(String name, Classfile oldClass, Classfile newClass) {
LogManager.getLogger(getClass()).debug("Begin {}", name);
ClassDifferences classDifferences;
if (((oldClass != null) && oldClass.isInterface()) || ((newClass != null) && newClass.isInterface())) {
classDifferences = new InterfaceDifferences(name, oldClass, newClass);
} else {
classDifferences = new ClassDifferences(name, oldClass, newClass);
}
if (!classDifferences.isRemoved() && !classDifferences.isNew() && strategy.isDeclarationModified(oldClass, newClass)) {
classDifferences.setDeclarationModified(true);
}
Differences result = classDifferences;
this.newClass = newClass;
if (oldClass != null && newClass != null) {
LogManager.getLogger(getClass()).debug(" Collecting fields ...");
Map<String, String> fieldLevel = new TreeMap<>();
oldClass.getAllFields().forEach(field -> fieldLevel.put(field.getName(), field.getFullSignature()));
newClass.getAllFields().forEach(field -> fieldLevel.put(field.getName(), field.getFullSignature()));
LogManager.getLogger(getClass()).debug(" Diff'ing fields ...");
fieldLevel.forEach((fieldName, fullSignature) -> {
Predicate<Field_info> predicate = field -> field.getSignature().equals(fieldName);
Field_info oldField = oldClass.getField(predicate);
Field_info newField = newClass.getField(predicate);
if (strategy.isFieldDifferent(oldField, newField)) {
classDifferences.getFeatureDifferences().add(createFeatureDifferences(fullSignature, oldField, newField));
}
});
LogManager.getLogger(getClass()).debug(" Collecting methods ...");
Map<String, String> methodLevel = new TreeMap<>();
oldClass.getAllMethods().forEach(method -> methodLevel.put(method.getSignature(), method.getFullSignature()));
newClass.getAllMethods().forEach(method -> methodLevel.put(method.getSignature(), method.getFullSignature()));
LogManager.getLogger(getClass()).debug(" Diff'ing methods ...");
methodLevel.forEach((signature, fullSignature) -> {
Predicate<Method_info> predicate = method -> method.getSignature().equals(signature);
Method_info oldMethod = oldClass.getMethod(predicate);
Method_info newMethod = newClass.getMethod(predicate);
if (strategy.isMethodDifferent(oldMethod, newMethod)) {
classDifferences.getFeatureDifferences().add(createFeatureDifferences(fullSignature, oldMethod, newMethod));
}
});
LogManager.getLogger(getClass()).debug("{} has {} feature(s) that changed.", () -> name, () -> classDifferences.getFeatureDifferences().size());
if (oldClass.isDeprecated() != newClass.isDeprecated()) {
result = new DeprecatableDifferences(result, oldClass, newClass);
}
}
LogManager.getLogger(getClass()).debug("End {}", name);
return result;
}
public Differences createFeatureDifferences(String name, Feature_info oldFeature, Feature_info newFeature) {
LogManager.getLogger(getClass()).debug("Begin {}", name);
FeatureDifferences featureDifferences;
if (oldFeature instanceof Field_info || newFeature instanceof Field_info) {
featureDifferences = new FieldDifferences(name, (Field_info) oldFeature, (Field_info) newFeature);
if (!featureDifferences.isRemoved() && !featureDifferences.isNew() && strategy.isConstantValueDifferent(((Field_info) oldFeature).getConstantValue(), ((Field_info) newFeature).getConstantValue())) {
((FieldDifferences) featureDifferences).setConstantValueDifference(true);
}
if (featureDifferences.isRemoved() && newClass.locateField(field -> field.getName().equals(name)) != null) {
featureDifferences.setInherited(true);
}
} else {
if (((oldFeature instanceof Method_info) && ((Method_info) oldFeature).isConstructor()) || ((newFeature instanceof Method_info) && ((Method_info) newFeature).isConstructor())) {
featureDifferences = new ConstructorDifferences(name, (Method_info) oldFeature, (Method_info) newFeature);
} else {
featureDifferences = new MethodDifferences(name, (Method_info) oldFeature, (Method_info) newFeature);
}
if (!featureDifferences.isRemoved() && !featureDifferences.isNew() && strategy.isCodeDifferent(((Method_info) oldFeature).getCode(), ((Method_info) newFeature).getCode())) {
((CodeDifferences) featureDifferences).setCodeDifference(true);
}
if (featureDifferences.isRemoved()) {
Method_info attempt = newClass.locateMethod(method -> method.getSignature().equals(name));
if ((attempt != null) && (oldFeature.getClassfile().isInterface() == attempt.getClassfile().isInterface())) {
featureDifferences.setInherited(true);
}
}
}
Differences result = featureDifferences;
if (oldFeature != null && newFeature != null) {
if (oldFeature.isDeprecated() != newFeature.isDeprecated()) {
result = new DeprecatableDifferences(result, oldFeature, newFeature);
}
}
LogManager.getLogger(getClass()).debug("End {}", name);
return result;
}
}