• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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