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;
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);
}
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.values()
.stream()
.sorted(Comparator.comparing(PubApi::typeLine))
.forEach(type -> {
lines.add(typeLine(type));
for (String subline : type.pubApi.asListOfStrings())
lines.add(" " + subline);
});
variables.values()
.stream()
.map(PubApi::varLine)
.sorted()
.forEach(lines::add);
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(","));
}
private static String asString(Set<Modifier> modifiers) {
return modifiers.stream()
.map(mod -> mod + " ")
.sorted()
.collect(Collectors.joining());
}
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;
}
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());
}
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());
}
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();
}
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 {
if (!type.modifiers.equals(prevType.modifiers)) {
diffs.add("Modifiers for type " + scopePrefix + typeKey
+ " changed from " + prevType.modifiers + " to "
+ type.modifiers);
}
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());
}
}