1 /*
2  * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.sun.tools.sjavac.pubapi;
27 
28 
29 import static com.sun.tools.sjavac.Util.union;
30 
31 import java.io.Serializable;
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Optional;
41 import java.util.Set;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46 
47 import javax.lang.model.element.Modifier;
48 
49 import com.sun.tools.javac.util.Assert;
50 import com.sun.tools.javac.util.StringUtils;
51 
52 public class PubApi implements Serializable {
53 
54     private static final long serialVersionUID = 5926627347801986850L;
55 
56     // Used to have Set here. Problem is that the objects are mutated during
57     // javac_state loading, causing them to change hash codes. We could probably
58     // change back to Set once javac_state loading is cleaned up.
59     public final Map<String, PubType> types = new HashMap<>();
60     public final Map<String, PubVar> variables = new HashMap<>();
61     public final Map<String, PubMethod> methods = new HashMap<>();
62 
PubApi()63     public PubApi() {
64     }
65 
PubApi(Collection<PubType> types, Collection<PubVar> variables, Collection<PubMethod> methods)66     public PubApi(Collection<PubType> types,
67                   Collection<PubVar> variables,
68                   Collection<PubMethod> methods) {
69         types.forEach(this::addPubType);
70         variables.forEach(this::addPubVar);
71         methods.forEach(this::addPubMethod);
72     }
73 
74     // Currently this is implemented as equality. This is far from optimal. It
75     // should preferably make sure that all previous methods are still available
76     // and no abstract methods are added. It should also be aware of inheritance
77     // of course.
isBackwardCompatibleWith(PubApi older)78     public boolean isBackwardCompatibleWith(PubApi older) {
79         return equals(older);
80     }
81 
typeLine(PubType type)82     private static String typeLine(PubType type) {
83         if (type.fqName.isEmpty())
84             throw new RuntimeException("empty class name " + type);
85         return String.format("TYPE %s%s", asString(type.modifiers), type.fqName);
86     }
87 
varLine(PubVar var)88     private static String varLine(PubVar var) {
89         return String.format("VAR %s%s %s%s",
90                              asString(var.modifiers),
91                              TypeDesc.encodeAsString(var.type),
92                              var.identifier,
93                              var.getConstValue().map(v -> " = " + v).orElse(""));
94     }
95 
methodLine(PubMethod method)96     private static String methodLine(PubMethod method) {
97         return String.format("METHOD %s%s%s %s(%s)%s",
98                              asString(method.modifiers),
99                              method.typeParams.isEmpty() ? "" : ("<" + method.typeParams.stream().map(PubApiTypeParam::asString).collect(Collectors.joining(",")) + "> "),
100                              TypeDesc.encodeAsString(method.returnType),
101                              method.identifier,
102                              commaSeparated(method.paramTypes),
103                              method.throwDecls.isEmpty()
104                                  ? ""
105                                  : " throws " + commaSeparated(method.throwDecls));
106     }
107 
asListOfStrings()108     public List<String> asListOfStrings() {
109         List<String> lines = new ArrayList<>();
110 
111         // Types
112         types.values()
113              .stream()
114              .sorted(Comparator.comparing(PubApi::typeLine))
115              .forEach(type -> {
116                  lines.add(typeLine(type));
117                  for (String subline : type.pubApi.asListOfStrings())
118                      lines.add("  " + subline);
119              });
120 
121         // Variables
122         variables.values()
123                  .stream()
124                  .map(PubApi::varLine)
125                  .sorted()
126                  .forEach(lines::add);
127 
128         // Methods
129         methods.values()
130                .stream()
131                .map(PubApi::methodLine)
132                .sorted()
133                .forEach(lines::add);
134 
135         return lines;
136     }
137 
138     @Override
equals(Object obj)139     public boolean equals(Object obj) {
140         if (getClass() != obj.getClass())
141             return false;
142         PubApi other = (PubApi) obj;
143         return types.equals(other.types)
144             && variables.equals(other.variables)
145             && methods.equals(other.methods);
146     }
147 
148     @Override
hashCode()149     public int hashCode() {
150         return types.keySet().hashCode()
151              ^ variables.keySet().hashCode()
152              ^ methods.keySet().hashCode();
153     }
154 
commaSeparated(List<TypeDesc> typeDescs)155     private static String commaSeparated(List<TypeDesc> typeDescs) {
156         return typeDescs.stream()
157                         .map(TypeDesc::encodeAsString)
158                         .collect(Collectors.joining(","));
159     }
160 
161     // Create space separated list of modifiers (with a trailing space)
asString(Set<Modifier> modifiers)162     private static String asString(Set<Modifier> modifiers) {
163         return modifiers.stream()
164                         .map(mod -> mod + " ")
165                         .sorted()
166                         .collect(Collectors.joining());
167     }
168 
169     // Used to combine class PubApis to package level PubApis
mergeTypes(PubApi api1, PubApi api2)170     public static PubApi mergeTypes(PubApi api1, PubApi api2) {
171         Assert.check(api1.methods.isEmpty(), "Can only merge types.");
172         Assert.check(api2.methods.isEmpty(), "Can only merge types.");
173         Assert.check(api1.variables.isEmpty(), "Can only merge types.");
174         Assert.check(api2.variables.isEmpty(), "Can only merge types.");
175         PubApi merged = new PubApi();
176         merged.types.putAll(api1.types);
177         merged.types.putAll(api2.types);
178         return merged;
179     }
180 
181 
182     // Used for line-by-line parsing
183     private PubType lastInsertedType = null;
184 
185     private final static String MODIFIERS = Stream.of(Modifier.values())
186                                                   .map(Modifier::name)
187                                                   .map(StringUtils::toLowerCase)
188                                                   .collect(Collectors.joining("|", "(", ")"));
189 
190     private final static Pattern MOD_PATTERN = Pattern.compile("(" + MODIFIERS + " )*");
191     private final static Pattern METHOD_PATTERN = Pattern.compile("(?<ret>.+?) (?<name>\\S+)\\((?<params>.*)\\)( throws (?<throws>.*))?");
192     private final static Pattern VAR_PATTERN = Pattern.compile("VAR (?<modifiers>("+MODIFIERS+" )*)(?<type>.+?) (?<id>\\S+)( = (?<val>.*))?");
193     private final static Pattern TYPE_PATTERN = Pattern.compile("TYPE (?<modifiers>("+MODIFIERS+" )*)(?<fullyQualified>\\S+)");
194 
appendItem(String l)195     public void appendItem(String l) {
196         try {
197             if (l.startsWith("  ")) {
198                 lastInsertedType.pubApi.appendItem(l.substring(2));
199                 return;
200             }
201 
202             if (l.startsWith("METHOD")) {
203                 l = l.substring("METHOD ".length());
204                 Set<Modifier> modifiers = new HashSet<>();
205                 Matcher modMatcher = MOD_PATTERN.matcher(l);
206                 if (modMatcher.find()) {
207                     String modifiersStr = modMatcher.group();
208                     modifiers.addAll(parseModifiers(modifiersStr));
209                     l = l.substring(modifiersStr.length());
210                 }
211                 List<PubApiTypeParam> typeParams = new ArrayList<>();
212                 if (l.startsWith("<")) {
213                     int closingPos = findClosingTag(l, 0);
214                     String str = l.substring(1, closingPos);
215                     l = l.substring(closingPos+1);
216                     typeParams.addAll(parseTypeParams(splitOnTopLevelCommas(str)));
217                 }
218                 Matcher mm = METHOD_PATTERN.matcher(l);
219                 if (!mm.matches())
220                     throw new AssertionError("Could not parse return type, identifier, parameter types or throws declaration of method: " + l);
221 
222                 List<String> params = splitOnTopLevelCommas(mm.group("params"));
223                 String th = Optional.ofNullable(mm.group("throws")).orElse("");
224                 List<String> throwz = splitOnTopLevelCommas(th);
225                 PubMethod m = new PubMethod(modifiers,
226                                             typeParams,
227                                             TypeDesc.decodeString(mm.group("ret")),
228                                             mm.group("name"),
229                                             parseTypeDescs(params),
230                                             parseTypeDescs(throwz));
231                 addPubMethod(m);
232                 return;
233             }
234 
235             Matcher vm = VAR_PATTERN.matcher(l);
236             if (vm.matches()) {
237                 addPubVar(new PubVar(parseModifiers(vm.group("modifiers")),
238                                      TypeDesc.decodeString(vm.group("type")),
239                                      vm.group("id"),
240                                      vm.group("val")));
241                 return;
242             }
243 
244             Matcher tm = TYPE_PATTERN.matcher(l);
245             if (tm.matches()) {
246                 addPubType(new PubType(parseModifiers(tm.group("modifiers")),
247                                        tm.group("fullyQualified"),
248                                        new PubApi()));
249                 return;
250             }
251 
252             throw new AssertionError("No matching line pattern.");
253         } catch (Throwable e) {
254             throw new AssertionError("Could not parse API line: " + l, e);
255         }
256     }
257 
addPubType(PubType t)258     public void addPubType(PubType t) {
259         types.put(t.fqName, t);
260         lastInsertedType = t;
261     }
262 
addPubVar(PubVar v)263     public void addPubVar(PubVar v) {
264         variables.put(v.identifier, v);
265     }
266 
addPubMethod(PubMethod m)267     public void addPubMethod(PubMethod m) {
268         methods.put(m.asSignatureString(), m);
269     }
270 
parseTypeDescs(List<String> strs)271     private static List<TypeDesc> parseTypeDescs(List<String> strs) {
272         return strs.stream()
273                    .map(TypeDesc::decodeString)
274                    .collect(Collectors.toList());
275     }
276 
parseTypeParams(List<String> strs)277     private static List<PubApiTypeParam> parseTypeParams(List<String> strs) {
278         return strs.stream().map(PubApi::parseTypeParam).collect(Collectors.toList());
279     }
280 
281     // Parse a type parameter string. Example input:
282     //     identifier
283     //     identifier extends Type (& Type)*
parseTypeParam(String typeParamString)284     private static PubApiTypeParam parseTypeParam(String typeParamString) {
285         int extPos = typeParamString.indexOf(" extends ");
286         if (extPos == -1)
287             return new PubApiTypeParam(typeParamString, Collections.emptyList());
288         String identifier = typeParamString.substring(0, extPos);
289         String rest = typeParamString.substring(extPos + " extends ".length());
290         List<TypeDesc> bounds = parseTypeDescs(splitOnTopLevelChars(rest, '&'));
291         return new PubApiTypeParam(identifier, bounds);
292     }
293 
parseModifiers(String modifiers)294     public Set<Modifier> parseModifiers(String modifiers) {
295         if (modifiers == null)
296             return Collections.emptySet();
297         return Stream.of(modifiers.split(" "))
298                      .map(String::trim)
299                      .map(StringUtils::toUpperCase)
300                      .filter(s -> !s.isEmpty())
301                      .map(Modifier::valueOf)
302                      .collect(Collectors.toSet());
303     }
304 
305     // Find closing tag of the opening tag at the given 'pos'.
findClosingTag(String l, int pos)306     private static int findClosingTag(String l, int pos) {
307         while (true) {
308             pos = pos + 1;
309             if (l.charAt(pos) == '>')
310                 return pos;
311             if (l.charAt(pos) == '<')
312                 pos = findClosingTag(l, pos);
313         }
314     }
315 
splitOnTopLevelCommas(String s)316     public List<String> splitOnTopLevelCommas(String s) {
317         return splitOnTopLevelChars(s, ',');
318     }
319 
splitOnTopLevelChars(String s, char split)320     public static List<String> splitOnTopLevelChars(String s, char split) {
321         if (s.isEmpty())
322             return Collections.emptyList();
323         List<String> result = new ArrayList<>();
324         StringBuilder buf = new StringBuilder();
325         int depth = 0;
326         for (char c : s.toCharArray()) {
327             if (c == split && depth == 0) {
328                 result.add(buf.toString().trim());
329                 buf = new StringBuilder();
330             } else {
331                 if (c == '<') depth++;
332                 if (c == '>') depth--;
333                 buf.append(c);
334             }
335         }
336         result.add(buf.toString().trim());
337         return result;
338     }
339 
isEmpty()340     public boolean isEmpty() {
341         return types.isEmpty() && variables.isEmpty() && methods.isEmpty();
342     }
343 
344     // Used for descriptive debug messages when figuring out what triggers
345     // recompilation.
diff(PubApi prevApi)346     public List<String> diff(PubApi prevApi) {
347         return diff("", prevApi);
348     }
diff(String scopePrefix, PubApi prevApi)349     private List<String> diff(String scopePrefix, PubApi prevApi) {
350 
351         List<String> diffs = new ArrayList<>();
352 
353         for (String typeKey : union(types.keySet(), prevApi.types.keySet())) {
354             PubType type = types.get(typeKey);
355             PubType prevType = prevApi.types.get(typeKey);
356             if (prevType == null) {
357                 diffs.add("Type " + scopePrefix + typeKey + " was added");
358             } else if (type == null) {
359                 diffs.add("Type " + scopePrefix + typeKey + " was removed");
360             } else {
361                 // Check modifiers
362                 if (!type.modifiers.equals(prevType.modifiers)) {
363                     diffs.add("Modifiers for type " + scopePrefix + typeKey
364                             + " changed from " + prevType.modifiers + " to "
365                             + type.modifiers);
366                 }
367 
368                 // Recursively check types pub API
369                 diffs.addAll(type.pubApi.diff(prevType.pubApi));
370             }
371         }
372 
373         for (String varKey : union(variables.keySet(), prevApi.variables.keySet())) {
374             PubVar var = variables.get(varKey);
375             PubVar prevVar = prevApi.variables.get(varKey);
376             if (prevVar == null) {
377                 diffs.add("Variable " + scopePrefix + varKey + " was added");
378             } else if (var == null) {
379                 diffs.add("Variable " + scopePrefix + varKey + " was removed");
380             } else {
381                 if (!var.modifiers.equals(prevVar.modifiers)) {
382                     diffs.add("Modifiers for var " + scopePrefix + varKey
383                             + " changed from " + prevVar.modifiers + " to "
384                             + var.modifiers);
385                 }
386                 if (!var.type.equals(prevVar.type)) {
387                     diffs.add("Type of " + scopePrefix + varKey
388                             + " changed from " + prevVar.type + " to "
389                             + var.type);
390                 }
391                 if (!var.getConstValue().equals(prevVar.getConstValue())) {
392                     diffs.add("Const value of " + scopePrefix + varKey
393                             + " changed from " + prevVar.getConstValue().orElse("<none>")
394                             + " to " + var.getConstValue().orElse("<none>"));
395                 }
396             }
397         }
398 
399         for (String methodKey : union(methods.keySet(), prevApi.methods.keySet())) {
400             PubMethod method = methods.get(methodKey);
401             PubMethod prevMethod = prevApi.methods.get(methodKey);
402             if (prevMethod == null) {
403                 diffs.add("Method " + scopePrefix + methodKey + " was added");
404             } else if (method == null) {
405                 diffs.add("Method " + scopePrefix + methodKey + " was removed");
406             } else {
407                 if (!method.modifiers.equals(prevMethod.modifiers)) {
408                     diffs.add("Modifiers for method " + scopePrefix + methodKey
409                             + " changed from " + prevMethod.modifiers + " to "
410                             + method.modifiers);
411                 }
412                 if (!method.typeParams.equals(prevMethod.typeParams)) {
413                     diffs.add("Type parameters for method " + scopePrefix
414                             + methodKey + " changed from " + prevMethod.typeParams
415                             + " to " + method.typeParams);
416                 }
417                 if (!method.throwDecls.equals(prevMethod.throwDecls)) {
418                     diffs.add("Throw decl for method " + scopePrefix + methodKey
419                             + " changed from " + prevMethod.throwDecls + " to "
420                             + " to " + method.throwDecls);
421                 }
422             }
423         }
424 
425         return diffs;
426     }
427 
toString()428     public String toString() {
429         return String.format("%s[types: %s, variables: %s, methods: %s]",
430                              getClass().getSimpleName(),
431                              types.values(),
432                              variables.values(),
433                              methods.values());
434     }
435 }
436