content: /*
   * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
   * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   *
   * This code is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License version 2 only, as
   * published by the Free Software Foundation.  Oracle designates this
   * particular file as subject to the "Classpath" exception as provided
   * by Oracle in the LICENSE file that accompanied this code.
   *
   * This code is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
   * version 2 for more details (a copy is included in the LICENSE file that
   * accompanied this code).
   *
   * You should have received a copy of the GNU General Public License version
   * 2 along with this work; if not, write to the Free Software Foundation,
   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
   *
   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
   * or visit www.oracle.com if you need additional information or have any
   * questions.
   */
  
  package openjdk.tools.sjavac.pubapi;
  
  
  import static openjdk.tools.sjavac.Util.union;
  
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  import jdkx.lang.model.element.Modifier;
  
  import openjdk.tools.javac.util.Assert;
  import openjdk.tools.javac.util.StringUtils;
  
  public class PubApi implements Serializable {
  
      private static final long serialVersionUID = 5926627347801986850L;
  
      // Used to have Set here. Problem is that the objects are mutated during
      // javac_state loading, causing them to change hash codes. We could probably
      // change back to Set once javac_state loading is cleaned up.
      public final Map<String, PubType> types = new HashMap<>();
      public final Map<String, PubVar> variables = new HashMap<>();
      public final Map<String, PubMethod> methods = new HashMap<>();
      public final Map<String, PubVar> recordComponents = new HashMap<>();
  
      public PubApi() {
      }
  
      public PubApi(Collection<PubType> types,
                    Collection<PubVar> variables,
                    Collection<PubMethod> methods) {
          types.forEach(this::addPubType);
          variables.forEach(this::addPubVar);
          methods.forEach(this::addPubMethod);
      }
  
      // Currently this is implemented as equality. This is far from optimal. It
      // should preferably make sure that all previous methods are still available
      // and no abstract methods are added. It should also be aware of inheritance
      // of course.
      public boolean isBackwardCompatibleWith(PubApi older) {
          return equals(older);
      }
  
      private static String typeLine(PubType type) {
          if (type.fqName.isEmpty())
              throw new RuntimeException("empty class name " + type);
          return String.format("TYPE %s%s", asString(type.modifiers), type.fqName);
      }
  
      private static String varLine(PubVar var) {
          return String.format("VAR %s%s %s%s",
                               asString(var.modifiers),
                               TypeDesc.encodeAsString(var.type),
                               var.identifier,
                               var.getConstValue().map(v -> " = " + v).orElse(""));
      }
  
      private static String methodLine(PubMethod method) {
          return String.format("METHOD %s%s%s %s(%s)%s",
                               asString(method.modifiers),
                               method.typeParams.isEmpty() ? "" : ("<" + method.typeParams.stream().map(PubApiTypeParam::asString).collect(Collectors.joining(",")) + "> "),
                               TypeDesc.encodeAsString(method.returnType),
                               method.identifier,
                               commaSeparated(method.paramTypes),
                               method.throwDecls.isEmpty()
                                   ? ""
                                   : " throws " + commaSeparated(method.throwDecls));
      }
  
      public List<String> asListOfStrings() {
          List<String> lines = new ArrayList<>();
  
          // Types
          types.values()
               .stream()
               .sorted(Comparator.comparing(PubApi::typeLine))
               .forEach(type -> {
                   lines.add(typeLine(type));
                   for (String subline : type.pubApi.asListOfStrings())
                       lines.add("  " + subline);
               });
  
          // Variables
          variables.values()
                   .stream()
                   .map(PubApi::varLine)
                   .sorted()
                   .forEach(lines::add);
  
          // Methods
          methods.values()
                 .stream()
                 .map(PubApi::methodLine)
                 .sorted()
                 .forEach(lines::add);
  
          return lines;
      }
  
      @Override
      public boolean equals(Object obj) {
          if (getClass() != obj.getClass())
              return false;
          PubApi other = (PubApi) obj;
          return types.equals(other.types)
              && variables.equals(other.variables)
              && methods.equals(other.methods);
      }
  
      @Override
      public int hashCode() {
          return types.keySet().hashCode()
               ^ variables.keySet().hashCode()
               ^ methods.keySet().hashCode();
      }
  
      private static String commaSeparated(List<TypeDesc> typeDescs) {
          return typeDescs.stream()
                          .map(TypeDesc::encodeAsString)
                          .collect(Collectors.joining(","));
      }
  
      // Create space separated list of modifiers (with a trailing space)
      private static String asString(Set<Modifier> modifiers) {
          return modifiers.stream()
                          .map(mod -> mod + " ")
                          .sorted()
                          .collect(Collectors.joining());
      }
  
      // Used to combine class PubApis to package level PubApis
      public static PubApi mergeTypes(PubApi api1, PubApi api2) {
          Assert.check(api1.methods.isEmpty(), "Can only merge types.");
          Assert.check(api2.methods.isEmpty(), "Can only merge types.");
          Assert.check(api1.variables.isEmpty(), "Can only merge types.");
          Assert.check(api2.variables.isEmpty(), "Can only merge types.");
          PubApi merged = new PubApi();
          merged.types.putAll(api1.types);
          merged.types.putAll(api2.types);
          return merged;
      }
  
  
      // Used for line-by-line parsing
      private PubType lastInsertedType = null;
  
      private final static String MODIFIERS = Stream.of(Modifier.values())
                                                    .map(Modifier::name)
                                                    .map(StringUtils::toLowerCase)
                                                    .collect(Collectors.joining("|", "(", ")"));
  
      private final static Pattern MOD_PATTERN = Pattern.compile("(" + MODIFIERS + " )*");
      private final static Pattern METHOD_PATTERN = Pattern.compile("(?<ret>.+?) (?<name>\\S+)\\((?<params>.*)\\)( throws (?<throws>.*))?");
      private final static Pattern VAR_PATTERN = Pattern.compile("VAR (?<modifiers>("+MODIFIERS+" )*)(?<type>.+?) (?<id>\\S+)( = (?<val>.*))?");
      private final static Pattern TYPE_PATTERN = Pattern.compile("TYPE (?<modifiers>("+MODIFIERS+" )*)(?<fullyQualified>\\S+)");
  
      public void appendItem(String l) {
          try {
              if (l.startsWith("  ")) {
                  lastInsertedType.pubApi.appendItem(l.substring(2));
                  return;
              }
  
              if (l.startsWith("METHOD")) {
                  l = l.substring("METHOD ".length());
                  Set<Modifier> modifiers = new HashSet<>();
                  Matcher modMatcher = MOD_PATTERN.matcher(l);
                  if (modMatcher.find()) {
                      String modifiersStr = modMatcher.group();
                      modifiers.addAll(parseModifiers(modifiersStr));
                      l = l.substring(modifiersStr.length());
                  }
                  List<PubApiTypeParam> typeParams = new ArrayList<>();
                  if (l.startsWith("<")) {
                      int closingPos = findClosingTag(l, 0);
                      String str = l.substring(1, closingPos);
                      l = l.substring(closingPos+1);
                      typeParams.addAll(parseTypeParams(splitOnTopLevelCommas(str)));
                  }
                  Matcher mm = METHOD_PATTERN.matcher(l);
                  if (!mm.matches())
                      throw new AssertionError("Could not parse return type, identifier, parameter types or throws declaration of method: " + l);
  
                  List<String> params = splitOnTopLevelCommas(mm.group("params"));
                  String th = Optional.ofNullable(mm.group("throws")).orElse("");
                  List<String> throwz = splitOnTopLevelCommas(th);
                  PubMethod m = new PubMethod(modifiers,
                                              typeParams,
                                              TypeDesc.decodeString(mm.group("ret")),
                                              mm.group("name"),
                                              parseTypeDescs(params),
                                              parseTypeDescs(throwz));
                  addPubMethod(m);
                  return;
              }
  
              Matcher vm = VAR_PATTERN.matcher(l);
              if (vm.matches()) {
                  addPubVar(new PubVar(parseModifiers(vm.group("modifiers")),
                                       TypeDesc.decodeString(vm.group("type")),
                                       vm.group("id"),
                                       vm.group("val")));
                  return;
              }
  
              Matcher tm = TYPE_PATTERN.matcher(l);
              if (tm.matches()) {
                  addPubType(new PubType(parseModifiers(tm.group("modifiers")),
                                         tm.group("fullyQualified"),
                                         new PubApi()));
                  return;
              }
  
              throw new AssertionError("No matching line pattern.");
          } catch (Throwable e) {
              throw new AssertionError("Could not parse API line: " + l, e);
          }
      }
  
      public void addPubType(PubType t) {
          types.put(t.fqName, t);
          lastInsertedType = t;
      }
  
      public void addPubVar(PubVar v) {
          variables.put(v.identifier, v);
      }
  
      public void addPubMethod(PubMethod m) {
          methods.put(m.asSignatureString(), m);
      }
  
      private static List<TypeDesc> parseTypeDescs(List<String> strs) {
          return strs.stream()
                     .map(TypeDesc::decodeString)
                     .collect(Collectors.toList());
      }
  
      private static List<PubApiTypeParam> parseTypeParams(List<String> strs) {
          return strs.stream().map(PubApi::parseTypeParam).collect(Collectors.toList());
      }
  
      // Parse a type parameter string. Example input:
      //     identifier
      //     identifier extends Type (& Type)*
      private static PubApiTypeParam parseTypeParam(String typeParamString) {
          int extPos = typeParamString.indexOf(" extends ");
          if (extPos == -1)
              return new PubApiTypeParam(typeParamString, Collections.emptyList());
          String identifier = typeParamString.substring(0, extPos);
          String rest = typeParamString.substring(extPos + " extends ".length());
          List<TypeDesc> bounds = parseTypeDescs(splitOnTopLevelChars(rest, '&'));
          return new PubApiTypeParam(identifier, bounds);
      }
  
      public Set<Modifier> parseModifiers(String modifiers) {
          if (modifiers == null)
              return Collections.emptySet();
          return Stream.of(modifiers.split(" "))
                       .map(String::trim)
                       .map(StringUtils::toUpperCase)
                       .filter(s -> !s.isEmpty())
                       .map(Modifier::valueOf)
                       .collect(Collectors.toSet());
      }
  
      // Find closing tag of the opening tag at the given 'pos'.
      private static int findClosingTag(String l, int pos) {
          while (true) {
              pos = pos + 1;
              if (l.charAt(pos) == '>')
                  return pos;
              if (l.charAt(pos) == '<')
                  pos = findClosingTag(l, pos);
          }
      }
  
      public List<String> splitOnTopLevelCommas(String s) {
          return splitOnTopLevelChars(s, ',');
      }
  
      public static List<String> splitOnTopLevelChars(String s, char split) {
          if (s.isEmpty())
              return Collections.emptyList();
          List<String> result = new ArrayList<>();
          StringBuilder buf = new StringBuilder();
          int depth = 0;
          for (char c : s.toCharArray()) {
              if (c == split && depth == 0) {
                  result.add(buf.toString().trim());
                  buf = new StringBuilder();
              } else {
                  if (c == '<') depth++;
                  if (c == '>') depth--;
                  buf.append(c);
              }
          }
          result.add(buf.toString().trim());
          return result;
      }
  
      public boolean isEmpty() {
          return types.isEmpty() && variables.isEmpty() && methods.isEmpty();
      }
  
      // Used for descriptive debug messages when figuring out what triggers
      // recompilation.
      public List<String> diff(PubApi prevApi) {
          return diff("", prevApi);
      }
      private List<String> diff(String scopePrefix, PubApi prevApi) {
  
          List<String> diffs = new ArrayList<>();
  
          for (String typeKey : union(types.keySet(), prevApi.types.keySet())) {
              PubType type = types.get(typeKey);
              PubType prevType = prevApi.types.get(typeKey);
              if (prevType == null) {
                  diffs.add("Type " + scopePrefix + typeKey + " was added");
              } else if (type == null) {
                  diffs.add("Type " + scopePrefix + typeKey + " was removed");
              } else {
                  // Check modifiers
                  if (!type.modifiers.equals(prevType.modifiers)) {
                      diffs.add("Modifiers for type " + scopePrefix + typeKey
                              + " changed from " + prevType.modifiers + " to "
                              + type.modifiers);
                  }
  
                  // Recursively check types pub API
                  diffs.addAll(type.pubApi.diff(prevType.pubApi));
              }
          }
  
          for (String varKey : union(variables.keySet(), prevApi.variables.keySet())) {
              PubVar var = variables.get(varKey);
              PubVar prevVar = prevApi.variables.get(varKey);
              if (prevVar == null) {
                  diffs.add("Variable " + scopePrefix + varKey + " was added");
              } else if (var == null) {
                  diffs.add("Variable " + scopePrefix + varKey + " was removed");
              } else {
                  if (!var.modifiers.equals(prevVar.modifiers)) {
                      diffs.add("Modifiers for var " + scopePrefix + varKey
                              + " changed from " + prevVar.modifiers + " to "
                              + var.modifiers);
                  }
                  if (!var.type.equals(prevVar.type)) {
                      diffs.add("Type of " + scopePrefix + varKey
                              + " changed from " + prevVar.type + " to "
                              + var.type);
                  }
                  if (!var.getConstValue().equals(prevVar.getConstValue())) {
                      diffs.add("Const value of " + scopePrefix + varKey
                              + " changed from " + prevVar.getConstValue().orElse("<none>")
                              + " to " + var.getConstValue().orElse("<none>"));
                  }
              }
          }
  
          for (String methodKey : union(methods.keySet(), prevApi.methods.keySet())) {
              PubMethod method = methods.get(methodKey);
              PubMethod prevMethod = prevApi.methods.get(methodKey);
              if (prevMethod == null) {
                  diffs.add("Method " + scopePrefix + methodKey + " was added");
              } else if (method == null) {
                  diffs.add("Method " + scopePrefix + methodKey + " was removed");
              } else {
                  if (!method.modifiers.equals(prevMethod.modifiers)) {
                      diffs.add("Modifiers for method " + scopePrefix + methodKey
                              + " changed from " + prevMethod.modifiers + " to "
                              + method.modifiers);
                  }
                  if (!method.typeParams.equals(prevMethod.typeParams)) {
                      diffs.add("Type parameters for method " + scopePrefix
                              + methodKey + " changed from " + prevMethod.typeParams
                              + " to " + method.typeParams);
                  }
                  if (!method.throwDecls.equals(prevMethod.throwDecls)) {
                      diffs.add("Throw decl for method " + scopePrefix + methodKey
                              + " changed from " + prevMethod.throwDecls + " to "
                              + " to " + method.throwDecls);
                  }
              }
          }
  
          return diffs;
      }
  
      public String toString() {
          return String.format("%s[types: %s, variables: %s, methods: %s]",
                               getClass().getSimpleName(),
                               types.values(),
                               variables.values(),
                               methods.values());
      }
  }
  