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, (SourcePositionInfo) null,
70            "Failed to parse " + xmlFile + " for " + versionName + " since data.\n"
71                + stackTraceWriter.toString());
72        continue;
73      }
74
75      applyVersionsFromSpec(versionName, specApi, classDocs);
76    }
77
78    if (!xmlToName.isEmpty()) {
79      warnForMissingVersions(classDocs);
80    }
81  }
82
83  public boolean hasVersions() {
84    return !xmlToName.isEmpty();
85  }
86
87  /**
88   * Writes an index of the version names to {@code data}.
89   */
90  public void writeVersionNames(Data data) {
91    int index = 1;
92    for (String version : xmlToName.values()) {
93      data.setValue("since." + index + ".name", version);
94      index++;
95    }
96  }
97
98  /**
99   * Applies the version information to {@code classDocs} where not already present.
100   *
101   * @param versionName the version name
102   * @param specApi the spec for this version. If a symbol is in this spec, it was present in the
103   *        named version
104   * @param classDocs the doc model to update
105   */
106  private void applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs) {
107    for (ClassInfo classDoc : classDocs) {
108      PackageInfo packageSpec
109          = specApi.getPackages().get(classDoc.containingPackage().name());
110
111      if (packageSpec == null) {
112        continue;
113      }
114
115      ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name());
116
117      if (classSpec == null) {
118        continue;
119      }
120
121      versionPackage(versionName, classDoc.containingPackage());
122      versionClass(versionName, classSpec, classDoc);
123      versionConstructors(versionName, classSpec, classDoc);
124      versionFields(versionName, classSpec, classDoc);
125      versionMethods(versionName, classSpec, classDoc);
126    }
127  }
128
129  /**
130   * Applies version information to {@code doc} where not already present.
131   */
132  private void versionPackage(String versionName, PackageInfo doc) {
133    if (doc.getSince() == null) {
134      doc.setSince(versionName);
135    }
136  }
137
138  /**
139   * Applies version information to {@code doc} where not already present.
140   */
141  private void versionClass(String versionName, ClassInfo spec, ClassInfo doc) {
142    if (doc.getSince() == null) {
143      doc.setSince(versionName);
144    }
145
146    // Set deprecated version
147    if (doc.isDeprecated() && doc.getDeprecatedSince() == null) {
148      if (spec.isDeprecated()) {
149        doc.setDeprecatedSince(versionName);
150      }
151    }
152  }
153
154  /**
155   * Applies version information from {@code spec} to {@code doc} where not already present.
156   */
157  private void versionConstructors(String versionName, ClassInfo spec, ClassInfo doc) {
158    for (MethodInfo constructor : doc.constructors()) {
159      if (constructor.getSince() == null
160          && spec.hasConstructor(constructor)) {
161        constructor.setSince(versionName);
162      }
163
164      // Set deprecated version
165      if (constructor.isDeprecated() && constructor.getDeprecatedSince() == null) {
166        // Find matching field from API spec
167        if (spec.allConstructorsMap().containsKey(constructor.getHashableName())) {
168          MethodInfo specConstructor = spec.allConstructorsMap().get(constructor.getHashableName());
169          if (specConstructor.isDeprecated()) {
170            constructor.setDeprecatedSince(versionName);
171          }
172        }
173      }
174    }
175  }
176
177  /**
178   * Applies version information from {@code spec} to {@code doc} where not already present.
179   */
180  private void versionFields(String versionName, ClassInfo spec, ClassInfo doc) {
181    for (FieldInfo field : doc.fields()) {
182      if (field.getSince() == null && (spec.allFields().containsKey(field.name()) ||
183                                       spec.allEnums().containsKey(field.name()))) {
184        field.setSince(versionName);
185      }
186
187      // Set deprecated version
188      if (field.isDeprecated() && field.getDeprecatedSince() == null) {
189        // Find matching field from API spec
190        if (spec.allFields().containsKey(field.name())) {
191          FieldInfo specField = spec.allFields().get(field.name());
192          if (specField.isDeprecated()) {
193            field.setDeprecatedSince(versionName);
194          }
195        }
196      }
197    }
198  }
199
200  /**
201   * Applies version information from {@code spec} to {@code doc} where not already present.
202   */
203  private void versionMethods(String versionName, ClassInfo spec, ClassInfo doc) {
204    for (MethodInfo method : doc.methods()) {
205
206      // Set deprecated version
207      if (method.isDeprecated() && method.getDeprecatedSince() == null) {
208        // Find matching method from API spec
209        if (spec.allMethods().containsKey(method.getHashableName())) {
210          MethodInfo specMethod = spec.allMethods().get(method.getHashableName());
211          if (specMethod.isDeprecated()) {
212            method.setDeprecatedSince(versionName);
213          }
214        }
215      }
216
217      if (method.getSince() != null) {
218        continue;
219      }
220
221      for (ClassInfo superclass : spec.hierarchy()) {
222        if (superclass.allMethods().containsKey(method.getHashableName())) {
223          method.setSince(versionName);
224          break;
225        }
226      }
227    }
228  }
229
230  /**
231   * Warns if any symbols are missing version information. When configured properly, this will yield
232   * zero warnings because {@code apicheck} guarantees that all symbols are present in the most
233   * recent API.
234   */
235  private void warnForMissingVersions(ClassInfo[] classDocs) {
236    for (ClassInfo claz : classDocs) {
237      if (!checkLevelRecursive(claz)) {
238        continue;
239      }
240
241      if (claz.getSince() == null) {
242        Errors.error(Errors.NO_SINCE_DATA, claz.position(), "XML missing class "
243            + claz.qualifiedName());
244      }
245
246      for (FieldInfo field : missingVersions(claz.fields())) {
247        Errors.error(Errors.NO_SINCE_DATA, field.position(), "XML missing field "
248            + claz.qualifiedName() + "#" + field.name());
249      }
250
251      for (MethodInfo constructor : missingVersions(claz.constructors())) {
252        Errors.error(Errors.NO_SINCE_DATA, constructor.position(), "XML missing constructor "
253            + claz.qualifiedName() + "#" + constructor.getHashableName());
254      }
255
256      for (MethodInfo method : missingVersions(claz.methods())) {
257        Errors.error(Errors.NO_SINCE_DATA, method.position(), "XML missing method "
258            + claz.qualifiedName() + "#" + method.getHashableName());
259      }
260    }
261  }
262
263  /**
264   * Returns the DocInfos in {@code all} that are documented but do not have since tags.
265   */
266  private <T extends MemberInfo> Iterable<T> missingVersions(ArrayList<T> all) {
267    List<T> result = Collections.emptyList();
268    for (T t : all) {
269      // if this member has version info or isn't documented, skip it
270      if (t.getSince() != null || t.isHiddenOrRemoved() ||
271          !checkLevelRecursive(t.realContainingClass())) {
272        continue;
273      }
274
275      if (result.isEmpty()) {
276        result = new ArrayList<T>(); // lazily construct a mutable list
277      }
278      result.add(t);
279    }
280    return result;
281  }
282
283  /**
284   * Returns true if {@code claz} and all containing classes are documented. The result may be used
285   * to filter out members that exist in the API data structure but aren't a part of the API.
286   */
287  private boolean checkLevelRecursive(ClassInfo claz) {
288    for (ClassInfo c = claz; c != null; c = c.containingClass()) {
289      if (!c.checkLevel()) {
290        return false;
291      }
292    }
293    return true;
294  }
295}
296