Home | History | Annotate | Download | only in doclava
      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;
     18 
     19 import com.google.clearsilver.jsilver.data.Data;
     20 import com.google.doclava.apicheck.ApiCheck;
     21 import com.google.doclava.apicheck.ApiInfo;
     22 import com.google.doclava.apicheck.ApiParseException;
     23 import java.io.PrintWriter;
     24 import java.io.StringWriter;
     25 import java.util.ArrayList;
     26 import java.util.Collections;
     27 import java.util.LinkedHashMap;
     28 import java.util.List;
     29 import java.util.Map;
     30 
     31 
     32 /**
     33  * Applies version information to the Doclava class model from apicheck XML files. Sample usage:
     34  *
     35  * <pre>
     36  *   ClassInfo[] classInfos = ...
     37  *
     38  *   SinceTagger sinceTagger = new SinceTagger()
     39  *   sinceTagger.addVersion("frameworks/base/api/1.xml", "product 1.0")
     40  *   sinceTagger.addVersion("frameworks/base/api/2.xml", "product 1.5")
     41  *   sinceTagger.tagAll(...);
     42  * </pre>
     43  */
     44 public class SinceTagger {
     45 
     46   private final Map<String, String> xmlToName = new LinkedHashMap<String, String>();
     47 
     48   /**
     49    * Specifies the apicheck XML file and the API version it holds. Calls to this method should be
     50    * called in order from oldest version to newest.
     51    */
     52   public void addVersion(String file, String name) {
     53     xmlToName.put(file, name);
     54   }
     55 
     56   public void tagAll(ClassInfo[] classDocs) {
     57     // read through the XML files in order, applying their since information
     58     // to the Javadoc models
     59     for (Map.Entry<String, String> versionSpec : xmlToName.entrySet()) {
     60       String xmlFile = versionSpec.getKey();
     61       String versionName = versionSpec.getValue();
     62 
     63       ApiInfo specApi;
     64       try {
     65         specApi = new ApiCheck().parseApi(xmlFile);
     66       } catch (ApiParseException e) {
     67         StringWriter stackTraceWriter = new StringWriter();
     68         e.printStackTrace(new PrintWriter(stackTraceWriter));
     69         Errors.error(Errors.BROKEN_SINCE_FILE, null, "Failed to parse " + xmlFile
     70                 + " for " + versionName + " since data.\n" + stackTraceWriter.toString());
     71         continue;
     72       }
     73 
     74       applyVersionsFromSpec(versionName, specApi, classDocs);
     75     }
     76 
     77     if (!xmlToName.isEmpty()) {
     78       warnForMissingVersions(classDocs);
     79     }
     80   }
     81 
     82   public boolean hasVersions() {
     83     return !xmlToName.isEmpty();
     84   }
     85 
     86   /**
     87    * Writes an index of the version names to {@code data}.
     88    */
     89   public void writeVersionNames(Data data) {
     90     int index = 1;
     91     for (String version : xmlToName.values()) {
     92       data.setValue("since." + index + ".name", version);
     93       index++;
     94     }
     95   }
     96 
     97   /**
     98    * Applies the version information to {@code classDocs} where not already present.
     99    *
    100    * @param versionName the version name
    101    * @param specApi the spec for this version. If a symbol is in this spec, it was present in the
    102    *        named version
    103    * @param classDocs the doc model to update
    104    */
    105   private void applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs) {
    106     for (ClassInfo classDoc : classDocs) {
    107       PackageInfo packageSpec
    108           = specApi.getPackages().get(classDoc.containingPackage().name());
    109 
    110       if (packageSpec == null) {
    111         continue;
    112       }
    113 
    114       ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name());
    115 
    116       if (classSpec == null) {
    117         continue;
    118       }
    119 
    120       versionPackage(versionName, classDoc.containingPackage());
    121       versionClass(versionName, classSpec, classDoc);
    122       versionConstructors(versionName, classSpec, classDoc);
    123       versionFields(versionName, classSpec, classDoc);
    124       versionMethods(versionName, classSpec, classDoc);
    125     }
    126   }
    127 
    128   /**
    129    * Applies version information to {@code doc} where not already present.
    130    */
    131   private void versionPackage(String versionName, PackageInfo doc) {
    132     if (doc.getSince() == null) {
    133       doc.setSince(versionName);
    134     }
    135   }
    136 
    137   /**
    138    * Applies version information to {@code doc} where not already present.
    139    */
    140   private void versionClass(String versionName, ClassInfo spec, ClassInfo doc) {
    141     if (doc.getSince() == null) {
    142       doc.setSince(versionName);
    143     }
    144 
    145     // Set deprecated version
    146     if (doc.isDeprecated() && doc.getDeprecatedSince() == null) {
    147       if (spec.isDeprecated()) {
    148         doc.setDeprecatedSince(versionName);
    149       }
    150     }
    151   }
    152 
    153   /**
    154    * Applies version information from {@code spec} to {@code doc} where not already present.
    155    */
    156   private void versionConstructors(String versionName, ClassInfo spec, ClassInfo doc) {
    157     for (MethodInfo constructor : doc.constructors()) {
    158       if (constructor.getSince() == null
    159           && spec.hasConstructor(constructor)) {
    160         constructor.setSince(versionName);
    161       }
    162 
    163       // Set deprecated version
    164       if (constructor.isDeprecated() && constructor.getDeprecatedSince() == null) {
    165         // Find matching field from API spec
    166         if (spec.allConstructorsMap().containsKey(constructor.getHashableName())) {
    167           MethodInfo specConstructor = spec.allConstructorsMap().get(constructor.getHashableName());
    168           if (specConstructor.isDeprecated()) {
    169             constructor.setDeprecatedSince(versionName);
    170           }
    171         }
    172       }
    173     }
    174   }
    175 
    176   /**
    177    * Applies version information from {@code spec} to {@code doc} where not already present.
    178    */
    179   private void versionFields(String versionName, ClassInfo spec, ClassInfo doc) {
    180     for (FieldInfo field : doc.fields()) {
    181       if (field.getSince() == null && (spec.allFields().containsKey(field.name()) ||
    182                                        spec.allEnums().containsKey(field.name()))) {
    183         field.setSince(versionName);
    184       }
    185 
    186       // Set deprecated version
    187       if (field.isDeprecated() && field.getDeprecatedSince() == null) {
    188         // Find matching field from API spec
    189         if (spec.allFields().containsKey(field.name())) {
    190           FieldInfo specField = spec.allFields().get(field.name());
    191           if (specField.isDeprecated()) {
    192             field.setDeprecatedSince(versionName);
    193           }
    194         }
    195       }
    196     }
    197   }
    198 
    199   /**
    200    * Applies version information from {@code spec} to {@code doc} where not already present.
    201    */
    202   private void versionMethods(String versionName, ClassInfo spec, ClassInfo doc) {
    203     for (MethodInfo method : doc.methods()) {
    204 
    205       // Set deprecated version
    206       if (method.isDeprecated() && method.getDeprecatedSince() == null) {
    207         // Find matching method from API spec
    208         if (spec.allMethods().containsKey(method.getHashableName())) {
    209           MethodInfo specMethod = spec.allMethods().get(method.getHashableName());
    210           if (specMethod.isDeprecated()) {
    211             method.setDeprecatedSince(versionName);
    212           }
    213         }
    214       }
    215 
    216       if (method.getSince() != null) {
    217         continue;
    218       }
    219 
    220       for (ClassInfo superclass : spec.hierarchy()) {
    221         if (superclass.allMethods().containsKey(method.getHashableName())) {
    222           method.setSince(versionName);
    223           break;
    224         }
    225       }
    226     }
    227   }
    228 
    229   /**
    230    * Warns if any symbols are missing version information. When configured properly, this will yield
    231    * zero warnings because {@code apicheck} guarantees that all symbols are present in the most
    232    * recent API.
    233    */
    234   private void warnForMissingVersions(ClassInfo[] classDocs) {
    235     for (ClassInfo claz : classDocs) {
    236       if (!checkLevelRecursive(claz)) {
    237         continue;
    238       }
    239 
    240       if (claz.getSince() == null) {
    241         Errors.error(Errors.NO_SINCE_DATA, claz.position(), "XML missing class "
    242             + claz.qualifiedName());
    243       }
    244 
    245       for (FieldInfo field : missingVersions(claz.fields())) {
    246         Errors.error(Errors.NO_SINCE_DATA, field.position(), "XML missing field "
    247             + claz.qualifiedName() + "#" + field.name());
    248       }
    249 
    250       for (MethodInfo constructor : missingVersions(claz.constructors())) {
    251         Errors.error(Errors.NO_SINCE_DATA, constructor.position(), "XML missing constructor "
    252             + claz.qualifiedName() + "#" + constructor.getHashableName());
    253       }
    254 
    255       for (MethodInfo method : missingVersions(claz.methods())) {
    256         Errors.error(Errors.NO_SINCE_DATA, method.position(), "XML missing method "
    257             + claz.qualifiedName() + "#" + method.getHashableName());
    258       }
    259     }
    260   }
    261 
    262   /**
    263    * Returns the DocInfos in {@code all} that are documented but do not have since tags.
    264    */
    265   private <T extends MemberInfo> Iterable<T> missingVersions(ArrayList<T> all) {
    266     List<T> result = Collections.emptyList();
    267     for (T t : all) {
    268       // if this member has version info or isn't documented, skip it
    269       if (t.getSince() != null || t.isHidden() || !checkLevelRecursive(t.realContainingClass())) {
    270         continue;
    271       }
    272 
    273       if (result.isEmpty()) {
    274         result = new ArrayList<T>(); // lazily construct a mutable list
    275       }
    276       result.add(t);
    277     }
    278     return result;
    279   }
    280 
    281   /**
    282    * Returns true if {@code claz} and all containing classes are documented. The result may be used
    283    * to filter out members that exist in the API data structure but aren't a part of the API.
    284    */
    285   private boolean checkLevelRecursive(ClassInfo claz) {
    286     for (ClassInfo c = claz; c != null; c = c.containingClass()) {
    287       if (!c.checkLevel()) {
    288         return false;
    289       }
    290     }
    291     return true;
    292   }
    293 }
    294