Home | History | Annotate | Download | only in apicheck
      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