blob: 418dae42c874b232162465e5de602ca6da49722b [file] [log] [blame]
/*
* Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.sjavac.pubapi;
import static com.sun.tools.sjavac.Util.union;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.element.Modifier;
import com.sun.tools.javac.util.Assert;
import com.sun.tools.javac.util.StringUtils;
public class PubApi implements Serializable {
private static final long serialVersionUID = 5926627347801986850L;
// Used to have Set here. Problem is that the objects are mutated during
// javac_state loading, causing them to change hash codes. We could probably
// change back to Set once javac_state loading is cleaned up.
public final Map<String, PubType> types = new HashMap<>();
public final Map<String, PubVar> variables = new HashMap<>();
public final Map<String, PubMethod> methods = new HashMap<>();
public PubApi() {
}
public PubApi(Collection<PubType> types,
Collection<PubVar> variables,
Collection<PubMethod> methods) {
types.forEach(this::addPubType);
variables.forEach(this::addPubVar);
methods.forEach(this::addPubMethod);
}
// Currently this is implemented as equality. This is far from optimal. It
// should preferably make sure that all previous methods are still available
// and no abstract methods are added. It should also be aware of inheritance
// of course.
public boolean isBackwardCompatibleWith(PubApi older) {
return equals(older);
}
private static String typeLine(PubType type) {
if (type.fqName.isEmpty())
throw new RuntimeException("empty class name " + type);
return String.format("TYPE %s%s", asString(type.modifiers), type.fqName);
}
private static String varLine(PubVar var) {
return String.format("VAR %s%s %s%s",
asString(var.modifiers),
TypeDesc.encodeAsString(var.type),
var.identifier,
var.getConstValue().map(v -> " = " + v).orElse(""));
}
private static String methodLine(PubMethod method) {
return String.format("METHOD %s%s%s %s(%s)%s",
asString(method.modifiers),
method.typeParams.isEmpty() ? "" : ("<" + method.typeParams.stream().map(PubApiTypeParam::asString).collect(Collectors.joining(",")) + "> "),
TypeDesc.encodeAsString(method.returnType),
method.identifier,
commaSeparated(method.paramTypes),
method.throwDecls.isEmpty()
? ""
: " throws " + commaSeparated(method.throwDecls));
}
public List<String> asListOfStrings() {
List<String> lines = new ArrayList<>();
// Types
types.values()
.stream()
.sorted(Comparator.comparing(PubApi::typeLine))
.forEach(type -> {
lines.add(typeLine(type));
for (String subline : type.pubApi.asListOfStrings())
lines.add(" " + subline);
});
// Variables
variables.values()
.stream()
.map(PubApi::varLine)
.sorted()
.forEach(lines::add);
// Methods
methods.values()
.stream()
.map(PubApi::methodLine)
.sorted()
.forEach(lines::add);
return lines;
}
@Override
public boolean equals(Object obj) {
if (getClass() != obj.getClass())
return false;
PubApi other = (PubApi) obj;
return types.equals(other.types)
&& variables.equals(other.variables)
&& methods.equals(other.methods);
}
@Override
public int hashCode() {
return types.keySet().hashCode()
^ variables.keySet().hashCode()
^ methods.keySet().hashCode();
}
private static String commaSeparated(List<TypeDesc> typeDescs) {
return typeDescs.stream()
.map(TypeDesc::encodeAsString)
.collect(Collectors.joining(","));
}
// Create space separated list of modifiers (with a trailing space)
private static String asString(Set<Modifier> modifiers) {
return modifiers.stream()
.map(mod -> mod + " ")
.sorted()
.collect(Collectors.joining());
}
// Used to combine class PubApis to package level PubApis
public static PubApi mergeTypes(PubApi api1, PubApi api2) {
Assert.check(api1.methods.isEmpty(), "Can only merge types.");
Assert.check(api2.methods.isEmpty(), "Can only merge types.");
Assert.check(api1.variables.isEmpty(), "Can only merge types.");
Assert.check(api2.variables.isEmpty(), "Can only merge types.");
PubApi merged = new PubApi();
merged.types.putAll(api1.types);
merged.types.putAll(api2.types);
return merged;
}
// Used for line-by-line parsing
private PubType lastInsertedType = null;
private final static String MODIFIERS = Stream.of(Modifier.values())
.map(Modifier::name)
.map(StringUtils::toLowerCase)
.collect(Collectors.joining("|", "(", ")"));
private final static Pattern MOD_PATTERN = Pattern.compile("(" + MODIFIERS + " )*");
private final static Pattern METHOD_PATTERN = Pattern.compile("(?<ret>.+?) (?<name>\\S+)\\((?<params>.*)\\)( throws (?<throws>.*))?");
private final static Pattern VAR_PATTERN = Pattern.compile("VAR (?<modifiers>("+MODIFIERS+" )*)(?<type>.+?) (?<id>\\S+)( = (?<val>.*))?");
private final static Pattern TYPE_PATTERN = Pattern.compile("TYPE (?<modifiers>("+MODIFIERS+" )*)(?<fullyQualified>\\S+)");
public void appendItem(String l) {
try {
if (l.startsWith(" ")) {
lastInsertedType.pubApi.appendItem(l.substring(2));
return;
}
if (l.startsWith("METHOD")) {
l = l.substring("METHOD ".length());
Set<Modifier> modifiers = new HashSet<>();
Matcher modMatcher = MOD_PATTERN.matcher(l);
if (modMatcher.find()) {
String modifiersStr = modMatcher.group();
modifiers.addAll(parseModifiers(modifiersStr));
l = l.substring(modifiersStr.length());
}
List<PubApiTypeParam> typeParams = new ArrayList<>();
if (l.startsWith("<")) {
int closingPos = findClosingTag(l, 0);
String str = l.substring(1, closingPos);
l = l.substring(closingPos+1);
typeParams.addAll(parseTypeParams(splitOnTopLevelCommas(str)));
}
Matcher mm = METHOD_PATTERN.matcher(l);
if (!mm.matches())
throw new AssertionError("Could not parse return type, identifier, parameter types or throws declaration of method: " + l);
List<String> params = splitOnTopLevelCommas(mm.group("params"));
String th = Optional.ofNullable(mm.group("throws")).orElse("");
List<String> throwz = splitOnTopLevelCommas(th);
PubMethod m = new PubMethod(modifiers,
typeParams,
TypeDesc.decodeString(mm.group("ret")),
mm.group("name"),
parseTypeDescs(params),
parseTypeDescs(throwz));
addPubMethod(m);
return;
}
Matcher vm = VAR_PATTERN.matcher(l);
if (vm.matches()) {
addPubVar(new PubVar(parseModifiers(vm.group("modifiers")),
TypeDesc.decodeString(vm.group("type")),
vm.group("id"),
vm.group("val")));
return;
}
Matcher tm = TYPE_PATTERN.matcher(l);
if (tm.matches()) {
addPubType(new PubType(parseModifiers(tm.group("modifiers")),
tm.group("fullyQualified"),
new PubApi()));
return;
}
throw new AssertionError("No matching line pattern.");
} catch (Throwable e) {
throw new AssertionError("Could not parse API line: " + l, e);
}
}
public void addPubType(PubType t) {
types.put(t.fqName, t);
lastInsertedType = t;
}
public void addPubVar(PubVar v) {
variables.put(v.identifier, v);
}
public void addPubMethod(PubMethod m) {
methods.put(m.asSignatureString(), m);
}
private static List<TypeDesc> parseTypeDescs(List<String> strs) {
return strs.stream()
.map(TypeDesc::decodeString)
.collect(Collectors.toList());
}
private static List<PubApiTypeParam> parseTypeParams(List<String> strs) {
return strs.stream().map(PubApi::parseTypeParam).collect(Collectors.toList());
}
// Parse a type parameter string. Example input:
// identifier
// identifier extends Type (& Type)*
private static PubApiTypeParam parseTypeParam(String typeParamString) {
int extPos = typeParamString.indexOf(" extends ");
if (extPos == -1)
return new PubApiTypeParam(typeParamString, Collections.emptyList());
String identifier = typeParamString.substring(0, extPos);
String rest = typeParamString.substring(extPos + " extends ".length());
List<TypeDesc> bounds = parseTypeDescs(splitOnTopLevelChars(rest, '&'));
return new PubApiTypeParam(identifier, bounds);
}
public Set<Modifier> parseModifiers(String modifiers) {
if (modifiers == null)
return Collections.emptySet();
return Stream.of(modifiers.split(" "))
.map(String::trim)
.map(StringUtils::toUpperCase)
.filter(s -> !s.isEmpty())
.map(Modifier::valueOf)
.collect(Collectors.toSet());
}
// Find closing tag of the opening tag at the given 'pos'.
private static int findClosingTag(String l, int pos) {
while (true) {
pos = pos + 1;
if (l.charAt(pos) == '>')
return pos;
if (l.charAt(pos) == '<')
pos = findClosingTag(l, pos);
}
}
public List<String> splitOnTopLevelCommas(String s) {
return splitOnTopLevelChars(s, ',');
}
public static List<String> splitOnTopLevelChars(String s, char split) {
if (s.isEmpty())
return Collections.emptyList();
List<String> result = new ArrayList<>();
StringBuilder buf = new StringBuilder();
int depth = 0;
for (char c : s.toCharArray()) {
if (c == split && depth == 0) {
result.add(buf.toString().trim());
buf = new StringBuilder();
} else {
if (c == '<') depth++;
if (c == '>') depth--;
buf.append(c);
}
}
result.add(buf.toString().trim());
return result;
}
public boolean isEmpty() {
return types.isEmpty() && variables.isEmpty() && methods.isEmpty();
}
// Used for descriptive debug messages when figuring out what triggers
// recompilation.
public List<String> diff(PubApi prevApi) {
return diff("", prevApi);
}
private List<String> diff(String scopePrefix, PubApi prevApi) {
List<String> diffs = new ArrayList<>();
for (String typeKey : union(types.keySet(), prevApi.types.keySet())) {
PubType type = types.get(typeKey);
PubType prevType = prevApi.types.get(typeKey);
if (prevType == null) {
diffs.add("Type " + scopePrefix + typeKey + " was added");
} else if (type == null) {
diffs.add("Type " + scopePrefix + typeKey + " was removed");
} else {
// Check modifiers
if (!type.modifiers.equals(prevType.modifiers)) {
diffs.add("Modifiers for type " + scopePrefix + typeKey
+ " changed from " + prevType.modifiers + " to "
+ type.modifiers);
}
// Recursively check types pub API
diffs.addAll(type.pubApi.diff(prevType.pubApi));
}
}
for (String varKey : union(variables.keySet(), prevApi.variables.keySet())) {
PubVar var = variables.get(varKey);
PubVar prevVar = prevApi.variables.get(varKey);
if (prevVar == null) {
diffs.add("Variable " + scopePrefix + varKey + " was added");
} else if (var == null) {
diffs.add("Variable " + scopePrefix + varKey + " was removed");
} else {
if (!var.modifiers.equals(prevVar.modifiers)) {
diffs.add("Modifiers for var " + scopePrefix + varKey
+ " changed from " + prevVar.modifiers + " to "
+ var.modifiers);
}
if (!var.type.equals(prevVar.type)) {
diffs.add("Type of " + scopePrefix + varKey
+ " changed from " + prevVar.type + " to "
+ var.type);
}
if (!var.getConstValue().equals(prevVar.getConstValue())) {
diffs.add("Const value of " + scopePrefix + varKey
+ " changed from " + prevVar.getConstValue().orElse("<none>")
+ " to " + var.getConstValue().orElse("<none>"));
}
}
}
for (String methodKey : union(methods.keySet(), prevApi.methods.keySet())) {
PubMethod method = methods.get(methodKey);
PubMethod prevMethod = prevApi.methods.get(methodKey);
if (prevMethod == null) {
diffs.add("Method " + scopePrefix + methodKey + " was added");
} else if (method == null) {
diffs.add("Method " + scopePrefix + methodKey + " was removed");
} else {
if (!method.modifiers.equals(prevMethod.modifiers)) {
diffs.add("Modifiers for method " + scopePrefix + methodKey
+ " changed from " + prevMethod.modifiers + " to "
+ method.modifiers);
}
if (!method.typeParams.equals(prevMethod.typeParams)) {
diffs.add("Type parameters for method " + scopePrefix
+ methodKey + " changed from " + prevMethod.typeParams
+ " to " + method.typeParams);
}
if (!method.throwDecls.equals(prevMethod.throwDecls)) {
diffs.add("Throw decl for method " + scopePrefix + methodKey
+ " changed from " + prevMethod.throwDecls + " to "
+ " to " + method.throwDecls);
}
}
}
return diffs;
}
public String toString() {
return String.format("%s[types: %s, variables: %s, methods: %s]",
getClass().getSimpleName(),
types.values(),
variables.values(),
methods.values());
}
}