1 // Copyright 2009 Google Inc. All Rights Reserved. 2 3 import com.android.apicheck.ApiCheck; 4 import com.android.apicheck.ApiInfo; 5 6 import java.util.ArrayList; 7 import java.util.LinkedHashMap; 8 import java.util.List; 9 import java.util.Map; 10 import java.util.Collections; 11 12 import org.clearsilver.HDF; 13 14 /** 15 * Applies version information to the DroidDoc class model from apicheck XML 16 * files. Sample usage: 17 * <pre> 18 * ClassInfo[] classInfos = ... 19 * 20 * SinceTagger sinceTagger = new SinceTagger() 21 * sinceTagger.addVersion("frameworks/base/api/1.xml", "Android 1.0") 22 * sinceTagger.addVersion("frameworks/base/api/2.xml", "Android 1.5") 23 * sinceTagger.tagAll(...); 24 * </pre> 25 */ 26 public class SinceTagger { 27 28 private final Map<String, String> xmlToName 29 = new LinkedHashMap<String, String>(); 30 31 /** 32 * Specifies the apicheck XML file and the API version it holds. Calls to 33 * this method should be called in order from oldest version to newest. 34 */ addVersion(String file, String name)35 public void addVersion(String file, String name) { 36 xmlToName.put(file, name); 37 } 38 tagAll(ClassInfo[] classDocs)39 public void tagAll(ClassInfo[] classDocs) { 40 // read through the XML files in order, applying their since information 41 // to the Javadoc models 42 for (Map.Entry<String, String> versionSpec : xmlToName.entrySet()) { 43 String xmlFile = versionSpec.getKey(); 44 String versionName = versionSpec.getValue(); 45 ApiInfo specApi = new ApiCheck().parseApi(xmlFile); 46 47 applyVersionsFromSpec(versionName, specApi, classDocs); 48 } 49 50 if (!xmlToName.isEmpty()) { 51 warnForMissingVersions(classDocs); 52 } 53 } 54 55 /** 56 * Writes an index of the version names to {@code data}. 57 */ writeVersionNames(HDF data)58 public void writeVersionNames(HDF data) { 59 int index = 1; 60 for (String version : xmlToName.values()) { 61 data.setValue("since." + index + ".name", version); 62 index++; 63 } 64 } 65 66 /** 67 * Applies the version information to {@code classDocs} where not already 68 * present. 69 * 70 * @param versionName the version name 71 * @param specApi the spec for this version. If a symbol is in this spec, it 72 * was present in the named version 73 * @param classDocs the doc model to update 74 */ applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs)75 private void applyVersionsFromSpec(String versionName, 76 ApiInfo specApi, ClassInfo[] classDocs) { 77 for (ClassInfo classDoc : classDocs) { 78 com.android.apicheck.PackageInfo packageSpec 79 = specApi.getPackages().get(classDoc.containingPackage().name()); 80 81 if (packageSpec == null) { 82 continue; 83 } 84 85 com.android.apicheck.ClassInfo classSpec 86 = packageSpec.allClasses().get(classDoc.name()); 87 88 if (classSpec == null) { 89 continue; 90 } 91 92 versionPackage(versionName, classDoc.containingPackage()); 93 versionClass(versionName, classDoc); 94 versionConstructors(versionName, classSpec, classDoc); 95 versionFields(versionName, classSpec, classDoc); 96 versionMethods(versionName, classSpec, classDoc); 97 } 98 } 99 100 /** 101 * Applies version information to {@code doc} where not already present. 102 */ versionPackage(String versionName, PackageInfo doc)103 private void versionPackage(String versionName, PackageInfo doc) { 104 if (doc.getSince() == null) { 105 doc.setSince(versionName); 106 } 107 } 108 109 /** 110 * Applies version information to {@code doc} where not already present. 111 */ versionClass(String versionName, ClassInfo doc)112 private void versionClass(String versionName, ClassInfo doc) { 113 if (doc.getSince() == null) { 114 doc.setSince(versionName); 115 } 116 } 117 118 /** 119 * Applies version information from {@code spec} to {@code doc} where not 120 * already present. 121 */ versionConstructors(String versionName, com.android.apicheck.ClassInfo spec, ClassInfo doc)122 private void versionConstructors(String versionName, 123 com.android.apicheck.ClassInfo spec, ClassInfo doc) { 124 for (MethodInfo constructor : doc.constructors()) { 125 if (constructor.getSince() == null 126 && spec.allConstructors().containsKey(constructor.getHashableName())) { 127 constructor.setSince(versionName); 128 } 129 } 130 } 131 132 /** 133 * Applies version information from {@code spec} to {@code doc} where not 134 * already present. 135 */ versionFields(String versionName, com.android.apicheck.ClassInfo spec, ClassInfo doc)136 private void versionFields(String versionName, 137 com.android.apicheck.ClassInfo spec, ClassInfo doc) { 138 for (FieldInfo field : doc.fields()) { 139 if (field.getSince() == null 140 && spec.allFields().containsKey(field.name())) { 141 field.setSince(versionName); 142 } 143 } 144 } 145 146 /** 147 * Applies version information from {@code spec} to {@code doc} where not 148 * already present. 149 */ versionMethods(String versionName, com.android.apicheck.ClassInfo spec, ClassInfo doc)150 private void versionMethods(String versionName, 151 com.android.apicheck.ClassInfo spec, ClassInfo doc) { 152 for (MethodInfo method : doc.methods()) { 153 if (method.getSince() != null) { 154 continue; 155 } 156 157 for (com.android.apicheck.ClassInfo superclass : spec.hierarchy()) { 158 if (superclass.allMethods().containsKey(method.getHashableName())) { 159 method.setSince(versionName); 160 break; 161 } 162 } 163 } 164 } 165 166 /** 167 * Warns if any symbols are missing version information. When configured 168 * properly, this will yield zero warnings because {@code apicheck} 169 * guarantees that all symbols are present in the most recent API. 170 */ warnForMissingVersions(ClassInfo[] classDocs)171 private void warnForMissingVersions(ClassInfo[] classDocs) { 172 for (ClassInfo claz : classDocs) { 173 if (!checkLevelRecursive(claz)) { 174 continue; 175 } 176 177 if (claz.getSince() == null) { 178 Errors.error(Errors.NO_SINCE_DATA, claz.position(), 179 "XML missing class " + claz.qualifiedName()); 180 } 181 182 for (FieldInfo field : missingVersions(claz.fields())) { 183 Errors.error(Errors.NO_SINCE_DATA, field.position(), 184 "XML missing field " + claz.qualifiedName() 185 + "#" + field.name()); 186 } 187 188 for (MethodInfo constructor : missingVersions(claz.constructors())) { 189 Errors.error(Errors.NO_SINCE_DATA, constructor.position(), 190 "XML missing constructor " + claz.qualifiedName() 191 + "#" + constructor.getHashableName()); 192 } 193 194 for (MethodInfo method : missingVersions(claz.methods())) { 195 Errors.error(Errors.NO_SINCE_DATA, method.position(), 196 "XML missing method " + claz.qualifiedName() 197 + "#" + method.getHashableName()); 198 } 199 } 200 } 201 202 /** 203 * Returns the DocInfos in {@code all} that are documented but do not have 204 * since tags. 205 */ missingVersions(T[] all)206 private <T extends MemberInfo> Iterable<T> missingVersions(T[] all) { 207 List<T> result = Collections.emptyList(); 208 for (T t : all) { 209 // if this member has version info or isn't documented, skip it 210 if (t.getSince() != null 211 || t.isHidden() 212 || !checkLevelRecursive(t.realContainingClass())) { 213 continue; 214 } 215 216 if (result.isEmpty()) { 217 result = new ArrayList<T>(); // lazily construct a mutable list 218 } 219 result.add(t); 220 } 221 return result; 222 } 223 224 /** 225 * Returns true if {@code claz} and all containing classes are documented. 226 * The result may be used to filter out members that exist in the API 227 * data structure but aren't a part of the API. 228 */ checkLevelRecursive(ClassInfo claz)229 private boolean checkLevelRecursive(ClassInfo claz) { 230 for (ClassInfo c = claz; c != null; c = c.containingClass()) { 231 if (!c.checkLevel()) { 232 return false; 233 } 234 } 235 return true; 236 } 237 } 238