/*
* Copyright 2011 William Bernardet
*
* 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 com.googlecode.japi.checker;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.googlecode.japi.checker.Severity;
import com.googlecode.japi.checker.Reporter.Report;
import com.googlecode.japi.checker.model.ClassData;
import com.googlecode.japi.checker.utils.AntPatternMatcher;
/**
* The class driving the backward compatibility checks.
* It handles the Rules, Reporter, dependencies and
* include/exclude mechanism for the checking.
*
* @author william.bernardet@gmail.com
*
*/
public class BCChecker {
private List<File> referenceClasspath = new ArrayList<File>();
private List<File> newArtifactClasspath = new ArrayList<File>();
private List<AntPatternMatcher> includes = new ArrayList<AntPatternMatcher>();
private List<AntPatternMatcher> excludes = new ArrayList<AntPatternMatcher>();
private ClassDataLoaderFactory classDataLoaderFactory = new DefaultClassDataLoaderFactory();
private boolean warnOnDependencyLoadingError;
private Reporter reporter;
private List<Rule> rules = Collections.emptyList();
/**
* Add a path either a jar or a directory to the reference artifact classpath.
* @param path
*/
public void addToReferenceClasspath(File path) {
this.referenceClasspath.add(path);
}
/**
* Add a path either a jar or a directory to the new artifact classpath.
* @param path
*/
public void addToNewArtifactClasspath(File path) {
this.newArtifactClasspath.add(path);
}
/**
* Add an include pattern for the class file scanning.
* e.g: org/myproject/mypackage/api/**/*.class
* @param include
*/
public void addInclude(String include) {
includes.add(new AntPatternMatcher(include));
}
/**
* Add an exclude pattern for the class file scanning.
* e.g: org/myproject/mypackage/api/**/*.class
* @param include
*/
public void addExclude(String exclude) {
excludes.add(new AntPatternMatcher(exclude));
}
/**
* Defines a custom reporter.
* @param reporter
*/
public void setReporter(Reporter reporter) {
this.reporter = reporter;
}
/**
* Get the current reporter.
* @return
*/
public Reporter getReporter() {
return this.reporter;
}
/**
* Defines the rules to apply during the check.
* @param rules
*/
public void setRules(List<Rule> rules) {
if (rules == null) {
rules = Collections.emptyList();
}
this.rules = rules;
}
/**
* Run the check between the reference and the newArtifact.
* @param reference
* @param newArtifact
* @throws IOException
*/
public void checkBacwardCompatibility(File reference, File newArtifact) throws IOException {
if (reference == null) {
throw new IllegalArgumentException("The reference parameter cannot be null.");
}
if (newArtifact == null) {
throw new IllegalArgumentException("The newArtifact parameter cannot be null.");
}
if (!reference.isDirectory() && !Utils.isArchive(reference)) {
throw new IllegalArgumentException("reference must be either a directory" +
" or a jar (or a zip kind of archive) file");
}
if (!newArtifact.isDirectory() && !Utils.isArchive(newArtifact)) {
throw new IllegalArgumentException("new artifact must be either a directory" +
" or a jar (or a zip kind of archive) file");
}
Reporter reporter = this.getReporter();
if (reporter == null) {
// if reporter is not defined just stub it...
reporter = new Reporter() {
@Override
public void report(Report report) { }
};
}
ClassDataLoader referenceDataLoader = classDataLoaderFactory.createClassDataLoader();
reporter.report(new Report(Severity.INFO, "Reading reference artifact: " + reference));
referenceDataLoader.read(reference.toURI());
for (File file : this.referenceClasspath) {
try {
reporter.report(new Report(Severity.INFO, "Reading reference dependency: " + file));
referenceDataLoader.read(file.toURI());
} catch (ReadClassException e){
if (this.shouldWarnOnDependencyLoadingError()) {
reporter.report(new Report(Severity.WARNING, e.getMessage()));
} else {
throw e;
}
}
}
List<ClassData> referenceData = referenceDataLoader.getClasses(reference.toURI(), includes, excludes);
ClassDataLoader newArtifactDataLoader = classDataLoaderFactory.createClassDataLoader();
reporter.report(new Report(Severity.INFO, "Reading artifact: " + newArtifact));
newArtifactDataLoader.read(newArtifact.toURI());
for (File file : this.newArtifactClasspath) {
try {
reporter.report(new Report(Severity.INFO, "Reading dependency: " + file));
newArtifactDataLoader.read(file.toURI());
} catch (ReadClassException e){
if (this.shouldWarnOnDependencyLoadingError()) {
reporter.report(new Report(Severity.WARNING, e.getMessage()));
} else {
throw e;
}
}
}
List<ClassData> newData = newArtifactDataLoader.getClasses(newArtifact.toURI(), includes, excludes);
for (ClassData clazz : referenceData) {
boolean found = false;
for (ClassData newClazz : newData) {
if (clazz.isSame(newClazz)) {
for (Rule rule : rules) {
rule.checkBackwardCompatibility(reporter, clazz, newClazz);
}
newClazz.checkBackwardCompatibility(reporter, clazz, rules);
found = true;
break;
}
}
if (!found && clazz.getVisibility() == Scope.PUBLIC) {
reporter.report(new Report(Severity.ERROR, "Public class " + clazz.getName() + " has been removed.", clazz, null));
}
}
}
/**
* Defines if the checker should just warn on dependency loading error via the reporter,
* or simply fails.
* @param warnOnDependencyLoadingError
*/
public void setWarnOnDependencyLoadingError(boolean warnOnDependencyLoadingError) {
this.warnOnDependencyLoadingError = warnOnDependencyLoadingError;
}
/**
*
* @return
*/
public boolean shouldWarnOnDependencyLoadingError() {
return this.warnOnDependencyLoadingError;
}
}