| /* |
| * 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()); |
| } |
| } |