/*
* Copyright 2015 Lukas Krejci
*
* 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.revapi;
import java.io.Reader;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.SortedSet;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.revapi.configuration.Configurable;
import org.revapi.configuration.ConfigurationValidator;
import org.revapi.configuration.ValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The main entry point to the library. The instance of this class is initialized with the different extensions and then
* can run analyses on APIs with different configurations using the {@link #analyze(AnalysisContext)} method.
*
* @author Lukas Krejci
* @since 1.0
*/
public final class Revapi implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(Revapi.class);
static final Logger TIMING_LOG = LoggerFactory.getLogger("revapi.analysis.timing");
private final Set<ApiAnalyzer> availableApiAnalyzers;
private final Set<Reporter> availableReporters;
private final Set<DifferenceTransform<?>> availableTransforms;
private final CompoundFilter availableFilters;
private final ConfigurationValidator configurationValidator;
private final Map<String, List<DifferenceTransform<?>>> matchingTransformsCache = new HashMap<>();
/**
* Use the {@link #builder()} instead.
*
* @param availableApiAnalyzers the set of analyzers to use
* @param availableReporters the set of reporters to use
* @param availableTransforms the set of transforms to use
* @param elementFilters the set of element filters to use
* @throws java.lang.IllegalArgumentException if any of the parameters is null
*/
public Revapi(@Nonnull Set<ApiAnalyzer> availableApiAnalyzers, @Nonnull Set<Reporter> availableReporters,
@Nonnull Set<DifferenceTransform<?>> availableTransforms,
@Nonnull Set<ElementFilter> elementFilters) {
this.availableApiAnalyzers = availableApiAnalyzers;
this.availableReporters = availableReporters;
this.availableTransforms = availableTransforms;
this.availableFilters = new CompoundFilter(elementFilters);
this.configurationValidator = new ConfigurationValidator();
}
@Nonnull
public static Builder builder() {
return new Builder();
}
/**
* Validates the configuration of the analysis context.
*
* @param analysisContext the analysis context
* @return the validation result
*/
public ValidationResult validateConfiguration(@Nonnull AnalysisContext analysisContext) {
ValidationResult validation = ValidationResult.success();
validation = validate(analysisContext, validation, availableFilters);
validation = validate(analysisContext, validation, availableReporters);
validation = validate(analysisContext, validation, availableApiAnalyzers);
validation = validate(analysisContext, validation, availableTransforms);
return validation;
}
/**
* Performs the analysis configured by the given analysis context.
* <p>
* Make sure to call the {@link #close()} method (or perform the analysis in try-with-resources block).
*
* @param analysisContext describes the analysis to be performed
* @throws Exception
*/
public void analyze(@Nonnull AnalysisContext analysisContext) throws Exception {
TIMING_LOG.debug("Analysis starts");
initialize(analysisContext, availableFilters);
initialize(analysisContext, availableReporters);
initialize(analysisContext, availableApiAnalyzers);
initialize(analysisContext, availableTransforms);
TIMING_LOG.debug("Initialization complete.");
matchingTransformsCache.clear();
for (ApiAnalyzer analyzer : availableApiAnalyzers) {
analyzeWith(analyzer, analysisContext.getOldApi(), analysisContext.getNewApi());
}
}
public void close() throws Exception {
TIMING_LOG.debug("Closing all extensions");
closeAll(availableTransforms, "problem transform");
closeAll(availableFilters, "element filters");
closeAll(availableApiAnalyzers, "api analyzer");
closeAll(availableReporters, "reporter");
TIMING_LOG.debug("Extensions closed. Analysis complete.");
TIMING_LOG.debug(Stats.asString());
}
private void initialize(@Nonnull AnalysisContext analysisContext, Iterable<? extends Configurable> configurables) {
for (Configurable c : configurables) {
c.initialize(analysisContext);
}
}
private ValidationResult validate(@Nonnull AnalysisContext analysisContext, ValidationResult validationResult,
Iterable<? extends Configurable> configurables) {
for (Configurable c : configurables) {
ValidationResult partial = configurationValidator.validate(analysisContext.getConfiguration(), c);
validationResult = validationResult.merge(partial);
}
return validationResult;
}
private void closeAll(Iterable<? extends AutoCloseable> closeables, String type) {
for (AutoCloseable c : closeables) {
try {
c.close();
} catch (Exception e) {
LOG.warn("Failed to close " + type + " " + c, e);
}
}
}
private void analyzeWith(ApiAnalyzer apiAnalyzer, API oldApi, API newApi)
throws Exception {
if (TIMING_LOG.isDebugEnabled()) {
TIMING_LOG.debug("Commencing analysis using " + apiAnalyzer + " on:\nOld API:\n" + oldApi + "\n\nNew API:\n"
+ newApi);
}
ArchiveAnalyzer oldAnalyzer = apiAnalyzer.getArchiveAnalyzer(oldApi);
ArchiveAnalyzer newAnalyzer = apiAnalyzer.getArchiveAnalyzer(newApi);
TIMING_LOG.debug("Obtaining API trees.");
ElementForest oldTree = oldAnalyzer.analyze();
ElementForest newTree = newAnalyzer.analyze();
TIMING_LOG.debug("API trees obtained");
DifferenceAnalyzer elementDifferenceAnalyzer = apiAnalyzer.getDifferenceAnalyzer(oldAnalyzer, newAnalyzer);
TIMING_LOG.debug("Obtaining API roots");
SortedSet<? extends Element> as = oldTree.getRoots();
SortedSet<? extends Element> bs = newTree.getRoots();
TIMING_LOG.debug("API roots obtained");
if (LOG.isDebugEnabled()) {
LOG.debug("Old tree: {}", oldTree);
LOG.debug("New tree: {}", newTree);
}
TIMING_LOG.debug("Opening difference analyzer");
elementDifferenceAnalyzer.open();
analyze(apiAnalyzer.getCorrespondenceDeducer(), elementDifferenceAnalyzer, as, bs);
TIMING_LOG.debug("Closing difference analyzer");
elementDifferenceAnalyzer.close();
TIMING_LOG.debug("Difference analyzer closed");
}
private void analyze(CorrespondenceComparatorDeducer deducer, DifferenceAnalyzer elementDifferenceAnalyzer,
SortedSet<? extends Element> as, SortedSet<? extends Element> bs) {
List<Element> sortedAs = new ArrayList<>(as);
List<Element> sortedBs = new ArrayList<>(bs);
Stats.of("sorts").start();
Comparator<? super Element> comp = deducer.sortAndGetCorrespondenceComparator(sortedAs, sortedBs);
Stats.of("sorts").end(sortedAs, sortedBs);
CoIterator<Element> it = new CoIterator<>(sortedAs.iterator(), sortedBs.iterator(), comp);
while (it.hasNext()) {
it.next();
Element a = it.getLeft();
Element b = it.getRight();
Stats.of("filters").start();
boolean analyzeThis =
(a == null || availableFilters.applies(a)) && (b == null || availableFilters.applies(b));
Stats.of("filters").end(a, b);
long beginDuration = 0;
if (analyzeThis) {
Stats.of("analyses").start();
Stats.of("analysisBegins").start();
elementDifferenceAnalyzer.beginAnalysis(a, b);
Stats.of("analysisBegins").end(a, b);
beginDuration = Stats.of("analyses").reset();
}
Stats.of("descends").start();
boolean shouldDescend = a != null && b != null && availableFilters.shouldDescendInto(a) &&
availableFilters.shouldDescendInto(b);
Stats.of("descends").end(a, b);
if (shouldDescend) {
analyze(deducer, elementDifferenceAnalyzer, a.getChildren(), b.getChildren());
}
if (analyzeThis) {
Stats.of("analyses").start();
Stats.of("analysisEnds").start();
Report r = elementDifferenceAnalyzer.endAnalysis(a, b);
Stats.of("analysisEnds").end(a, b);
Stats.of("analyses").end(beginDuration, new AbstractMap.SimpleEntry<>(a, b));
transformAndReport(r);
}
}
}
private void transformAndReport(Report report) {
if (report == null) {
return;
}
Stats.of("transforms").start();
int iteration = 0;
boolean listChanged;
do {
listChanged = false;
ListIterator<Difference> it = report.getDifferences().listIterator();
List<Difference> transformed = new ArrayList<>(1); //this will hopefully be the max of transforms
while (it.hasNext()) {
Difference d = it.next();
transformed.clear();
boolean shouldBeRemoved = false;
boolean differenceChanged = false;
for (DifferenceTransform<?> t : getTransformsForDifference(d)) {
// it is the responsibility of the transform to declare the proper type.
// it will get a ClassCastException if it fails to declare a type that is common to all differences
// it can handle
@SuppressWarnings("unchecked")
DifferenceTransform<Element> tt = (DifferenceTransform<Element>) t;
Difference td = d;
try {
td = tt.transform(report.getOldElement(), report.getNewElement(), d);
} catch (Exception e) {
LOG.warn("Difference transform " + t + " of class '" + t.getClass() + " threw an exception" +
" while processing difference " + d + " on old element " + report.getOldElement() +
" and" +
" new element " + report.getNewElement(), e);
}
// ignore if transformation returned null, meaning that it "swallowed" the difference..
if (td == null) {
shouldBeRemoved = true;
listChanged = true;
differenceChanged = true;
} else if (!d.equals(td)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Difference transform {} transforms {} to {}", t.getClass(), d, td);
}
transformed.add(td);
listChanged = true;
differenceChanged = true;
}
}
if (differenceChanged) {
//we need to remove the element in either case
it.remove();
if (!shouldBeRemoved) {
//if it was not removed, but transformed, let's add the transformed difference in the place of
//our currently removed element
for (Difference td : transformed) {
//this adds the new element *before* the currently pointed to index...
it.add(td);
//we want to check the newly added difference, so we need the iterator to point at it...
it.previous();
}
}
}
}
iteration++;
if (iteration % 100 == 0) {
LOG.warn("Transformation of differences in match report " + report + " has cycled " + iteration +
" times. Maybe we're in an infinite loop with differences transforming back and forth?");
}
if (iteration == Integer.MAX_VALUE) {
throw new IllegalStateException("Transformation failed to settle in " + Integer.MAX_VALUE +
" iterations. This is most probably an error in difference transform configuration that" +
" cycles between two or more changes back and forth.");
}
} while (listChanged);
Stats.of("transforms").end(report);
if (!report.getDifferences().isEmpty()) {
Stats.of("reports").start();
for (Reporter reporter : availableReporters) {
reporter.report(report);
}
Stats.of("reports").end(report);
}
}
private List<DifferenceTransform<?>> getTransformsForDifference(Difference diff) {
List<DifferenceTransform<?>> ret = matchingTransformsCache.get(diff.code);
if (ret == null) {
ret = new ArrayList<>();
for (DifferenceTransform<?> t : availableTransforms) {
for (Pattern p : t.getDifferenceCodePatterns()) {
if (p.matcher(diff.code).matches()) {
ret.add(t);
break;
}
}
}
matchingTransformsCache.put(diff.code, ret);
}
return ret;
}
public static final class Builder {
private Set<ApiAnalyzer> analyzers = null;
private Set<Reporter> reporters = null;
private Set<DifferenceTransform<?>> transforms = null;
private Set<ElementFilter> filters = null;
@Nonnull
public Builder withAnalyzersFromThreadContextClassLoader() {
return withAnalyzers(ServiceLoader.load(ApiAnalyzer.class));
}
@Nonnull
public Builder withAnalyzersFrom(@Nonnull ClassLoader cl) {
return withAnalyzers(ServiceLoader.load(ApiAnalyzer.class, cl));
}
@Nonnull
public Builder withAnalyzers(ApiAnalyzer... analyzers) {
return withAnalyzers(Arrays.asList(analyzers));
}
@Nonnull
public Builder withAnalyzers(@Nonnull Iterable<? extends ApiAnalyzer> analyzers) {
if (this.analyzers == null) {
this.analyzers = new HashSet<>();
}
for (ApiAnalyzer a : analyzers) {
this.analyzers.add(a);
}
return this;
}
@Nonnull
public Builder withReportersFromThreadContextClassLoader() {
return withReporters(ServiceLoader.load(Reporter.class));
}
@Nonnull
public Builder withReportersFrom(@Nonnull ClassLoader cl) {
return withReporters(ServiceLoader.load(Reporter.class, cl));
}
@Nonnull
public Builder withReporters(Reporter... reporters) {
return withReporters(Arrays.asList(reporters));
}
@Nonnull
public Builder withReporters(@Nonnull Iterable<? extends Reporter> reporters) {
if (this.reporters == null) {
this.reporters = new HashSet<>();
}
for (Reporter r : reporters) {
this.reporters.add(r);
}
return this;
}
@Nonnull
public Builder withTransformsFromThreadContextClassLoader() {
//don't you love Java generics? ;)
@SuppressWarnings("rawtypes")
Iterable trs = ServiceLoader.load(DifferenceTransform.class);
@SuppressWarnings("unchecked")
Iterable<DifferenceTransform<?>> rtrs = (Iterable<DifferenceTransform<?>>) trs;
return withTransforms(rtrs);
}
@Nonnull
public Builder withTransformsFrom(@Nonnull ClassLoader cl) {
//don't you love Java generics? ;)
@SuppressWarnings("rawtypes")
Iterable trs = ServiceLoader.load(DifferenceTransform.class, cl);
@SuppressWarnings("unchecked")
Iterable<DifferenceTransform<?>> rtrs = (Iterable<DifferenceTransform<?>>) trs;
return withTransforms(rtrs);
}
@Nonnull
public Builder withTransforms(DifferenceTransform<?>... transforms) {
return withTransforms(Arrays.asList(transforms));
}
@Nonnull
public Builder withTransforms(@Nonnull Iterable<? extends DifferenceTransform<?>> transforms) {
if (this.transforms == null) {
this.transforms = new HashSet<>();
}
for (DifferenceTransform<?> t : transforms) {
this.transforms.add(t);
}
return this;
}
@Nonnull
public Builder withFiltersFromThreadContextClassLoader() {
return withFilters(ServiceLoader.load(ElementFilter.class));
}
@Nonnull
public Builder withFiltersFrom(@Nonnull ClassLoader cl) {
return withFilters(ServiceLoader.load(ElementFilter.class, cl));
}
@Nonnull
public Builder withFilters(ElementFilter... filters) {
return withFilters(Arrays.asList(filters));
}
@Nonnull
public Builder withFilters(@Nonnull Iterable<? extends ElementFilter> filters) {
if (this.filters == null) {
this.filters = new HashSet<>();
}
for (ElementFilter f : filters) {
this.filters.add(f);
}
return this;
}
@Nonnull
public Builder withAllExtensionsFromThreadContextClassLoader() {
return withAllExtensionsFrom(Thread.currentThread().getContextClassLoader());
}
@Nonnull
public Builder withAllExtensionsFrom(@Nonnull ClassLoader cl) {
return withAnalyzersFrom(cl).withFiltersFrom(cl).withReportersFrom(cl)
.withTransformsFrom(cl);
}
/**
* @throws IllegalStateException if there are no api analyzers or no reporters added.
* @return a new Revapi instance
*/
@Nonnull
public Revapi build() throws IllegalStateException {
analyzers = analyzers == null ? Collections.<ApiAnalyzer>emptySet() : analyzers;
reporters = reporters == null ? Collections.<Reporter>emptySet() : reporters;
transforms = transforms == null ? Collections.<DifferenceTransform<?>>emptySet() : transforms;
filters = filters == null ? Collections.<ElementFilter>emptySet() : filters;
if (analyzers.isEmpty()) {
throw new IllegalStateException(
"No API analyzers defined. The analysis cannot run without an analyzer.");
}
if (reporters.isEmpty()) {
throw new IllegalStateException(
"No reporters defined. There is no way how to obtain the results of the analysis without" +
" a reporter.");
}
return new Revapi(analyzers, reporters, transforms, filters);
}
}
private static class CompoundFilter implements ElementFilter, Iterable<ElementFilter> {
private final Collection<? extends ElementFilter> filters;
private final String[] allConfigRoots;
private final Map<String, ElementFilter> rootsToFilters;
private CompoundFilter(Collection<? extends ElementFilter> filters) {
this.filters = filters;
rootsToFilters = new HashMap<>();
List<String> tmp = new ArrayList<>();
for (ElementFilter f : filters) {
String[] roots = f.getConfigurationRootPaths();
if (roots != null) {
for (String root : roots) {
tmp.add(root);
rootsToFilters.put(root, f);
}
}
}
allConfigRoots = tmp.toArray(new String[tmp.size()]);
}
@Override
public void close() throws Exception {
Exception thrown = null;
for (ElementFilter f : filters) {
try {
f.close();
} catch (Exception e) {
if (thrown == null) {
thrown = new Exception("Failed to close some element filters");
}
thrown.addSuppressed(e);
}
}
if (thrown != null) {
throw thrown;
}
}
@Nullable
@Override
public String[] getConfigurationRootPaths() {
return allConfigRoots;
}
@Nullable
@Override
public Reader getJSONSchema(@Nonnull String configurationRootPath) {
ElementFilter f = rootsToFilters.get(configurationRootPath);
return f == null ? null : f.getJSONSchema(configurationRootPath);
}
@Override
public void initialize(@Nonnull AnalysisContext analysisContext) {
for (ElementFilter f : filters) {
f.initialize(analysisContext);
}
}
@Override
public boolean applies(@Nullable Element element) {
for (ElementFilter f : filters) {
String name = f.getClass().getName() + ".applies";
Stats.of(name).start();
boolean applies = f.applies(element);
Stats.of(name).end(element);
if (!applies) {
return false;
}
}
return true;
}
@Override
public boolean shouldDescendInto(@Nullable Object element) {
Iterator<? extends ElementFilter> it = filters.iterator();
boolean hasNoFilters = !it.hasNext();
while (it.hasNext()) {
ElementFilter f = it.next();
String name = f.getClass().getName() + ".shouldDescendInto";
Stats.of(name).start();
boolean should = f.shouldDescendInto(element);
Stats.of(name).end(element);
if (should) {
return true;
}
}
return hasNoFilters;
}
@Override
@SuppressWarnings("unchecked")
public Iterator<ElementFilter> iterator() {
return (Iterator<ElementFilter>) filters.iterator();
}
}
}