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