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 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 PubApi()63 public PubApi() { 64 } 65 PubApi(Collection<PubType> types, Collection<PubVar> variables, Collection<PubMethod> methods)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. isBackwardCompatibleWith(PubApi older)78 public boolean isBackwardCompatibleWith(PubApi older) { 79 return equals(older); 80 } 81 typeLine(PubType type)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 varLine(PubVar var)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 methodLine(PubMethod method)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 asListOfStrings()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 equals(Object obj)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 hashCode()149 public int hashCode() { 150 return types.keySet().hashCode() 151 ^ variables.keySet().hashCode() 152 ^ methods.keySet().hashCode(); 153 } 154 commaSeparated(List<TypeDesc> typeDescs)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) asString(Set<Modifier> modifiers)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 mergeTypes(PubApi api1, PubApi api2)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 appendItem(String l)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 addPubType(PubType t)258 public void addPubType(PubType t) { 259 types.put(t.fqName, t); 260 lastInsertedType = t; 261 } 262 addPubVar(PubVar v)263 public void addPubVar(PubVar v) { 264 variables.put(v.identifier, v); 265 } 266 addPubMethod(PubMethod m)267 public void addPubMethod(PubMethod m) { 268 methods.put(m.asSignatureString(), m); 269 } 270 parseTypeDescs(List<String> strs)271 private static List<TypeDesc> parseTypeDescs(List<String> strs) { 272 return strs.stream() 273 .map(TypeDesc::decodeString) 274 .collect(Collectors.toList()); 275 } 276 parseTypeParams(List<String> strs)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)* parseTypeParam(String typeParamString)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 parseModifiers(String modifiers)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'. findClosingTag(String l, int 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 splitOnTopLevelCommas(String s)316 public List<String> splitOnTopLevelCommas(String s) { 317 return splitOnTopLevelChars(s, ','); 318 } 319 splitOnTopLevelChars(String s, char split)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 isEmpty()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. diff(PubApi prevApi)346 public List<String> diff(PubApi prevApi) { 347 return diff("", prevApi); 348 } diff(String scopePrefix, PubApi prevApi)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 toString()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