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    */
addVersion(String file, String name)52   public void addVersion(String file, String name) {
53     xmlToName.put(file, name);
54   }
55 
tagAll(ClassInfo[] classDocs)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 
hasVersions()82   public boolean hasVersions() {
83     return !xmlToName.isEmpty();
84   }
85 
86   /**
87    * Writes an index of the version names to {@code data}.
88    */
writeVersionNames(Data data)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    */
applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs)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    */
versionPackage(String versionName, PackageInfo doc)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    */
versionClass(String versionName, ClassInfo spec, ClassInfo doc)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    */
versionConstructors(String versionName, ClassInfo spec, ClassInfo doc)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    */
versionFields(String versionName, ClassInfo spec, ClassInfo doc)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    */
versionMethods(String versionName, ClassInfo spec, ClassInfo doc)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    */
warnForMissingVersions(ClassInfo[] classDocs)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    */
missingVersions(ArrayList<T> all)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    */
checkLevelRecursive(ClassInfo claz)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