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