/* * Copyright (C) 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.doclava; import com.google.clearsilver.jsilver.data.Data; import com.google.doclava.apicheck.ApiCheck; import com.google.doclava.apicheck.ApiInfo; import com.google.doclava.apicheck.ApiParseException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Applies version information to the Doclava class model from apicheck XML files. Sample usage: * *
 *   ClassInfo[] classInfos = ...
 *
 *   SinceTagger sinceTagger = new SinceTagger()
 *   sinceTagger.addVersion("frameworks/base/api/1.xml", "product 1.0")
 *   sinceTagger.addVersion("frameworks/base/api/2.xml", "product 1.5")
 *   sinceTagger.tagAll(...);
 * 
*/ public class SinceTagger { private final Map xmlToName = new LinkedHashMap(); /** * Specifies the apicheck XML file and the API version it holds. Calls to this method should be * called in order from oldest version to newest. */ public void addVersion(String file, String name) { xmlToName.put(file, name); } public void tagAll(ClassInfo[] classDocs) { // read through the XML files in order, applying their since information // to the Javadoc models for (Map.Entry versionSpec : xmlToName.entrySet()) { String xmlFile = versionSpec.getKey(); String versionName = versionSpec.getValue(); ApiInfo specApi; try { specApi = new ApiCheck().parseApi(xmlFile); } catch (ApiParseException e) { StringWriter stackTraceWriter = new StringWriter(); e.printStackTrace(new PrintWriter(stackTraceWriter)); Errors.error(Errors.BROKEN_SINCE_FILE, null, "Failed to parse " + xmlFile + " for " + versionName + " since data.\n" + stackTraceWriter.toString()); continue; } applyVersionsFromSpec(versionName, specApi, classDocs); } if (!xmlToName.isEmpty()) { warnForMissingVersions(classDocs); } } public boolean hasVersions() { return !xmlToName.isEmpty(); } /** * Writes an index of the version names to {@code data}. */ public void writeVersionNames(Data data) { int index = 1; for (String version : xmlToName.values()) { data.setValue("since." + index + ".name", version); index++; } } /** * Applies the version information to {@code classDocs} where not already present. * * @param versionName the version name * @param specApi the spec for this version. If a symbol is in this spec, it was present in the * named version * @param classDocs the doc model to update */ private void applyVersionsFromSpec(String versionName, ApiInfo specApi, ClassInfo[] classDocs) { for (ClassInfo classDoc : classDocs) { PackageInfo packageSpec = specApi.getPackages().get(classDoc.containingPackage().name()); if (packageSpec == null) { continue; } ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name()); if (classSpec == null) { continue; } versionPackage(versionName, classDoc.containingPackage()); versionClass(versionName, classSpec, classDoc); versionConstructors(versionName, classSpec, classDoc); versionFields(versionName, classSpec, classDoc); versionMethods(versionName, classSpec, classDoc); } } /** * Applies version information to {@code doc} where not already present. */ private void versionPackage(String versionName, PackageInfo doc) { if (doc.getSince() == null) { doc.setSince(versionName); } } /** * Applies version information to {@code doc} where not already present. */ private void versionClass(String versionName, ClassInfo spec, ClassInfo doc) { if (doc.getSince() == null) { doc.setSince(versionName); } // Set deprecated version if (doc.isDeprecated() && doc.getDeprecatedSince() == null) { if (spec.isDeprecated()) { doc.setDeprecatedSince(versionName); } } } /** * Applies version information from {@code spec} to {@code doc} where not already present. */ private void versionConstructors(String versionName, ClassInfo spec, ClassInfo doc) { for (MethodInfo constructor : doc.constructors()) { if (constructor.getSince() == null && spec.hasConstructor(constructor)) { constructor.setSince(versionName); } // Set deprecated version if (constructor.isDeprecated() && constructor.getDeprecatedSince() == null) { // Find matching field from API spec if (spec.allConstructorsMap().containsKey(constructor.getHashableName())) { MethodInfo specConstructor = spec.allConstructorsMap().get(constructor.getHashableName()); if (specConstructor.isDeprecated()) { constructor.setDeprecatedSince(versionName); } } } } } /** * Applies version information from {@code spec} to {@code doc} where not already present. */ private void versionFields(String versionName, ClassInfo spec, ClassInfo doc) { for (FieldInfo field : doc.fields()) { if (field.getSince() == null && (spec.allFields().containsKey(field.name()) || spec.allEnums().containsKey(field.name()))) { field.setSince(versionName); } // Set deprecated version if (field.isDeprecated() && field.getDeprecatedSince() == null) { // Find matching field from API spec if (spec.allFields().containsKey(field.name())) { FieldInfo specField = spec.allFields().get(field.name()); if (specField.isDeprecated()) { field.setDeprecatedSince(versionName); } } } } } /** * Applies version information from {@code spec} to {@code doc} where not already present. */ private void versionMethods(String versionName, ClassInfo spec, ClassInfo doc) { for (MethodInfo method : doc.methods()) { // Set deprecated version if (method.isDeprecated() && method.getDeprecatedSince() == null) { // Find matching method from API spec if (spec.allMethods().containsKey(method.getHashableName())) { MethodInfo specMethod = spec.allMethods().get(method.getHashableName()); if (specMethod.isDeprecated()) { method.setDeprecatedSince(versionName); } } } if (method.getSince() != null) { continue; } for (ClassInfo superclass : spec.hierarchy()) { if (superclass.allMethods().containsKey(method.getHashableName())) { method.setSince(versionName); break; } } } } /** * Warns if any symbols are missing version information. When configured properly, this will yield * zero warnings because {@code apicheck} guarantees that all symbols are present in the most * recent API. */ private void warnForMissingVersions(ClassInfo[] classDocs) { for (ClassInfo claz : classDocs) { if (!checkLevelRecursive(claz)) { continue; } if (claz.getSince() == null) { Errors.error(Errors.NO_SINCE_DATA, claz.position(), "XML missing class " + claz.qualifiedName()); } for (FieldInfo field : missingVersions(claz.fields())) { Errors.error(Errors.NO_SINCE_DATA, field.position(), "XML missing field " + claz.qualifiedName() + "#" + field.name()); } for (MethodInfo constructor : missingVersions(claz.constructors())) { Errors.error(Errors.NO_SINCE_DATA, constructor.position(), "XML missing constructor " + claz.qualifiedName() + "#" + constructor.getHashableName()); } for (MethodInfo method : missingVersions(claz.methods())) { Errors.error(Errors.NO_SINCE_DATA, method.position(), "XML missing method " + claz.qualifiedName() + "#" + method.getHashableName()); } } } /** * Returns the DocInfos in {@code all} that are documented but do not have since tags. */ private Iterable missingVersions(ArrayList all) { List result = Collections.emptyList(); for (T t : all) { // if this member has version info or isn't documented, skip it if (t.getSince() != null || t.isHidden() || !checkLevelRecursive(t.realContainingClass())) { continue; } if (result.isEmpty()) { result = new ArrayList(); // lazily construct a mutable list } result.add(t); } return result; } /** * Returns true if {@code claz} and all containing classes are documented. The result may be used * to filter out members that exist in the API data structure but aren't a part of the API. */ private boolean checkLevelRecursive(ClassInfo claz) { for (ClassInfo c = claz; c != null; c = c.containingClass()) { if (!c.checkLevel()) { return false; } } return true; } }