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
26package com.sun.tools.sjavac.pubapi;
27
28
29import static com.sun.tools.sjavac.Util.union;
30
31import java.io.Serializable;
32import java.util.ArrayList;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.Comparator;
36import java.util.HashMap;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Map;
40import java.util.Optional;
41import java.util.Set;
42import java.util.regex.Matcher;
43import java.util.regex.Pattern;
44import java.util.stream.Collectors;
45import java.util.stream.Stream;
46
47import javax.lang.model.element.Modifier;
48
49import com.sun.tools.javac.util.Assert;
50import com.sun.tools.javac.util.StringUtils;
51
52public 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
63    public PubApi() {
64    }
65
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.
78    public boolean isBackwardCompatibleWith(PubApi older) {
79        return equals(older);
80    }
81
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
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
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
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
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
149    public int hashCode() {
150        return types.keySet().hashCode()
151             ^ variables.keySet().hashCode()
152             ^ methods.keySet().hashCode();
153    }
154
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)
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
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
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
258    public void addPubType(PubType t) {
259        types.put(t.fqName, t);
260        lastInsertedType = t;
261    }
262
263    public void addPubVar(PubVar v) {
264        variables.put(v.identifier, v);
265    }
266
267    public void addPubMethod(PubMethod m) {
268        methods.put(m.asSignatureString(), m);
269    }
270
271    private static List<TypeDesc> parseTypeDescs(List<String> strs) {
272        return strs.stream()
273                   .map(TypeDesc::decodeString)
274                   .collect(Collectors.toList());
275    }
276
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)*
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
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'.
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
316    public List<String> splitOnTopLevelCommas(String s) {
317        return splitOnTopLevelChars(s, ',');
318    }
319
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
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.
346    public List<String> diff(PubApi prevApi) {
347        return diff("", prevApi);
348    }
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
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