/* * Copyright (c) 2014, 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 java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; 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 static com.sun.tools.sjavac.Util.*; import javax.lang.model.element.Modifier; import com.sun.tools.javac.util.Assert; 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 types = new HashMap<>(); public final Map variables = new HashMap<>(); public final Map methods = new HashMap<>(); // 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) { 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 asListOfStrings() { List 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 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 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 = Arrays.asList(Modifier.values()).stream().map(m -> m.name().toLowerCase(Locale.US)).collect(Collectors.joining("|", "(", ")")); private final static Pattern MOD_PATTERN = Pattern.compile("(" + MODIFIERS + " )*"); private final static Pattern METHOD_PATTERN = Pattern.compile("(?.+?) (?\\S+)\\((?.*)\\)( throws (?.*))?"); private final static Pattern VAR_PATTERN = Pattern.compile("VAR (?("+MODIFIERS+" )*)(?.+?) (?\\S+)( = (?.*))?"); private final static Pattern TYPE_PATTERN = Pattern.compile("TYPE (?("+MODIFIERS+" )*)(?\\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 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 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 params = splitOnTopLevelCommas(mm.group("params")); String th = Optional.ofNullable(mm.group("throws")).orElse(""); List throwz = splitOnTopLevelCommas(th); PubMethod m = new PubMethod(modifiers, typeParams, TypeDesc.decodeString(mm.group("ret")), mm.group("name"), parseTypeDescs(params), parseTypeDescs(throwz)); methods.put(m.asSignatureString(), m); return; } Matcher vm = VAR_PATTERN.matcher(l); if (vm.matches()) { PubVar v = new PubVar(parseModifiers(vm.group("modifiers")), TypeDesc.decodeString(vm.group("type")), vm.group("id"), vm.group("val")); variables.put(v.identifier, v); return; } Matcher tm = TYPE_PATTERN.matcher(l); if (tm.matches()) { lastInsertedType = new PubType(parseModifiers(tm.group("modifiers")), tm.group("fullyQualified"), new PubApi()); types.put(lastInsertedType.fqName, lastInsertedType); return; } throw new AssertionError("No matching line pattern."); } catch (Throwable e) { throw new AssertionError("Could not parse API line: " + l, e); } } private static List parseTypeDescs(List strs) { return strs.stream() .map(TypeDesc::decodeString) .collect(Collectors.toList()); } private static List parseTypeParams(List 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 bounds = parseTypeDescs(splitOnTopLevelChars(rest, '&')); return new PubApiTypeParam(identifier, bounds); } public Set parseModifiers(String modifiers) { if (modifiers == null) return Collections.emptySet(); return Arrays.asList(modifiers.split(" ")) .stream() .map(PubApi::trimToUpper) .filter(s -> !s.isEmpty()) .map(Modifier::valueOf) .collect(Collectors.toSet()); } public static String trimToUpper(String s) { return s.trim().toUpperCase(Locale.US); } // 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 splitOnTopLevelCommas(String s) { return splitOnTopLevelChars(s, ','); } public static List splitOnTopLevelChars(String s, char split) { if (s.isEmpty()) return Collections.emptyList(); List 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 String diff(PubApi prevApi) { List diffs = new ArrayList<>(); for (String addedTypeKey : subtract(types.keySet(), prevApi.types.keySet())) diffs.add("Type " + addedTypeKey + " was added."); for (String removedTypeKey : subtract(prevApi.types.keySet(), types.keySet())) diffs.add("Type " + removedTypeKey + " was removed."); for (String addedVarKey : subtract(variables.keySet(), prevApi.variables.keySet())) diffs.add("Variable " + addedVarKey + " was added."); for (String removedVarKey : subtract(prevApi.variables.keySet(), variables.keySet())) diffs.add("Variable " + removedVarKey + " was removed."); for (String addedMethodKey : subtract(methods.keySet(), prevApi.methods.keySet())) diffs.add("Method " + addedMethodKey + " was added."); for (String removedMethodKey : subtract(prevApi.methods.keySet(), methods.keySet())) diffs.add("Method " + removedMethodKey + " was removed."); if (diffs.isEmpty()) return null; String result = diffs.get(0); if (diffs.size() > 1) result += " (and " + (diffs.size() - 1) + " other differences)"; return result; } }