1 /* 2 * Copyright (C) 2020 The Android Open Source Project 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 21 import org.xml.sax.Attributes; 22 import org.xml.sax.InputSource; 23 import org.xml.sax.SAXException; 24 import org.xml.sax.XMLReader; 25 import org.xml.sax.helpers.DefaultHandler; 26 import org.xml.sax.helpers.XMLReaderFactory; 27 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.util.ArrayList; 33 import java.util.Comparator; 34 import java.util.List; 35 36 public class CompatInfo { 37 38 public static class CompatChange { 39 public final String name; 40 public final long id; 41 public final String description; 42 public final String definedInClass; 43 public final String sourceFile; 44 public final int sourceLine; 45 public final boolean disabled; 46 public final boolean loggingOnly; 47 public final int enableSinceTargetSdk; 48 49 CompatChange(String name, long id, String description, String definedInClass, String sourceFile, int sourceLine, boolean disabled, boolean loggingOnly, int enableAfterTargetSdk, int enableSinceTargetSdk)50 CompatChange(String name, long id, String description, String definedInClass, 51 String sourceFile, int sourceLine, boolean disabled, boolean loggingOnly, 52 int enableAfterTargetSdk, int enableSinceTargetSdk) { 53 this.name = name; 54 this.id = id; 55 this.description = description; 56 this.definedInClass = definedInClass; 57 this.sourceFile = sourceFile; 58 this.sourceLine = sourceLine; 59 this.disabled = disabled; 60 this.loggingOnly = loggingOnly; 61 if (enableSinceTargetSdk > 0) { 62 this.enableSinceTargetSdk = enableSinceTargetSdk; 63 } else if (enableAfterTargetSdk > 0) { 64 this.enableSinceTargetSdk = enableAfterTargetSdk + 1; 65 } else { 66 this.enableSinceTargetSdk = 0; 67 } 68 69 } 70 71 static class Builder { 72 private String mName; 73 private long mId; 74 private String mDescription; 75 private String mDefinedInClass; 76 private String mSourceFile; 77 private int mSourceLine; 78 private boolean mDisabled; 79 private boolean mLoggingOnly; 80 private int mEnableAfterTargetSdk; 81 private int mEnableSinceTargetSdk; 82 build()83 CompatChange build() { 84 return new CompatChange( 85 mName, mId, mDescription, mDefinedInClass, mSourceFile, mSourceLine, 86 mDisabled, mLoggingOnly, mEnableAfterTargetSdk, mEnableSinceTargetSdk); 87 } 88 name(String name)89 Builder name(String name) { 90 mName = name; 91 return this; 92 } 93 id(long id)94 Builder id(long id) { 95 mId = id; 96 return this; 97 } 98 description(String description)99 Builder description(String description) { 100 mDescription = description; 101 return this; 102 } 103 definedInClass(String definedInClass)104 Builder definedInClass(String definedInClass) { 105 mDefinedInClass = definedInClass; 106 return this; 107 } 108 sourcePosition(String sourcePosition)109 Builder sourcePosition(String sourcePosition) throws SAXException { 110 if (sourcePosition != null) { 111 int colonPos = sourcePosition.indexOf(":"); 112 if (colonPos == -1) { 113 throw new SAXException("Invalid source position: " + sourcePosition); 114 } 115 mSourceFile = sourcePosition.substring(0, colonPos); 116 try { 117 mSourceLine = Integer.parseInt(sourcePosition.substring(colonPos + 1)); 118 } catch (NumberFormatException nfe) { 119 throw new SAXException("Invalid source position: " + sourcePosition, nfe); 120 } 121 } 122 return this; 123 } 124 parseBool(String value)125 boolean parseBool(String value) { 126 if (value == null) { 127 return false; 128 } 129 boolean result = Boolean.parseBoolean(value); 130 return result; 131 } 132 disabled(String disabled)133 Builder disabled(String disabled) { 134 mDisabled = parseBool(disabled); 135 return this; 136 } 137 loggingOnly(String loggingOnly)138 Builder loggingOnly(String loggingOnly) { 139 mLoggingOnly = parseBool(loggingOnly); 140 return this; 141 } 142 enableAfterTargetSdk(String enableAfter)143 Builder enableAfterTargetSdk(String enableAfter) throws SAXException { 144 if (enableAfter == null) { 145 mEnableAfterTargetSdk = 0; 146 } else { 147 try { 148 mEnableAfterTargetSdk = Integer.parseInt(enableAfter); 149 } catch (NumberFormatException nfe) { 150 throw new SAXException("Invalid SDK version int: " + enableAfter, nfe); 151 } 152 } 153 return this; 154 } enableSinceTargetSdk(String enableSince)155 Builder enableSinceTargetSdk(String enableSince) throws SAXException { 156 if (enableSince == null) { 157 mEnableSinceTargetSdk = 0; 158 } else { 159 try { 160 mEnableSinceTargetSdk = Integer.parseInt(enableSince); 161 } catch (NumberFormatException nfe) { 162 throw new SAXException("Invalid SDK version int: " + enableSince, nfe); 163 } 164 } 165 return this; 166 } 167 } 168 169 } 170 171 private class CompatConfigXmlParser extends DefaultHandler { 172 @Override startElement(String uri, String localName, String qName, Attributes attributes)173 public void startElement(String uri, String localName, String qName, Attributes attributes) 174 throws SAXException { 175 if (qName.equals("compat-change")) { 176 mCurrentChange = new CompatChange.Builder(); 177 String idStr = attributes.getValue("id"); 178 if (idStr == null) { 179 throw new SAXException("<compat-change> element has no id"); 180 } 181 try { 182 mCurrentChange.id(Long.parseLong(idStr)); 183 } catch (NumberFormatException nfe) { 184 throw new SAXException("<compat-change> id is not a valid long", nfe); 185 } 186 mCurrentChange.name(attributes.getValue("name")) 187 .description(attributes.getValue("description")) 188 .enableAfterTargetSdk(attributes.getValue("enableAfterTargetSdk")) 189 .enableSinceTargetSdk(attributes.getValue("enableSinceTargetSdk")) 190 .disabled(attributes.getValue("disabled")) 191 .loggingOnly(attributes.getValue("loggingOnly")); 192 193 } else if (qName.equals("meta-data")) { 194 if (mCurrentChange == null) { 195 throw new SAXException("<meta-data> tag with no enclosing <compat-change>"); 196 } 197 mCurrentChange.definedInClass(attributes.getValue("definedIn")) 198 .sourcePosition(attributes.getValue("sourcePosition")); 199 } 200 } 201 202 @Override endElement(String uri, String localName, String qName)203 public void endElement(String uri, String localName, String qName) { 204 if (qName.equals("compat-change")) { 205 mChanges.add(mCurrentChange.build()); 206 mCurrentChange = null; 207 } 208 } 209 } 210 readCompatConfig(String source)211 public static CompatInfo readCompatConfig(String source) { 212 CompatInfo config = new CompatInfo(); 213 try { 214 InputStream in = new FileInputStream(new File(source)); 215 216 XMLReader xmlreader = XMLReaderFactory.createXMLReader(); 217 xmlreader.setContentHandler(config.mXmlParser); 218 xmlreader.setErrorHandler(config.mXmlParser); 219 xmlreader.parse(new InputSource(in)); 220 in.close(); 221 return config; 222 } catch (SAXException e) { 223 throw new RuntimeException("Failed to parse " + source, e); 224 } catch (IOException e) { 225 throw new RuntimeException("Failed to read " + source, e); 226 } 227 } 228 229 private final CompatConfigXmlParser mXmlParser = new CompatConfigXmlParser(); 230 private CompatChange.Builder mCurrentChange; 231 private List<CompatChange> mChanges = new ArrayList<>(); 232 getChanges()233 public List<CompatChange> getChanges() { 234 return mChanges; 235 } 236 makeHDF(Data hdf)237 public void makeHDF(Data hdf) { 238 // We construct a Comment for each compat change to re-use the default docs generation support 239 // for comments. 240 mChanges.sort(Comparator.comparing(a -> a.name)); 241 for (int i = 0; i < mChanges.size(); ++i) { 242 CompatInfo.CompatChange change = mChanges.get(i); 243 // we will get null ClassInfo here if the defining class is not in the SDK. 244 ContainerInfo definedInContainer = Converter.obtainClass(change.definedInClass); 245 if (definedInContainer == null) { 246 // This happens when the class defining the @ChangeId constant is not included in 247 // the sources that the SDK docs are generated from. Using package "android" as the 248 // container works, but means we lose the context of the original javadoc comment. 249 // This means that if the javadoc comment refers to classes imported by it's 250 // containing source file, we cannot resolve those imports here. 251 // TODO see if we could somehow plumb the import list from the original source file, 252 // via compat_config.xml, so we can resolve links properly here? 253 definedInContainer = Converter.obtainPackage("android"); 254 } 255 if (change.description == null) { 256 throw new RuntimeException("No description found for @ChangeId " + change.name); 257 } 258 Comment comment = new Comment(change.description, definedInContainer, new SourcePositionInfo( 259 change.sourceFile, change.sourceLine, 1)); 260 String path = "change." + i; 261 hdf.setValue(path + ".id", Long.toString(change.id)); 262 hdf.setValue(path + ".name", change.name); 263 if (change.enableSinceTargetSdk != 0) { 264 hdf.setValue(path + ".enableSinceTargetSdk", 265 Integer.toString(change.enableSinceTargetSdk)); 266 } 267 if (change.loggingOnly) { 268 hdf.setValue(path + ".loggingOnly", Boolean.toString(true)); 269 } 270 if (change.disabled) { 271 hdf.setValue(path + ".disabled", Boolean.toString(true)); 272 } 273 TagInfo.makeHDF(hdf, path + ".descr", comment.tags()); 274 } 275 } 276 } 277