1 /*
2  * Copyright (c) 2014, 2019, 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     public final Map<String, PubVar> recordComponents = new HashMap<>();
63 
PubApi()64     public PubApi() {
65     }
66 
PubApi(Collection<PubType> types, Collection<PubVar> variables, Collection<PubMethod> methods)67     public PubApi(Collection<PubType> types,
68                   Collection<PubVar> variables,
69                   Collection<PubMethod> methods) {
70         types.forEach(this::addPubType);
71         variables.forEach(this::addPubVar);
72         methods.forEach(this::addPubMethod);
73     }
74 
75     // Currently this is implemented as equality. This is far from optimal. It
76     // should preferably make sure that all previous methods are still available
77     // and no abstract methods are added. It should also be aware of inheritance
78     // of course.
isBackwardCompatibleWith(PubApi older)79     public boolean isBackwardCompatibleWith(PubApi older) {
80         return equals(older);
81     }
82 
typeLine(PubType type)83     private static String typeLine(PubType type) {
84         if (type.fqName.isEmpty())
85             throw new RuntimeException("empty class name " + type);
86         return String.format("TYPE %s%s", asString(type.modifiers), type.fqName);
87     }
88 
varLine(PubVar var)89     private static String varLine(PubVar var) {
90         return String.format("VAR %s%s %s%s",
91                              asString(var.modifiers),
92                              TypeDesc.encodeAsString(var.type),
93                              var.identifier,
94                              var.getConstValue().map(v -> " = " + v).orElse(""));
95     }
96 
methodLine(PubMethod method)97     private static String methodLine(PubMethod method) {
98         return String.format("METHOD %s%s%s %s(%s)%s",
99                              asString(method.modifiers),
100                              method.typeParams.isEmpty() ? "" : ("<" + method.typeParams.stream().map(PubApiTypeParam::asString).collect(Collectors.joining(",")) + "> "),
101                              TypeDesc.encodeAsString(method.returnType),
102                              method.identifier,
103                              commaSeparated(method.paramTypes),
104                              method.throwDecls.isEmpty()
105                                  ? ""
106                                  : " throws " + commaSeparated(method.throwDecls));
107     }
108 
asListOfStrings()109     public List<String> asListOfStrings() {
110         List<String> lines = new ArrayList<>();
111 
112         // Types
113         types.values()
114              .stream()
115              .sorted(Comparator.comparing(PubApi::typeLine))
116              .forEach(type -> {
117                  lines.add(typeLine(type));
118                  for (String subline : type.pubApi.asListOfStrings())
119                      lines.add("  " + subline);
120              });
121 
122         // Variables
123         variables.values()
124                  .stream()
125                  .map(PubApi::varLine)
126                  .sorted()
127                  .forEach(lines::add);
128 
129         // Methods
130         methods.values()
131                .stream()
132                .map(PubApi::methodLine)
133                .sorted()
134                .forEach(lines::add);
135 
136         return lines;
137     }
138 
139     @Override
equals(Object obj)140     public boolean equals(Object obj) {
141         if (getClass() != obj.getClass())
142             return false;
143         PubApi other = (PubApi) obj;
144         return types.equals(other.types)
145             && variables.equals(other.variables)
146             && methods.equals(other.methods);
147     }
148 
149     @Override
hashCode()150     public int hashCode() {
151         return types.keySet().hashCode()
152              ^ variables.keySet().hashCode()
153              ^ methods.keySet().hashCode();
154     }
155 
commaSeparated(List<TypeDesc> typeDescs)156     private static String commaSeparated(List<TypeDesc> typeDescs) {
157         return typeDescs.stream()
158                         .map(TypeDesc::encodeAsString)
159                         .collect(Collectors.joining(","));
160     }
161 
162     // Create space separated list of modifiers (with a trailing space)
asString(Set<Modifier> modifiers)163     private static String asString(Set<Modifier> modifiers) {
164         return modifiers.stream()
165                         .map(mod -> mod + " ")
166                         .sorted()
167                         .collect(Collectors.joining());
168     }
169 
170     // Used to combine class PubApis to package level PubApis
mergeTypes(PubApi api1, PubApi api2)171     public static PubApi mergeTypes(PubApi api1, PubApi api2) {
172         Assert.check(api1.methods.isEmpty(), "Can only merge types.");
173         Assert.check(api2.methods.isEmpty(), "Can only merge types.");
174         Assert.check(api1.variables.isEmpty(), "Can only merge types.");
175         Assert.check(api2.variables.isEmpty(), "Can only merge types.");
176         PubApi merged = new PubApi();
177         merged.types.putAll(api1.types);
178         merged.types.putAll(api2.types);
179         return merged;
180     }
181 
182 
183     // Used for line-by-line parsing
184     private PubType lastInsertedType = null;
185 
186     private final static String MODIFIERS = Stream.of(Modifier.values())
187                                                   .map(Modifier::name)
188                                                   .map(StringUtils::toLowerCase)
189                                                   .collect(Collectors.joining("|", "(", ")"));
190 
191     private final static Pattern MOD_PATTERN = Pattern.compile("(" + MODIFIERS + " )*");
192     private final static Pattern METHOD_PATTERN = Pattern.compile("(?<ret>.+?) (?<name>\\S+)\\((?<params>.*)\\)( throws (?<throws>.*))?");
193     private final static Pattern VAR_PATTERN = Pattern.compile("VAR (?<modifiers>("+MODIFIERS+" )*)(?<type>.+?) (?<id>\\S+)( = (?<val>.*))?");
194     private final static Pattern TYPE_PATTERN = Pattern.compile("TYPE (?<modifiers>("+MODIFIERS+" )*)(?<fullyQualified>\\S+)");
195 
appendItem(String l)196     public void appendItem(String l) {
197         try {
198             if (l.startsWith("  ")) {
199                 lastInsertedType.pubApi.appendItem(l.substring(2));
200                 return;
201             }
202 
203             if (l.startsWith("METHOD")) {
204                 l = l.substring("METHOD ".length());
205                 Set<Modifier> modifiers = new HashSet<>();
206                 Matcher modMatcher = MOD_PATTERN.matcher(l);
207                 if (modMatcher.find()) {
208                     String modifiersStr = modMatcher.group();
209                     modifiers.addAll(parseModifiers(modifiersStr));
210                     l = l.substring(modifiersStr.length());
211                 }
212                 List<PubApiTypeParam> typeParams = new ArrayList<>();
213                 if (l.startsWith("<")) {
214                     int closingPos = findClosingTag(l, 0);
215                     String str = l.substring(1, closingPos);
216                     l = l.substring(closingPos+1);
217                     typeParams.addAll(parseTypeParams(splitOnTopLevelCommas(str)));
218                 }
219                 Matcher mm = METHOD_PATTERN.matcher(l);
220                 if (!mm.matches())
221                     throw new AssertionError("Could not parse return type, identifier, parameter types or throws declaration of method: " + l);
222 
223                 List<String> params = splitOnTopLevelCommas(mm.group("params"));
224                 String th = Optional.ofNullable(mm.group("throws")).orElse("");
225                 List<String> throwz = splitOnTopLevelCommas(th);
226                 PubMethod m = new PubMethod(modifiers,
227                                             typeParams,
228                                             TypeDesc.decodeString(mm.group("ret")),
229                                             mm.group("name"),
230                                             parseTypeDescs(params),
231                                             parseTypeDescs(throwz));
232                 addPubMethod(m);
233                 return;
234             }
235 
236             Matcher vm = VAR_PATTERN.matcher(l);
237             if (vm.matches()) {
238                 addPubVar(new PubVar(parseModifiers(vm.group("modifiers")),
239                                      TypeDesc.decodeString(vm.group("type")),
240                                      vm.group("id"),
241                                      vm.group("val")));
242                 return;
243             }
244 
245             Matcher tm = TYPE_PATTERN.matcher(l);
246             if (tm.matches()) {
247                 addPubType(new PubType(parseModifiers(tm.group("modifiers")),
248                                        tm.group("fullyQualified"),
249                                        new PubApi()));
250                 return;
251             }
252 
253             throw new AssertionError("No matching line pattern.");
254         } catch (Throwable e) {
255             throw new AssertionError("Could not parse API line: " + l, e);
256         }
257     }
258 
addPubType(PubType t)259     public void addPubType(PubType t) {
260         types.put(t.fqName, t);
261         lastInsertedType = t;
262     }
263 
addPubVar(PubVar v)264     public void addPubVar(PubVar v) {
265         variables.put(v.identifier, v);
266     }
267 
addPubMethod(PubMethod m)268     public void addPubMethod(PubMethod m) {
269         methods.put(m.asSignatureString(), m);
270     }
271 
parseTypeDescs(List<String> strs)272     private static List<TypeDesc> parseTypeDescs(List<String> strs) {
273         return strs.stream()
274                    .map(TypeDesc::decodeString)
275                    .collect(Collectors.toList());
276     }
277 
parseTypeParams(List<String> strs)278     private static List<PubApiTypeParam> parseTypeParams(List<String> strs) {
279         return strs.stream().map(PubApi::parseTypeParam).collect(Collectors.toList());
280     }
281 
282     // Parse a type parameter string. Example input:
283     //     identifier
284     //     identifier extends Type (& Type)*
parseTypeParam(String typeParamString)285     private static PubApiTypeParam parseTypeParam(String typeParamString) {
286         int extPos = typeParamString.indexOf(" extends ");
287         if (extPos == -1)
288             return new PubApiTypeParam(typeParamString, Collections.emptyList());
289         String identifier = typeParamString.substring(0, extPos);
290         String rest = typeParamString.substring(extPos + " extends ".length());
291         List<TypeDesc> bounds = parseTypeDescs(splitOnTopLevelChars(rest, '&'));
292         return new PubApiTypeParam(identifier, bounds);
293     }
294 
parseModifiers(String modifiers)295     public Set<Modifier> parseModifiers(String modifiers) {
296         if (modifiers == null)
297             return Collections.emptySet();
298         return Stream.of(modifiers.split(" "))
299                      .map(String::trim)
300                      .map(StringUtils::toUpperCase)
301                      .filter(s -> !s.isEmpty())
302                      .map(Modifier::valueOf)
303                      .collect(Collectors.toSet());
304     }
305 
306     // Find closing tag of the opening tag at the given 'pos'.
findClosingTag(String l, int pos)307     private static int findClosingTag(String l, int pos) {
308         while (true) {
309             pos = pos + 1;
310             if (l.charAt(pos) == '>')
311                 return pos;
312             if (l.charAt(pos) == '<')
313                 pos = findClosingTag(l, pos);
314         }
315     }
316 
splitOnTopLevelCommas(String s)317     public List<String> splitOnTopLevelCommas(String s) {
318         return splitOnTopLevelChars(s, ',');
319     }
320 
splitOnTopLevelChars(String s, char split)321     public static List<String> splitOnTopLevelChars(String s, char split) {
322         if (s.isEmpty())
323             return Collections.emptyList();
324         List<String> result = new ArrayList<>();
325         StringBuilder buf = new StringBuilder();
326         int depth = 0;
327         for (char c : s.toCharArray()) {
328             if (c == split && depth == 0) {
329                 result.add(buf.toString().trim());
330                 buf = new StringBuilder();
331             } else {
332                 if (c == '<') depth++;
333                 if (c == '>') depth--;
334                 buf.append(c);
335             }
336         }
337         result.add(buf.toString().trim());
338         return result;
339     }
340 
isEmpty()341     public boolean isEmpty() {
342         return types.isEmpty() && variables.isEmpty() && methods.isEmpty();
343     }
344 
345     // Used for descriptive debug messages when figuring out what triggers
346     // recompilation.
diff(PubApi prevApi)347     public List<String> diff(PubApi prevApi) {
348         return diff("", prevApi);
349     }
diff(String scopePrefix, PubApi prevApi)350     private List<String> diff(String scopePrefix, PubApi prevApi) {
351 
352         List<String> diffs = new ArrayList<>();
353 
354         for (String typeKey : union(types.keySet(), prevApi.types.keySet())) {
355             PubType type = types.get(typeKey);
356             PubType prevType = prevApi.types.get(typeKey);
357             if (prevType == null) {
358                 diffs.add("Type " + scopePrefix + typeKey + " was added");
359             } else if (type == null) {
360                 diffs.add("Type " + scopePrefix + typeKey + " was removed");
361             } else {
362                 // Check modifiers
363                 if (!type.modifiers.equals(prevType.modifiers)) {
364                     diffs.add("Modifiers for type " + scopePrefix + typeKey
365                             + " changed from " + prevType.modifiers + " to "
366                             + type.modifiers);
367                 }
368 
369                 // Recursively check types pub API
370                 diffs.addAll(type.pubApi.diff(prevType.pubApi));
371             }
372         }
373 
374         for (String varKey : union(variables.keySet(), prevApi.variables.keySet())) {
375             PubVar var = variables.get(varKey);
376             PubVar prevVar = prevApi.variables.get(varKey);
377             if (prevVar == null) {
378                 diffs.add("Variable " + scopePrefix + varKey + " was added");
379             } else if (var == null) {
380                 diffs.add("Variable " + scopePrefix + varKey + " was removed");
381             } else {
382                 if (!var.modifiers.equals(prevVar.modifiers)) {
383                     diffs.add("Modifiers for var " + scopePrefix + varKey
384                             + " changed from " + prevVar.modifiers + " to "
385                             + var.modifiers);
386                 }
387                 if (!var.type.equals(prevVar.type)) {
388                     diffs.add("Type of " + scopePrefix + varKey
389                             + " changed from " + prevVar.type + " to "
390                             + var.type);
391                 }
392                 if (!var.getConstValue().equals(prevVar.getConstValue())) {
393                     diffs.add("Const value of " + scopePrefix + varKey
394                             + " changed from " + prevVar.getConstValue().orElse("<none>")
395                             + " to " + var.getConstValue().orElse("<none>"));
396                 }
397             }
398         }
399 
400         for (String methodKey : union(methods.keySet(), prevApi.methods.keySet())) {
401             PubMethod method = methods.get(methodKey);
402             PubMethod prevMethod = prevApi.methods.get(methodKey);
403             if (prevMethod == null) {
404                 diffs.add("Method " + scopePrefix + methodKey + " was added");
405             } else if (method == null) {
406                 diffs.add("Method " + scopePrefix + methodKey + " was removed");
407             } else {
408                 if (!method.modifiers.equals(prevMethod.modifiers)) {
409                     diffs.add("Modifiers for method " + scopePrefix + methodKey
410                             + " changed from " + prevMethod.modifiers + " to "
411                             + method.modifiers);
412                 }
413                 if (!method.typeParams.equals(prevMethod.typeParams)) {
414                     diffs.add("Type parameters for method " + scopePrefix
415                             + methodKey + " changed from " + prevMethod.typeParams
416                             + " to " + method.typeParams);
417                 }
418                 if (!method.throwDecls.equals(prevMethod.throwDecls)) {
419                     diffs.add("Throw decl for method " + scopePrefix + methodKey
420                             + " changed from " + prevMethod.throwDecls + " to "
421                             + " to " + method.throwDecls);
422                 }
423             }
424         }
425 
426         return diffs;
427     }
428 
toString()429     public String toString() {
430         return String.format("%s[types: %s, variables: %s, methods: %s]",
431                              getClass().getSimpleName(),
432                              types.values(),
433                              variables.values(),
434                              methods.values());
435     }
436 }
437