1 /* 2 * Copyright (C) 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.doclava.apicheck; 18 19 import java.io.FileInputStream; 20 import java.io.FileNotFoundException; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.PrintStream; 24 import java.net.URL; 25 import java.util.ArrayList; 26 import java.util.List; 27 import java.util.HashSet; 28 import java.util.Set; 29 import java.util.function.Predicate; 30 31 import com.google.doclava.Errors; 32 import com.google.doclava.PackageInfo; 33 import com.google.doclava.Errors.ErrorMessage; 34 import com.google.doclava.Stubs; 35 36 public class ApiCheck { 37 // parse out and consume the -whatever command line flags parseFlags(ArrayList<String> allArgs)38 private static ArrayList<String[]> parseFlags(ArrayList<String> allArgs) { 39 ArrayList<String[]> ret = new ArrayList<String[]>(); 40 41 int i; 42 for (i = 0; i < allArgs.size(); i++) { 43 // flags with one value attached 44 String flag = allArgs.get(i); 45 if (flag.equals("-error") || flag.equals("-warning") || flag.equals("-hide") 46 || flag.equals("-ignoreClass") || flag.equals("-ignorePackage")) { 47 String[] arg = new String[2]; 48 arg[0] = flag; 49 arg[1] = allArgs.get(++i); 50 ret.add(arg); 51 } else { 52 // we've consumed all of the -whatever args, so we're done 53 break; 54 } 55 } 56 57 // i now points to the first non-flag arg; strip what came before 58 for (; i > 0; i--) { 59 allArgs.remove(0); 60 } 61 return ret; 62 } 63 main(String[] originalArgs)64 public static void main(String[] originalArgs) { 65 if (originalArgs.length == 3 && "-convert".equals(originalArgs[0])) { 66 System.exit(convertToApi(originalArgs[1], originalArgs[2])); 67 } else if (originalArgs.length == 3 && "-convert2xml".equals(originalArgs[0])) { 68 System.exit(convertToXml(originalArgs[1], originalArgs[2], true)); 69 } else if (originalArgs.length == 3 && "-convert2xmlnostrip".equals(originalArgs[0])) { 70 System.exit(convertToXml(originalArgs[1], originalArgs[2], false)); 71 } else if (originalArgs.length == 4 && "-new_api".equals(originalArgs[0])) { 72 // command syntax: -new_api oldapi.txt newapi.txt diff.xml 73 // TODO: Support reading in other options for new_api, such as ignored classes/packages. 74 System.exit(newApi(originalArgs[1], originalArgs[2], originalArgs[3], true)); 75 } else if (originalArgs.length == 4 && "-new_api_no_strip".equals(originalArgs[0])) { 76 // command syntax: -new_api oldapi.txt newapi.txt diff.xml 77 // TODO: Support reading in other options for new_api, such as ignored classes/packages. 78 System.exit(newApi(originalArgs[1], originalArgs[2], originalArgs[3], false)); 79 } else { 80 ApiCheck acheck = new ApiCheck(); 81 Report report = acheck.checkApi(originalArgs); 82 83 Errors.printErrors(report.errors()); 84 System.exit(report.code); 85 } 86 } 87 88 /** 89 * Compares two api xml files for consistency. 90 */ checkApi(String[] originalArgs)91 public Report checkApi(String[] originalArgs) { 92 // translate to an ArrayList<String> for munging 93 ArrayList<String> args = new ArrayList<String>(originalArgs.length); 94 for (String a : originalArgs) { 95 args.add(a); 96 } 97 98 // Not having having any classes or packages ignored is the common case. 99 // Avoid a hashCode call in a common loop by not passing in a HashSet in this case. 100 Set<String> ignoredPackages = null; 101 Set<String> ignoredClasses = null; 102 103 ArrayList<String[]> flags = ApiCheck.parseFlags(args); 104 for (String[] a : flags) { 105 if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) { 106 try { 107 int level = -1; 108 if (a[0].equals("-error")) { 109 level = Errors.ERROR; 110 } else if (a[0].equals("-warning")) { 111 level = Errors.WARNING; 112 } else if (a[0].equals("-hide")) { 113 level = Errors.HIDDEN; 114 } 115 Errors.setErrorLevel(Integer.parseInt(a[1]), level); 116 } catch (NumberFormatException e) { 117 System.err.println("Bad argument: " + a[0] + " " + a[1]); 118 return new Report(2, Errors.getErrors()); 119 } 120 } else if (a[0].equals("-ignoreClass")) { 121 if (ignoredClasses == null) { 122 ignoredClasses = new HashSet<String>(); 123 } 124 ignoredClasses.add(a[1]); 125 } else if (a[0].equals("-ignorePackage")) { 126 if (ignoredPackages == null) { 127 ignoredPackages = new HashSet<String>(); 128 } 129 ignoredPackages.add(a[1]); 130 } 131 } 132 133 ApiInfo oldApi; 134 ApiInfo newApi; 135 ApiInfo oldRemovedApi = null; 136 ApiInfo newRemovedApi = null; 137 138 // commandline options look like: 139 // [other options] old_api.txt new_api.txt 140 // [other options] old_api.txt new_api.txt old_removed_api.txt new_removed_api.txt 141 try { 142 oldApi = parseApi(args.get(0)); 143 newApi = parseApi(args.get(1)); 144 if (args.size() > 2) { 145 oldRemovedApi = parseApi(args.get(2)); 146 newRemovedApi = parseApi(args.get(3)); 147 } 148 } catch (ApiParseException e) { 149 e.printStackTrace(); 150 System.err.println("Error parsing API"); 151 return new Report(1, Errors.getErrors()); 152 } 153 154 // only run the consistency check if we haven't had XML parse errors 155 if (!Errors.hadError) { 156 oldApi.isConsistent(newApi, null, ignoredPackages, ignoredClasses); 157 } 158 159 if (oldRemovedApi != null && !Errors.hadError) { 160 oldRemovedApi.isConsistent(newRemovedApi, null, ignoredPackages, ignoredClasses); 161 } 162 163 return new Report(Errors.hadError ? 1 : 0, Errors.getErrors()); 164 } 165 parseApi(String filename)166 public static ApiInfo parseApi(String filename) throws ApiParseException { 167 InputStream stream = null; 168 Throwable textParsingError = null; 169 Throwable xmlParsingError = null; 170 // try it as our format 171 try { 172 stream = new FileInputStream(filename); 173 } catch (IOException e) { 174 throw new ApiParseException("Could not open file for parsing: " + filename, e); 175 } 176 try { 177 return ApiFile.parseApi(filename, stream); 178 } catch (ApiParseException exception) { 179 textParsingError = exception; 180 } finally { 181 try { 182 stream.close(); 183 } catch (IOException ignored) {} 184 } 185 // try it as xml 186 try { 187 stream = new FileInputStream(filename); 188 } catch (IOException e) { 189 throw new ApiParseException("Could not open file for parsing: " + filename, e); 190 } 191 try { 192 return XmlApiFile.parseApi(stream); 193 } catch (ApiParseException exception) { 194 xmlParsingError = exception; 195 } finally { 196 try { 197 stream.close(); 198 } catch (IOException ignored) {} 199 } 200 // The file has failed to parse both as XML and as text. Build the string in this order as 201 // the message is easier to read with that error at the end. 202 throw new ApiParseException(filename + 203 " failed to parse as xml: \"" + xmlParsingError.getMessage() + 204 "\" and as text: \"" + textParsingError.getMessage() + "\""); 205 } 206 parseApi(URL url)207 public ApiInfo parseApi(URL url) throws ApiParseException { 208 InputStream stream = null; 209 // try it as our format 210 try { 211 stream = url.openStream(); 212 } catch (IOException e) { 213 throw new ApiParseException("Could not open stream for parsing: " + url, e); 214 } 215 try { 216 return ApiFile.parseApi(url.toString(), stream); 217 } catch (ApiParseException ignored) { 218 } finally { 219 try { 220 stream.close(); 221 } catch (IOException ignored) {} 222 } 223 // try it as xml 224 try { 225 stream = url.openStream(); 226 } catch (IOException e) { 227 throw new ApiParseException("Could not open stream for parsing: " + url, e); 228 } 229 try { 230 return XmlApiFile.parseApi(stream); 231 } finally { 232 try { 233 stream.close(); 234 } catch (IOException ignored) {} 235 } 236 } 237 238 public class Report { 239 private int code; 240 private Set<ErrorMessage> errors; 241 Report(int code, Set<ErrorMessage> errors)242 private Report(int code, Set<ErrorMessage> errors) { 243 this.code = code; 244 this.errors = errors; 245 } 246 code()247 public int code() { 248 return code; 249 } 250 errors()251 public Set<ErrorMessage> errors() { 252 return errors; 253 } 254 } 255 convertToApi(String src, String dst)256 static int convertToApi(String src, String dst) { 257 // This was historically used to convert XML to TXT format, which was a 258 // one-time migration. 259 throw new UnsupportedOperationException(); 260 } 261 convertToXml(String src, String dst, boolean strip)262 static int convertToXml(String src, String dst, boolean strip) { 263 ApiInfo api; 264 try { 265 api = parseApi(src); 266 } catch (ApiParseException e) { 267 e.printStackTrace(); 268 System.err.println("Error parsing API: " + src); 269 return 1; 270 } 271 272 PrintStream apiWriter = null; 273 try { 274 apiWriter = new PrintStream(dst); 275 } catch (FileNotFoundException ex) { 276 System.err.println("can't open file: " + dst); 277 } 278 279 Stubs.writeXml(apiWriter, api.getPackages().values(), strip); 280 281 return 0; 282 } 283 284 /** 285 * Generates a "diff": where new API is trimmed down by removing existing methods found in old API 286 * @param origApiPath path to old API text file 287 * @param newApiPath path to new API text file 288 * @param outputPath output XML path for the generated diff 289 * @param strip true if any unknown classes should be stripped from the output, false otherwise 290 * @return 291 */ newApi(String origApiPath, String newApiPath, String outputPath, boolean strip)292 static int newApi(String origApiPath, String newApiPath, String outputPath, boolean strip) { 293 ApiInfo origApi, newApi; 294 try { 295 origApi = parseApi(origApiPath); 296 } catch (ApiParseException e) { 297 e.printStackTrace(); 298 System.err.println("Error parsing API: " + origApiPath); 299 return 1; 300 } 301 try { 302 newApi = parseApi(newApiPath); 303 } catch (ApiParseException e) { 304 e.printStackTrace(); 305 System.err.println("Error parsing API: " + newApiPath); 306 return 1; 307 } 308 List<PackageInfo> pkgInfoDiff = new ArrayList<>(); 309 if (!origApi.isConsistent(newApi, pkgInfoDiff)) { 310 PrintStream apiWriter = null; 311 try { 312 apiWriter = new PrintStream(outputPath); 313 } catch (FileNotFoundException ex) { 314 System.err.println("can't open file: " + outputPath); 315 } 316 Stubs.writeXml(apiWriter, pkgInfoDiff, strip); 317 } else { 318 System.err.println("No API change detected, not generating diff."); 319 } 320 return 0; 321 } 322 } 323