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