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;
23import java.io.PrintWriter;
24import java.io.StringWriter;
25import java.util.ArrayList;
26import java.util.Collections;
27import java.util.LinkedHashMap;
28import java.util.List;
29import 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 */
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        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.isHiddenOrRemoved() ||
270          !checkLevelRecursive(t.realContainingClass())) {
271        continue;
272      }
273
274      if (result.isEmpty()) {
275        result = new ArrayList<T>(); // lazily construct a mutable list
276      }
277      result.add(t);
278    }
279    return result;
280  }
281
282  /**
283   * Returns true if {@code claz} and all containing classes are documented. The result may be used
284   * to filter out members that exist in the API data structure but aren't a part of the API.
285   */
286  private boolean checkLevelRecursive(ClassInfo claz) {
287    for (ClassInfo c = claz; c != null; c = c.containingClass()) {
288      if (!c.checkLevel()) {
289        return false;
290      }
291    }
292    return true;
293  }
294}
295