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