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