SinceTagger.java revision 920dbbbaca6aa578f3b26d89e99d12754c26ed60
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
17package com.google.doclava;
18
19import com.google.clearsilver.jsilver.data.Data;
20import com.google.doclava.apicheck.ApiCheck;
21import com.google.doclava.apicheck.ApiInfo;
22import com.google.doclava.apicheck.ApiParseException;
23
24
25import java.util.ArrayList;
26import java.util.LinkedHashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.Collections;
30
31
32/**
33 * Applies version information to the DroidDoc 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", "Android 1.0")
40 *   sinceTagger.addVersion("frameworks/base/api/2.xml", "Android 1.5")
41 *   sinceTagger.tagAll(...);
42 * </pre>
43 */
44public 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        Errors.error(Errors.NO_SINCE_DATA, null, "Could not add since data for " + versionName);
68        continue;
69      }
70
71      applyVersionsFromSpec(versionName, specApi, classDocs);
72    }
73
74    if (!xmlToName.isEmpty()) {
75      warnForMissingVersions(classDocs);
76    }
77  }
78
79  public boolean hasVersions() {
80    return !xmlToName.isEmpty();
81  }
82
83  /**
84   * Writes an index of the version names to {@code data}.
85   */
86  public void writeVersionNames(Data data) {
87    int index = 1;
88    for (String version : xmlToName.values()) {
89      data.setValue("since." + index + ".name", version);
90      index++;
91    }
92  }
93
94  /**
95   * Applies the version information to {@code classDocs} where not already present.
96   *
97   * @param versionName the version name
98   * @param specApi the spec for this version. If a symbol is in this spec, it was present in the
99   *        named version
100   * @param classDocs the doc model to update
101   */
102  private void applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs) {
103    for (ClassInfo classDoc : classDocs) {
104      PackageInfo packageSpec
105          = specApi.getPackages().get(classDoc.containingPackage().name());
106
107      if (packageSpec == null) {
108        continue;
109      }
110
111      ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name());
112
113      if (classSpec == null) {
114        continue;
115      }
116
117      versionPackage(versionName, classDoc.containingPackage());
118      versionClass(versionName, classDoc);
119      versionConstructors(versionName, classSpec, classDoc);
120      versionFields(versionName, classSpec, classDoc);
121      versionMethods(versionName, classSpec, classDoc);
122    }
123  }
124
125  /**
126   * Applies version information to {@code doc} where not already present.
127   */
128  private void versionPackage(String versionName, PackageInfo doc) {
129    if (doc.getSince() == null) {
130      doc.setSince(versionName);
131    }
132  }
133
134  /**
135   * Applies version information to {@code doc} where not already present.
136   */
137  private void versionClass(String versionName, ClassInfo doc) {
138    if (doc.getSince() == null) {
139      doc.setSince(versionName);
140    }
141  }
142
143  /**
144   * Applies version information from {@code spec} to {@code doc} where not already present.
145   */
146  private void versionConstructors(String versionName, ClassInfo spec, ClassInfo doc) {
147    for (MethodInfo constructor : doc.constructors()) {
148      if (constructor.getSince() == null
149          && spec.hasConstructor(constructor)) {
150        constructor.setSince(versionName);
151      }
152    }
153  }
154
155  /**
156   * Applies version information from {@code spec} to {@code doc} where not already present.
157   */
158  private void versionFields(String versionName, ClassInfo spec, ClassInfo doc) {
159    for (FieldInfo field : doc.fields()) {
160      if (field.getSince() == null && spec.allFields().containsKey(field.name())) {
161        field.setSince(versionName);
162      }
163    }
164  }
165
166  /**
167   * Applies version information from {@code spec} to {@code doc} where not already present.
168   */
169  private void versionMethods(String versionName, ClassInfo spec, ClassInfo doc) {
170    for (MethodInfo method : doc.methods()) {
171      if (method.getSince() != null) {
172        continue;
173      }
174
175      for (ClassInfo superclass : spec.hierarchy()) {
176        if (superclass.allMethods().containsKey(method.getHashableName())) {
177          method.setSince(versionName);
178          break;
179        }
180      }
181    }
182  }
183
184  /**
185   * Warns if any symbols are missing version information. When configured properly, this will yield
186   * zero warnings because {@code apicheck} guarantees that all symbols are present in the most
187   * recent API.
188   */
189  private void warnForMissingVersions(ClassInfo[] classDocs) {
190    for (ClassInfo claz : classDocs) {
191      if (!checkLevelRecursive(claz)) {
192        continue;
193      }
194
195      if (claz.getSince() == null) {
196        Errors.error(Errors.NO_SINCE_DATA, claz.position(), "XML missing class "
197            + claz.qualifiedName());
198      }
199
200      for (FieldInfo field : missingVersions(claz.fields())) {
201        Errors.error(Errors.NO_SINCE_DATA, field.position(), "XML missing field "
202            + claz.qualifiedName() + "#" + field.name());
203      }
204
205      for (MethodInfo constructor : missingVersions(claz.constructors())) {
206        Errors.error(Errors.NO_SINCE_DATA, constructor.position(), "XML missing constructor "
207            + claz.qualifiedName() + "#" + constructor.getHashableName());
208      }
209
210      for (MethodInfo method : missingVersions(claz.methods())) {
211        Errors.error(Errors.NO_SINCE_DATA, method.position(), "XML missing method "
212            + claz.qualifiedName() + "#" + method.getHashableName());
213      }
214    }
215  }
216
217  /**
218   * Returns the DocInfos in {@code all} that are documented but do not have since tags.
219   */
220  private <T extends MemberInfo> Iterable<T> missingVersions(T[] all) {
221    List<T> result = Collections.emptyList();
222    for (T t : all) {
223      // if this member has version info or isn't documented, skip it
224      if (t.getSince() != null || t.isHidden() || !checkLevelRecursive(t.realContainingClass())) {
225        continue;
226      }
227
228      if (result.isEmpty()) {
229        result = new ArrayList<T>(); // lazily construct a mutable list
230      }
231      result.add(t);
232    }
233    return result;
234  }
235
236  /**
237   * Returns true if {@code claz} and all containing classes are documented. The result may be used
238   * to filter out members that exist in the API data structure but aren't a part of the API.
239   */
240  private boolean checkLevelRecursive(ClassInfo claz) {
241    for (ClassInfo c = claz; c != null; c = c.containingClass()) {
242      if (!c.checkLevel()) {
243        return false;
244      }
245    }
246    return true;
247  }
248}
249