blob: 19998188b393d4a8061da04ed60e0a375b34cd6b [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class CompatInfo {
public static class CompatChange {
public final String name;
public final long id;
public final String description;
public final String definedInClass;
public final String sourceFile;
public final int sourceLine;
public final boolean disabled;
public final boolean loggingOnly;
public final int enableSinceTargetSdk;
CompatChange(String name, long id, String description, String definedInClass,
String sourceFile, int sourceLine, boolean disabled, boolean loggingOnly,
int enableAfterTargetSdk, int enableSinceTargetSdk) {
this.name = name;
this.id = id;
this.description = description;
this.definedInClass = definedInClass;
this.sourceFile = sourceFile;
this.sourceLine = sourceLine;
this.disabled = disabled;
this.loggingOnly = loggingOnly;
if (enableSinceTargetSdk > 0) {
this.enableSinceTargetSdk = enableSinceTargetSdk;
} else if (enableAfterTargetSdk > 0) {
this.enableSinceTargetSdk = enableAfterTargetSdk + 1;
} else {
this.enableSinceTargetSdk = 0;
}
}
static class Builder {
private String mName;
private long mId;
private String mDescription;
private String mDefinedInClass;
private String mSourceFile;
private int mSourceLine;
private boolean mDisabled;
private boolean mLoggingOnly;
private int mEnableAfterTargetSdk;
private int mEnableSinceTargetSdk;
CompatChange build() {
return new CompatChange(
mName, mId, mDescription, mDefinedInClass, mSourceFile, mSourceLine,
mDisabled, mLoggingOnly, mEnableAfterTargetSdk, mEnableSinceTargetSdk);
}
Builder name(String name) {
mName = name;
return this;
}
Builder id(long id) {
mId = id;
return this;
}
Builder description(String description) {
mDescription = description;
return this;
}
Builder definedInClass(String definedInClass) {
mDefinedInClass = definedInClass;
return this;
}
Builder sourcePosition(String sourcePosition) throws SAXException {
if (sourcePosition != null) {
int colonPos = sourcePosition.indexOf(":");
if (colonPos == -1) {
throw new SAXException("Invalid source position: " + sourcePosition);
}
mSourceFile = sourcePosition.substring(0, colonPos);
try {
mSourceLine = Integer.parseInt(sourcePosition.substring(colonPos + 1));
} catch (NumberFormatException nfe) {
throw new SAXException("Invalid source position: " + sourcePosition, nfe);
}
}
return this;
}
boolean parseBool(String value) {
if (value == null) {
return false;
}
boolean result = Boolean.parseBoolean(value);
return result;
}
Builder disabled(String disabled) {
mDisabled = parseBool(disabled);
return this;
}
Builder loggingOnly(String loggingOnly) {
mLoggingOnly = parseBool(loggingOnly);
return this;
}
Builder enableAfterTargetSdk(String enableAfter) throws SAXException {
if (enableAfter == null) {
mEnableAfterTargetSdk = 0;
} else {
try {
mEnableAfterTargetSdk = Integer.parseInt(enableAfter);
} catch (NumberFormatException nfe) {
throw new SAXException("Invalid SDK version int: " + enableAfter, nfe);
}
}
return this;
}
Builder enableSinceTargetSdk(String enableSince) throws SAXException {
if (enableSince == null) {
mEnableSinceTargetSdk = 0;
} else {
try {
mEnableSinceTargetSdk = Integer.parseInt(enableSince);
} catch (NumberFormatException nfe) {
throw new SAXException("Invalid SDK version int: " + enableSince, nfe);
}
}
return this;
}
}
}
private class CompatConfigXmlParser extends DefaultHandler {
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (qName.equals("compat-change")) {
mCurrentChange = new CompatChange.Builder();
String idStr = attributes.getValue("id");
if (idStr == null) {
throw new SAXException("<compat-change> element has no id");
}
try {
mCurrentChange.id(Long.parseLong(idStr));
} catch (NumberFormatException nfe) {
throw new SAXException("<compat-change> id is not a valid long", nfe);
}
mCurrentChange.name(attributes.getValue("name"))
.description(attributes.getValue("description"))
.enableAfterTargetSdk(attributes.getValue("enableAfterTargetSdk"))
.enableSinceTargetSdk(attributes.getValue("enableSinceTargetSdk"))
.disabled(attributes.getValue("disabled"))
.loggingOnly(attributes.getValue("loggingOnly"));
} else if (qName.equals("meta-data")) {
if (mCurrentChange == null) {
throw new SAXException("<meta-data> tag with no enclosing <compat-change>");
}
mCurrentChange.definedInClass(attributes.getValue("definedIn"))
.sourcePosition(attributes.getValue("sourcePosition"));
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if (qName.equals("compat-change")) {
mChanges.add(mCurrentChange.build());
mCurrentChange = null;
}
}
}
public static CompatInfo readCompatConfig(String source) {
CompatInfo config = new CompatInfo();
try {
InputStream in = new FileInputStream(new File(source));
XMLReader xmlreader = XMLReaderFactory.createXMLReader();
xmlreader.setContentHandler(config.mXmlParser);
xmlreader.setErrorHandler(config.mXmlParser);
xmlreader.parse(new InputSource(in));
in.close();
return config;
} catch (SAXException e) {
throw new RuntimeException("Failed to parse " + source, e);
} catch (IOException e) {
throw new RuntimeException("Failed to read " + source, e);
}
}
private final CompatConfigXmlParser mXmlParser = new CompatConfigXmlParser();
private CompatChange.Builder mCurrentChange;
private List<CompatChange> mChanges = new ArrayList<>();
public List<CompatChange> getChanges() {
return mChanges;
}
public void makeHDF(Data hdf) {
// We construct a Comment for each compat change to re-use the default docs generation support
// for comments.
mChanges.sort(Comparator.comparing(a -> a.name));
for (int i = 0; i < mChanges.size(); ++i) {
CompatInfo.CompatChange change = mChanges.get(i);
// we will get null ClassInfo here if the defining class is not in the SDK.
ContainerInfo definedInContainer = Converter.obtainClass(change.definedInClass);
if (definedInContainer == null) {
// This happens when the class defining the @ChangeId constant is not included in
// the sources that the SDK docs are generated from. Using package "android" as the
// container works, but means we lose the context of the original javadoc comment.
// This means that if the javadoc comment refers to classes imported by it's
// containing source file, we cannot resolve those imports here.
// TODO see if we could somehow plumb the import list from the original source file,
// via compat_config.xml, so we can resolve links properly here?
definedInContainer = Converter.obtainPackage("android");
}
if (change.description == null) {
throw new RuntimeException("No description found for @ChangeId " + change.name);
}
Comment comment = new Comment(change.description, definedInContainer, new SourcePositionInfo(
change.sourceFile, change.sourceLine, 1));
String path = "change." + i;
hdf.setValue(path + ".id", Long.toString(change.id));
hdf.setValue(path + ".name", change.name);
if (change.enableSinceTargetSdk != 0) {
hdf.setValue(path + ".enableSinceTargetSdk",
Integer.toString(change.enableSinceTargetSdk));
}
if (change.loggingOnly) {
hdf.setValue(path + ".loggingOnly", Boolean.toString(true));
}
if (change.disabled) {
hdf.setValue(path + ".disabled", Boolean.toString(true));
}
TagInfo.makeHDF(hdf, path + ".descr", comment.tags());
}
}
}