/*
* 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.java;
import static java.util.stream.Collectors.toList;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.lang.model.util.Types;
import org.revapi.API;
import org.revapi.AnalysisContext;
import org.revapi.ApiAnalyzer;
import org.revapi.ArchiveAnalyzer;
import org.revapi.CoIterator;
import org.revapi.CorrespondenceComparatorDeducer;
import org.revapi.DifferenceAnalyzer;
import org.revapi.Element;
import org.revapi.java.compilation.CompilationValve;
import org.revapi.java.compilation.InclusionFilter;
import org.revapi.java.compilation.ProbingEnvironment;
import org.revapi.java.model.JavaElementFactory;
import org.revapi.java.model.MethodElement;
import org.revapi.java.model.TypeElement;
import org.revapi.java.spi.Check;
import org.revapi.java.spi.Util;
/**
* @author Lukas Krejci
* @since 0.1
*/
public final class JavaApiAnalyzer implements ApiAnalyzer {
private final ExecutorService compilationExecutor = Executors.newFixedThreadPool(2, new ThreadFactory() {
private volatile int cnt;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Java API Compilation Thread #" + (++cnt));
}
});
private AnalysisContext analysisContext;
private AnalysisConfiguration configuration;
private final Iterable<Check> checks;
public JavaApiAnalyzer() {
this(ServiceLoader.load(Check.class, JavaApiAnalyzer.class.getClassLoader()));
}
public JavaApiAnalyzer(Iterable<Check> checks) {
this.checks = checks;
}
@Override
public @Nonnull CorrespondenceComparatorDeducer getCorrespondenceDeducer() {
return (l1, l2) -> {
//so, we have to come up with some correspondence order... This is pretty easy for all java elements
//but methods.
if (l1.isEmpty() || l2.isEmpty()) {
return Comparator.naturalOrder();
}
//quickly peek inside to see if there even can be methods in the lists - all of the elements in either list
//will have a common parent and parents of both lists will have the same type or be both null.
Element parent = l1.get(0).getParent();
if (!(parent instanceof TypeElement)) {
return Comparator.naturalOrder();
}
IdentityHashMap<MethodElement, Integer> c1MethodOrder = new IdentityHashMap<>(l1.size());
IdentityHashMap<MethodElement, Integer> c2MethodOrder = new IdentityHashMap<>(l2.size());
//this will reorder the methods in the lists and will also fill in the method order indices in the maps
//so that they can be used for comparisons below
determineOrder(l1, l2, c1MethodOrder, c2MethodOrder);
//and return a comparator
return (e1, e2) -> {
int ret = JavaElementFactory.compareByType(e1, e2);
if (ret != 0) {
return ret;
}
//the only "special" treatment is required for methods - we determined the method order already, so
//let's just look that up.
if (e1 instanceof MethodElement && e2 instanceof MethodElement) {
MethodElement m1 = (MethodElement) e1;
MethodElement m2 = (MethodElement) e2;
return c1MethodOrder.get(m1) - c2MethodOrder.get(m2);
} else {
return e1.compareTo(e2);
}
};
};
}
private static void determineOrder(List<Element> l1, List<Element> l2,
IdentityHashMap<MethodElement, Integer> l1MethodOrder,
IdentityHashMap<MethodElement, Integer> l2MethodOrder) {
TreeMap<String, List<MethodElement>> l1MethodsByName = new TreeMap<>();
TreeMap<String, List<MethodElement>> l2MethodsByName = new TreeMap<>();
int l1MethodsSize = addAllMethods(l1, l1MethodsByName);
int l2MethodsSize = addAllMethods(l2, l2MethodsByName);
//rehash overloads that are present in both collections - those are then reordered using their mutual
//resemblance
int index = 0;
Iterator<Map.Entry<String, List<MethodElement>>> l1MethodsIterator = l1MethodsByName.entrySet().iterator();
Iterator<Map.Entry<String, List<MethodElement>>> l2MethodsIterator = l2MethodsByName.entrySet().iterator();
//iterate over the maps, sorted by name and assign the comparison index to the methods.
//we iterate over the maps sorted by method name
CoIterator<Map.Entry<String, List<MethodElement>>> coit = new CoIterator<>(l1MethodsIterator, l2MethodsIterator,
(e1, e2) -> e1.getKey().compareTo(e2.getKey()));
List<Element> l2MethodsInOrder = new ArrayList<>(l1MethodsSize);
List<Element> l1MethodsInOrder = new ArrayList<>(l2MethodsSize);
while (coit.hasNext()) {
coit.next();
Map.Entry<String, List<MethodElement>> l1e = coit.getLeft();
Map.Entry<String, List<MethodElement>> l2e = coit.getRight();
if (l1e == null) {
//no overloads with the name present in l1
for (MethodElement m : l2e.getValue()) {
l2MethodOrder.put(m, index++);
l2MethodsInOrder.add(m);
}
} else if (l2e == null) {
//no overloads with the name present in l2
for (MethodElement m : l1e.getValue()) {
l1MethodOrder.put(m, index++);
l1MethodsInOrder.add(m);
}
} else {
//overloads of the same name present in both maps
//the lists were already sorted by the method above
List<MethodElement> l1Overloads = l1e.getValue();
List<MethodElement> l2Overloads = l2e.getValue();
if (l1Overloads.size() == 1 && l2Overloads.size() == 1) {
//fast path for hopefully the vast majority of cases
//just indicate the same order for both methods from l1 and l2
MethodElement m1 = l1Overloads.get(0);
MethodElement m2 = l2Overloads.get(0);
l1MethodsInOrder.add(m1);
l2MethodsInOrder.add(m2);
l2MethodOrder.put(m2, index);
l1MethodOrder.put(m1, index++);
} else {
//slow path - for each overload in l1, we need to pick the appropriate one from l2 and put it in the
//same place
List<MethodElement> as = l1Overloads;
List<MethodElement> bs = l2Overloads;
List<Element> aio = l1MethodsInOrder;
List<Element> bio = l2MethodsInOrder;
IdentityHashMap<MethodElement, Integer> ao = l1MethodOrder;
IdentityHashMap<MethodElement, Integer> bo = l2MethodOrder;
if (l1Overloads.size() > l2Overloads.size()) {
as = l2Overloads;
bs = l1Overloads;
aio = l2MethodsInOrder;
bio = l1MethodsInOrder;
ao = l2MethodOrder;
bo = l1MethodOrder;
}
for (MethodElement aMethod : as) {
ao.put(aMethod, index);
aio.add(aMethod);
MethodElement bMethod = removeBestMatch(aMethod, bs);
bo.put(bMethod, index++);
bio.add(bMethod);
}
//add the rest
for (MethodElement m : bs) {
bo.put(m, index++);
bio.add(m);
}
}
}
}
//ok, so now we have the method indices right in the comparison matrices...
//but we also have to reorder the lists themselves to contain the methods in that order so that we
//conform to the restrictions imposed by the co-iteration of the lists during the analysis
//the lists are already sorted in the natural order of the java elements which is first and foremost sorted
//by element type (see org.revapi.java.model.JavaElementFactory). Let's exploit that and just remove all the
//methods in the list and re-add them in the correct order.
reAddSortedMethods(l1, l1MethodsInOrder);
reAddSortedMethods(l2, l2MethodsInOrder);
}
private static void reAddSortedMethods(List<Element> elements, List<Element> sortedMethods) {
int methodRank = JavaElementFactory.getModelTypeRank(MethodElement.class);
int index = 0;
for (; index < elements.size(); ++index) {
Element e = elements.get(index);
if (JavaElementFactory.getModelTypeRank(e.getClass()) >= methodRank) {
break;
}
}
//remove all the method elements
while (index < elements.size()) {
Element e = elements.get(index);
if (e instanceof MethodElement) {
elements.remove(index);
} else {
break;
}
}
//and re-add them in the newly established order
elements.addAll(index, sortedMethods);
}
private static MethodElement removeBestMatch(MethodElement blueprint, List<MethodElement> candidates) {
MethodElement best = null;
float maxScore = 0;
int bestIdx = -1;
List<String> fullBlueprintSignature = methodParamsSignature(blueprint, false);
List<String> erasedBlueprintSignature = methodParamsSignature(blueprint, true);
String fullBlueprintReturnType = Util.toUniqueString(blueprint.getModelRepresentation().getReturnType());
String erasedBlueprintReturnType = Util.toUniqueString(blueprint.getTypeEnvironment().getTypeUtils()
.erasure(blueprint.getModelRepresentation().getReturnType()));
int idx = 0;
for (MethodElement candidate : candidates) {
float score = computeMatchScore(fullBlueprintReturnType, fullBlueprintSignature,
erasedBlueprintReturnType, erasedBlueprintSignature, candidate);
if (maxScore <= score) {
best = candidate;
maxScore = score;
bestIdx = idx;
}
idx++;
}
if (bestIdx != -1) {
candidates.remove(bestIdx);
}
return best;
}
private static List<String> methodParamsSignature(MethodElement method, boolean erased) {
if (erased) {
Types types = method.getTypeEnvironment().getTypeUtils();
return method.getDeclaringElement().getParameters().stream().map(p ->
Util.toUniqueString(types.erasure(p.asType()))).collect(toList());
} else {
return method.getModelRepresentation().getParameterTypes().stream().map(Util::toUniqueString)
.collect(toList());
}
}
private static float computeMatchScore(String blueprintReturnType, List<String> blueprintParamSignature,
String erasedReturnType, List<String> erasedParamSignature, MethodElement method) {
String mRt = Util.toUniqueString(method.getModelRepresentation().getReturnType());
String emRt = Util.toUniqueString(method.getTypeEnvironment().getTypeUtils()
.erasure(method.getModelRepresentation().getReturnType()));
List<String> mPs = methodParamsSignature(method, false);
List<String> emPs = methodParamsSignature(method, true);
//consider the return type as if it was another parameter
int maxParams = Math.max(blueprintParamSignature.size(), mPs.size()) + 1;
int commonParams = longestCommonSubsequenceLength(blueprintParamSignature, mPs,
(blueprintIndex, methodIndex) -> {
String fullBlueprintSig = blueprintParamSignature.get(blueprintIndex);
String erasedBlueprintSig = erasedParamSignature.get(blueprintIndex);
String fullMethodSig = mPs.get(methodIndex);
String erasedMethodSig = emPs.get(methodIndex);
if (fullBlueprintSig.equals(fullMethodSig)) {
return 2;
} else if (erasedBlueprintSig.equals(erasedMethodSig)) {
return 1;
} else {
return 0;
}
});
//consider the return type as if it was another matching parameter
if (blueprintReturnType.equals(mRt)) {
commonParams += 2;
} else if (erasedReturnType.equals(emRt)) {
commonParams += 1;
}
if (maxParams == 1) {
//both methods have no parameters
//we consider that fact a "complete match"
return commonParams + 2;
} else {
//just consider the return type as one of parameters
return ((float) commonParams) / maxParams;
}
}
private static int longestCommonSubsequenceLength(List<?> as, List<?> bs, BiFunction<Integer, Integer, Integer>
matchScoreFunction) {
int[][] lengths = new int[as.size() + 1][bs.size() + 1];
int maxLen = 0;
// row 0 and column 0 are initialized to 0 already
for (int i = 0; i < as.size(); i++) {
for (int j = 0; j < bs.size(); j++) {
int matchScore = matchScoreFunction.apply(i, j);
if (matchScore > 0) {
maxLen = lengths[i + 1][j + 1] = lengths[i][j] + matchScore;
} else {
lengths[i + 1][j + 1] =
Math.max(lengths[i + 1][j], lengths[i][j + 1]);
}
}
}
return maxLen;
}
private static int addAllMethods(Collection<? extends Element> els, TreeMap<String,
List<MethodElement>> methods) {
int ret = 0;
for (Element e : els) {
if (e instanceof MethodElement) {
add((MethodElement) e, methods);
ret++;
}
}
return ret;
}
private static void add(MethodElement method, TreeMap<String, List<MethodElement>> methods) {
String name = method.getDeclaringElement().getSimpleName().toString();
List<MethodElement> overloads = methods.get(name);
if (overloads == null) {
overloads = new ArrayList<>();
methods.put(name, overloads);
}
overloads.add(method);
}
@Nullable
@Override
public String[] getConfigurationRootPaths() {
ArrayList<String> checkConfigPaths = new ArrayList<>();
checkConfigPaths.add("revapi.java");
for (Check c : checks) {
String[] cp = c.getConfigurationRootPaths();
if (cp != null) {
checkConfigPaths.addAll(Arrays.asList(cp));
}
}
String[] configs = new String[checkConfigPaths.size()];
configs = checkConfigPaths.toArray(configs);
return configs;
}
@Nullable
@Override
public Reader getJSONSchema(@Nonnull String configurationRootPath) {
if ("revapi.java".equals(configurationRootPath)) {
return new InputStreamReader(getClass().getResourceAsStream("/META-INF/config-schema.json"),
Charset.forName("UTF-8"));
}
for (Check check : checks) {
String[] roots = check.getConfigurationRootPaths();
if (roots == null) {
continue;
}
for (String root : check.getConfigurationRootPaths()) {
if (configurationRootPath.equals(root)) {
return check.getJSONSchema(root);
}
}
}
return null;
}
@Override
public void initialize(@Nonnull AnalysisContext analysisContext) {
this.analysisContext = analysisContext;
this.configuration = AnalysisConfiguration.fromModel(analysisContext.getConfiguration());
}
@Nonnull
@Override
public ArchiveAnalyzer getArchiveAnalyzer(@Nonnull API api) {
boolean ignoreMissingAnnotations = configuration.isIgnoreMissingAnnotations();
InclusionFilter inclusionFilter = composeInclusionFilter(configuration);
return new JavaArchiveAnalyzer(api, compilationExecutor, configuration.getMissingClassReporting(),
ignoreMissingAnnotations, inclusionFilter);
}
@Nonnull
@Override
public DifferenceAnalyzer getDifferenceAnalyzer(@Nonnull ArchiveAnalyzer oldArchive,
@Nonnull ArchiveAnalyzer newArchive) {
JavaArchiveAnalyzer oldA = (JavaArchiveAnalyzer) oldArchive;
JavaArchiveAnalyzer newA = (JavaArchiveAnalyzer) newArchive;
ProbingEnvironment oldEnvironment = oldA.getProbingEnvironment();
ProbingEnvironment newEnvironment = newA.getProbingEnvironment();
CompilationValve oldValve = oldA.getCompilationValve();
CompilationValve newValve = newA.getCompilationValve();
return new JavaElementDifferenceAnalyzer(analysisContext, oldEnvironment, oldValve, newEnvironment, newValve,
checks, configuration);
}
@Override
public void close() {
compilationExecutor.shutdown();
}
private static InclusionFilter composeInclusionFilter(AnalysisConfiguration config) {
final Set<Pattern> inclClasses = config.getClassInclusionFilters();
final Set<Pattern> exclClasses = config.getClassExclusionFilters();
final Set<Pattern> inclPkgs = config.getPackageInclusionFilters();
final Set<Pattern> exclPkgs = config.getPackageExclusionFilters();
return new InclusionFilter() {
@Override
public boolean accepts(String typeBinaryName, String typeCanonicalName) {
for (Pattern p : inclClasses) {
if (p.matcher(typeCanonicalName).matches()) {
return true;
}
}
int lastDot = typeBinaryName.lastIndexOf('.');
String pkg = lastDot == -1 ? "" : typeBinaryName.substring(0, lastDot);
for (Pattern p : inclPkgs) {
if (p.matcher(pkg).matches()) {
return true;
}
}
return false;
}
@Override
public boolean rejects(String typeBinaryName, String typeCanonicalName) {
for (Pattern p : exclClasses) {
if (p.matcher(typeCanonicalName).matches()) {
return true;
}
}
int lastDot = typeBinaryName.lastIndexOf('.');
String pkg = lastDot == -1 ? "" : typeBinaryName.substring(0, lastDot);
for (Pattern p : exclPkgs) {
if (p.matcher(pkg).matches()) {
return true;
}
}
return false;
}
@Override
public boolean defaultCase() {
return inclClasses.isEmpty() && inclPkgs.isEmpty();
}
};
}
}