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.Stack; 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 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 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])); 69 } else if (originalArgs.length == 4 && "-new_api".equals(originalArgs[0])) { 70 // command syntax: -new_api oldapi.txt newapi.txt diff.xml 71 // TODO: Support reading in other options for new_api, such as ignored classes/packages. 72 System.exit(newApi(originalArgs[1], originalArgs[2], originalArgs[3])); 73 } else { 74 ApiCheck acheck = new ApiCheck(); 75 Report report = acheck.checkApi(originalArgs); 76 77 Errors.printErrors(report.errors()); 78 System.exit(report.code); 79 } 80 } 81 82 /** 83 * Compares two api xml files for consistency. 84 */ 85 public Report checkApi(String[] originalArgs) { 86 // translate to an ArrayList<String> for munging 87 ArrayList<String> args = new ArrayList<String>(originalArgs.length); 88 for (String a : originalArgs) { 89 args.add(a); 90 } 91 92 // Not having having any classes or packages ignored is the common case. 93 // Avoid a hashCode call in a common loop by not passing in a HashSet in this case. 94 Set<String> ignoredPackages = null; 95 Set<String> ignoredClasses = null; 96 97 ArrayList<String[]> flags = ApiCheck.parseFlags(args); 98 for (String[] a : flags) { 99 if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) { 100 try { 101 int level = -1; 102 if (a[0].equals("-error")) { 103 level = Errors.ERROR; 104 } else if (a[0].equals("-warning")) { 105 level = Errors.WARNING; 106 } else if (a[0].equals("-hide")) { 107 level = Errors.HIDDEN; 108 } 109 Errors.setErrorLevel(Integer.parseInt(a[1]), level); 110 } catch (NumberFormatException e) { 111 System.err.println("Bad argument: " + a[0] + " " + a[1]); 112 return new Report(2, Errors.getErrors()); 113 } 114 } else if (a[0].equals("-ignoreClass")) { 115 if (ignoredClasses == null) { 116 ignoredClasses = new HashSet<String>(); 117 } 118 ignoredClasses.add(a[1]); 119 } else if (a[0].equals("-ignorePackage")) { 120 if (ignoredPackages == null) { 121 ignoredPackages = new HashSet<String>(); 122 } 123 ignoredPackages.add(a[1]); 124 } 125 } 126 127 ApiInfo oldApi; 128 ApiInfo newApi; 129 ApiInfo oldRemovedApi; 130 ApiInfo newRemovedApi; 131 132 // commandline options look like: 133 // [other optoins] old_api.txt new_api.txt old_removed_api.txt new_removed_api.txt 134 try { 135 oldApi = parseApi(args.get(0)); 136 newApi = parseApi(args.get(1)); 137 oldRemovedApi = parseApi(args.get(2)); 138 newRemovedApi = parseApi(args.get(3)); 139 } catch (ApiParseException e) { 140 e.printStackTrace(); 141 System.err.println("Error parsing API"); 142 return new Report(1, Errors.getErrors()); 143 } 144 145 // only run the consistency check if we haven't had XML parse errors 146 if (!Errors.hadError) { 147 oldApi.isConsistent(newApi, null, ignoredPackages, ignoredClasses); 148 } 149 150 if (!Errors.hadError) { 151 oldRemovedApi.isConsistent(newRemovedApi, null, ignoredPackages, ignoredClasses); 152 } 153 154 return new Report(Errors.hadError ? 1 : 0, Errors.getErrors()); 155 } 156 157 public static ApiInfo parseApi(String filename) throws ApiParseException { 158 InputStream stream = null; 159 Throwable textParsingError = null; 160 Throwable xmlParsingError = null; 161 // try it as our format 162 try { 163 stream = new FileInputStream(filename); 164 } catch (IOException e) { 165 throw new ApiParseException("Could not open file for parsing: " + filename, e); 166 } 167 try { 168 return ApiFile.parseApi(filename, stream); 169 } catch (ApiParseException exception) { 170 textParsingError = exception; 171 } finally { 172 try { 173 stream.close(); 174 } catch (IOException ignored) {} 175 } 176 // try it as xml 177 try { 178 stream = new FileInputStream(filename); 179 } catch (IOException e) { 180 throw new ApiParseException("Could not open file for parsing: " + filename, e); 181 } 182 try { 183 return XmlApiFile.parseApi(stream); 184 } catch (ApiParseException exception) { 185 xmlParsingError = exception; 186 } finally { 187 try { 188 stream.close(); 189 } catch (IOException ignored) {} 190 } 191 // The file has failed to parse both as XML and as text. Build the string in this order as 192 // the message is easier to read with that error at the end. 193 throw new ApiParseException(filename + 194 " failed to parse as xml: \"" + xmlParsingError.getMessage() + 195 "\" and as text: \"" + textParsingError.getMessage() + "\""); 196 } 197 198 public ApiInfo parseApi(URL url) throws ApiParseException { 199 InputStream stream = null; 200 // try it as our format 201 try { 202 stream = url.openStream(); 203 } catch (IOException e) { 204 throw new ApiParseException("Could not open stream for parsing: " + url, e); 205 } 206 try { 207 return ApiFile.parseApi(url.toString(), stream); 208 } catch (ApiParseException ignored) { 209 } finally { 210 try { 211 stream.close(); 212 } catch (IOException ignored) {} 213 } 214 // try it as xml 215 try { 216 stream = url.openStream(); 217 } catch (IOException e) { 218 throw new ApiParseException("Could not open stream for parsing: " + url, e); 219 } 220 try { 221 return XmlApiFile.parseApi(stream); 222 } finally { 223 try { 224 stream.close(); 225 } catch (IOException ignored) {} 226 } 227 } 228 229 public class Report { 230 private int code; 231 private Set<ErrorMessage> errors; 232 233 private Report(int code, Set<ErrorMessage> errors) { 234 this.code = code; 235 this.errors = errors; 236 } 237 238 public int code() { 239 return code; 240 } 241 242 public Set<ErrorMessage> errors() { 243 return errors; 244 } 245 } 246 247 static int convertToApi(String src, String dst) { 248 ApiInfo api; 249 try { 250 api = parseApi(src); 251 } catch (ApiParseException e) { 252 e.printStackTrace(); 253 System.err.println("Error parsing API: " + src); 254 return 1; 255 } 256 257 PrintStream apiWriter = null; 258 try { 259 apiWriter = new PrintStream(dst); 260 } catch (FileNotFoundException ex) { 261 System.err.println("can't open file: " + dst); 262 } 263 264 Stubs.writeApi(apiWriter, api.getPackages().values()); 265 266 return 0; 267 } 268 269 static int convertToXml(String src, String dst) { 270 ApiInfo api; 271 try { 272 api = parseApi(src); 273 } catch (ApiParseException e) { 274 e.printStackTrace(); 275 System.err.println("Error parsing API: " + src); 276 return 1; 277 } 278 279 PrintStream apiWriter = null; 280 try { 281 apiWriter = new PrintStream(dst); 282 } catch (FileNotFoundException ex) { 283 System.err.println("can't open file: " + dst); 284 } 285 286 Stubs.writeXml(apiWriter, api.getPackages().values()); 287 288 return 0; 289 } 290 291 /** 292 * Generates a "diff": where new API is trimmed down by removing existing methods found in old API 293 * @param origApiPath path to old API text file 294 * @param newApiPath path to new API text file 295 * @param outputPath output XML path for the generated diff 296 * @return 297 */ 298 static int newApi(String origApiPath, String newApiPath, String outputPath) { 299 ApiInfo origApi, newApi; 300 try { 301 origApi = parseApi(origApiPath); 302 } catch (ApiParseException e) { 303 e.printStackTrace(); 304 System.err.println("Error parsing API: " + origApiPath); 305 return 1; 306 } 307 try { 308 newApi = parseApi(newApiPath); 309 } catch (ApiParseException e) { 310 e.printStackTrace(); 311 System.err.println("Error parsing API: " + newApiPath); 312 return 1; 313 } 314 List<PackageInfo> pkgInfoDiff = new ArrayList<>(); 315 if (!origApi.isConsistent(newApi, pkgInfoDiff)) { 316 PrintStream apiWriter = null; 317 try { 318 apiWriter = new PrintStream(outputPath); 319 } catch (FileNotFoundException ex) { 320 System.err.println("can't open file: " + outputPath); 321 } 322 Stubs.writeXml(apiWriter, pkgInfoDiff); 323 } else { 324 System.err.println("No API change detected, not generating diff."); 325 } 326 return 0; 327 } 328 } 329