blob: e324e95a0636e4f226e0cee954ea8d7832dfc1d0 [file] [log] [blame]
/*
* 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 DroidDoc class model from apicheck XML files. Sample usage:
*
* <pre>
* 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(...);
* </pre>
*/
public class SinceTagger {
private final Map<String, String> xmlToName = new LinkedHashMap<String, String>();
/**
* 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<String, String> 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, 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 doc) {
if (doc.getSince() == null) {
doc.setSince(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);
}
}
}
/**
* 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())) {
field.setSince(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()) {
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 <T extends MemberInfo> Iterable<T> missingVersions(ArrayList<T> all) {
List<T> 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<T>(); // 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;
}
}