1 /* 2 * Copyright (C) 2017 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.android.settingslib.license; 18 19 import android.text.TextUtils; 20 import android.util.Log; 21 import android.util.Xml; 22 23 import androidx.annotation.VisibleForTesting; 24 25 import org.xmlpull.v1.XmlPullParser; 26 import org.xmlpull.v1.XmlPullParserException; 27 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.FileReader; 31 import java.io.IOException; 32 import java.io.InputStreamReader; 33 import java.io.PrintWriter; 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.HashMap; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Set; 41 import java.util.SortedMap; 42 import java.util.TreeMap; 43 import java.util.zip.GZIPInputStream; 44 45 /** 46 * The utility class that generate a license html file from xml files. 47 * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py. 48 * 49 * TODO: Remove duplicate codes once backward support ends. 50 */ 51 class LicenseHtmlGeneratorFromXml { 52 private static final String TAG = "LicenseGeneratorFromXml"; 53 54 private static final String TAG_ROOT = "licenses"; 55 private static final String TAG_FILE_NAME = "file-name"; 56 private static final String TAG_FILE_CONTENT = "file-content"; 57 private static final String ATTR_CONTENT_ID = "contentId"; 58 private static final String ATTR_LIBRARY_NAME = "lib"; 59 60 private static final String HTML_HEAD_STRING = 61 "<html><head>\n" 62 + "<style type=\"text/css\">\n" 63 + "body { padding: 0; font-family: sans-serif; }\n" 64 + ".same-license { background-color: #eeeeee;\n" 65 + " border-top: 20px solid white;\n" 66 + " padding: 10px; }\n" 67 + ".label { font-weight: bold; }\n" 68 + ".file-list { margin-left: 1em; color: blue; }\n" 69 + "</style>\n" 70 + "</head>" 71 + "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n" 72 + "<div class=\"toc\">"; 73 private static final String LIBRARY_HEAD_STRING = 74 "<strong>Libraries</strong>\n<ul class=\"libraries\">"; 75 private static final String LIBRARY_TAIL_STRING = "</ul>\n<strong>Files</strong>"; 76 77 private static final String FILES_HEAD_STRING = "<ul class=\"files\">"; 78 private static final String FILES_TAIL_STRING = "</ul>\n</div><!-- table of contents -->"; 79 80 private static final String CONTENT_HEAD_STRING = 81 "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">"; 82 private static final String CONTENT_TAIL_STRING = "</table>"; 83 84 private static final String IMAGES_HEAD_STRING = 85 "<div class=\"images-list\"><strong>Images</strong>\n<ul class=\"images\">"; 86 private static final String IMAGES_TAIL_STRING = "</ul></div>\n"; 87 88 private static final String PATH_COUNTS_HEAD_STRING = 89 "<div class=\"path-counts\"><table>\n <tr><th>Path prefix</th><th>Count</th></tr>\n"; 90 private static final String PATH_COUNTS_TAIL_STRING = "</table></div>\n"; 91 92 private static final String HTML_TAIL_STRING = 93 "</body></html>"; 94 95 private final List<File> mXmlFiles; 96 97 /* 98 * A map from a file name to a library name (may be empty) to a content id (MD5 sum of file 99 * content) for its license. 100 * For example, "/system/priv-app/TeleService/TeleService.apk" maps to "service/Telephony" to 101 * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum 102 * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2. 103 */ 104 private final Map<String, Map<String, Set<String>>> mFileNameToLibraryToContentIdMap = 105 new HashMap(); 106 107 /* 108 * A map from a content id (MD5 sum of file content) to a license file content. 109 * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of 110 * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595" 111 * is a MD5 sum of the file content. 112 */ 113 private final Map<String, String> mContentIdToFileContentMap = new HashMap(); 114 115 static class ContentIdAndFileNames { 116 final String mContentId; 117 final Map<String, List<String>> mLibraryToFileNameMap = new TreeMap(); 118 ContentIdAndFileNames(String contentId)119 ContentIdAndFileNames(String contentId) { 120 mContentId = contentId; 121 } 122 } 123 LicenseHtmlGeneratorFromXml(List<File> xmlFiles)124 private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) { 125 mXmlFiles = xmlFiles; 126 } 127 generateHtml(List<File> xmlFiles, File outputFile, String noticeHeader)128 public static boolean generateHtml(List<File> xmlFiles, File outputFile, 129 String noticeHeader) { 130 LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles); 131 return genertor.generateHtml(outputFile, noticeHeader); 132 } 133 generateHtml(File outputFile, String noticeHeader)134 private boolean generateHtml(File outputFile, String noticeHeader) { 135 for (File xmlFile : mXmlFiles) { 136 parse(xmlFile); 137 } 138 139 if (mFileNameToLibraryToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) { 140 return false; 141 } 142 143 PrintWriter writer = null; 144 try { 145 writer = new PrintWriter(outputFile); 146 147 generateHtml(mXmlFiles, mFileNameToLibraryToContentIdMap, mContentIdToFileContentMap, 148 writer, noticeHeader); 149 150 writer.flush(); 151 writer.close(); 152 return true; 153 } catch (IOException | SecurityException e) { 154 Log.e(TAG, "Failed to generate " + outputFile, e); 155 156 if (writer != null) { 157 writer.close(); 158 } 159 return false; 160 } 161 } 162 parse(File xmlFile)163 private void parse(File xmlFile) { 164 if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) { 165 return; 166 } 167 168 InputStreamReader in = null; 169 try { 170 if (xmlFile.getName().endsWith(".gz")) { 171 in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile))); 172 } else { 173 in = new FileReader(xmlFile); 174 } 175 176 parse(in, mFileNameToLibraryToContentIdMap, mContentIdToFileContentMap); 177 178 in.close(); 179 } catch (XmlPullParserException | IOException e) { 180 Log.e(TAG, "Failed to parse " + xmlFile, e); 181 if (in != null) { 182 try { 183 in.close(); 184 } catch (IOException ie) { 185 Log.w(TAG, "Failed to close " + xmlFile); 186 } 187 } 188 } 189 } 190 191 /* 192 * Parses an input stream and fills a map from a file name to a content id for its license 193 * and a map from a content id to a license file content. 194 * 195 * Following xml format is expected from the input stream. 196 * 197 * <licenses> 198 * <file-name contentId="content_id_of_license1">file1</file-name> 199 * <file-name contentId="content_id_of_license2" lib="name of library">file2</file-name> 200 * <file-name contentId="content_id_of_license2" lib="another library">file2</file-name> 201 * ... 202 * <file-content contentId="content_id_of_license1">license1 file contents</file-content> 203 * <file-content contentId="content_id_of_license2">license2 file contents</file-content> 204 * ... 205 * </licenses> 206 */ 207 @VisibleForTesting parse(InputStreamReader in, Map<String, Map<String, Set<String>>> outFileNameToLibraryToContentIdMap, Map<String, String> outContentIdToFileContentMap)208 static void parse(InputStreamReader in, 209 Map<String, Map<String, Set<String>>> outFileNameToLibraryToContentIdMap, 210 Map<String, String> outContentIdToFileContentMap) 211 throws XmlPullParserException, IOException { 212 Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap = 213 new HashMap<String, Map<String, Set<String>>>(); 214 Map<String, String> contentIdToFileContentMap = new HashMap<String, String>(); 215 216 XmlPullParser parser = Xml.newPullParser(); 217 parser.setInput(in); 218 parser.nextTag(); 219 220 parser.require(XmlPullParser.START_TAG, "", TAG_ROOT); 221 222 int state = parser.getEventType(); 223 while (state != XmlPullParser.END_DOCUMENT) { 224 if (state == XmlPullParser.START_TAG) { 225 if (TAG_FILE_NAME.equals(parser.getName())) { 226 String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID); 227 String libraryName = parser.getAttributeValue("", ATTR_LIBRARY_NAME); 228 if (!TextUtils.isEmpty(contentId)) { 229 String fileName = readText(parser).trim(); 230 if (!TextUtils.isEmpty(fileName)) { 231 Map<String, Set<String>> libs = 232 fileNameToLibraryToContentIdMap.computeIfAbsent( 233 fileName, k -> new HashMap<>()); 234 Set<String> contentIds = libs.computeIfAbsent( 235 libraryName, k -> new HashSet<>()); 236 contentIds.add(contentId); 237 } 238 } 239 } else if (TAG_FILE_CONTENT.equals(parser.getName())) { 240 String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID); 241 if (!TextUtils.isEmpty(contentId) 242 && !outContentIdToFileContentMap.containsKey(contentId) 243 && !contentIdToFileContentMap.containsKey(contentId)) { 244 String fileContent = readText(parser); 245 if (!TextUtils.isEmpty(fileContent)) { 246 contentIdToFileContentMap.put(contentId, fileContent); 247 } 248 } 249 } 250 } 251 252 state = parser.next(); 253 } 254 for (Map.Entry<String, Map<String, Set<String>>> mapEntry : 255 fileNameToLibraryToContentIdMap.entrySet()) { 256 outFileNameToLibraryToContentIdMap.merge( 257 mapEntry.getKey(), mapEntry.getValue(), (m1, m2) -> { 258 for (Map.Entry<String, Set<String>> entry : m2.entrySet()) { 259 m1.merge(entry.getKey(), entry.getValue(), (s1, s2) -> { 260 s1.addAll(s2); 261 return s1; 262 }); 263 } 264 return m1; 265 }); 266 } 267 outContentIdToFileContentMap.putAll(contentIdToFileContentMap); 268 } 269 readText(XmlPullParser parser)270 private static String readText(XmlPullParser parser) 271 throws IOException, XmlPullParserException { 272 StringBuffer result = new StringBuffer(); 273 int state = parser.next(); 274 while (state == XmlPullParser.TEXT) { 275 result.append(parser.getText()); 276 state = parser.next(); 277 } 278 return result.toString(); 279 } 280 pathPrefix(String path)281 private static String pathPrefix(String path) { 282 String prefix = path; 283 while (prefix.length() > 0 && prefix.substring(0, 1).equals("/")) { 284 prefix = prefix.substring(1); 285 } 286 int idx = prefix.indexOf("/"); 287 if (idx > 0) { 288 prefix = prefix.substring(0, idx); 289 } 290 return prefix; 291 } 292 293 @VisibleForTesting generateHtml(List<File> xmlFiles, Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap, Map<String, String> contentIdToFileContentMap, PrintWriter writer, String noticeHeader)294 static void generateHtml(List<File> xmlFiles, 295 Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap, 296 Map<String, String> contentIdToFileContentMap, PrintWriter writer, 297 String noticeHeader) throws IOException { 298 List<String> fileNameList = new ArrayList(); 299 fileNameList.addAll(fileNameToLibraryToContentIdMap.keySet()); 300 Collections.sort(fileNameList); 301 302 SortedMap<String, Integer> prefixToCount = new TreeMap(); 303 for (String f : fileNameList) { 304 String prefix = pathPrefix(f); 305 prefixToCount.merge(prefix, 1, Integer::sum); 306 } 307 308 SortedMap<String, Set<String>> libraryToContentIdMap = new TreeMap(); 309 for (Map<String, Set<String>> libraryToContentValue : 310 fileNameToLibraryToContentIdMap.values()) { 311 for (Map.Entry<String, Set<String>> entry : libraryToContentValue.entrySet()) { 312 if (TextUtils.isEmpty(entry.getKey())) { 313 continue; 314 } 315 libraryToContentIdMap.merge( 316 entry.getKey(), entry.getValue(), (s1, s2) -> { 317 s1.addAll(s2); 318 return s1; 319 }); 320 } 321 } 322 323 writer.println(HTML_HEAD_STRING); 324 325 if (!TextUtils.isEmpty(noticeHeader)) { 326 writer.println(noticeHeader); 327 } 328 329 int count = 0; 330 Map<String, Integer> contentIdToOrderMap = new HashMap(); 331 List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList(); 332 333 if (!libraryToContentIdMap.isEmpty()) { 334 writer.println(LIBRARY_HEAD_STRING); 335 for (Map.Entry<String, Set<String>> entry: libraryToContentIdMap.entrySet()) { 336 String libraryName = entry.getKey(); 337 for (String contentId : entry.getValue()) { 338 // Assigns an id to a newly referred license file content. 339 if (!contentIdToOrderMap.containsKey(contentId)) { 340 contentIdToOrderMap.put(contentId, count); 341 342 // An index in contentIdAndFileNamesList is the order of each element. 343 contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId)); 344 count++; 345 } 346 int id = contentIdToOrderMap.get(contentId); 347 writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, libraryName); 348 } 349 } 350 writer.println(LIBRARY_TAIL_STRING); 351 } 352 353 if (!fileNameList.isEmpty()) { 354 writer.println(FILES_HEAD_STRING); 355 // Prints all the file list with a link to its license file content. 356 for (String fileName : fileNameList) { 357 for (Map.Entry<String, Set<String>> libToContentId : 358 fileNameToLibraryToContentIdMap.get(fileName).entrySet()) { 359 String libraryName = libToContentId.getKey(); 360 if (libraryName == null) { 361 libraryName = ""; 362 } 363 for (String contentId : libToContentId.getValue()) { 364 // Assigns an id to a newly referred license file content. 365 if (!contentIdToOrderMap.containsKey(contentId)) { 366 contentIdToOrderMap.put(contentId, count); 367 368 // An index in contentIdAndFileNamesList is the order of each element. 369 contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId)); 370 count++; 371 } 372 373 int id = contentIdToOrderMap.get(contentId); 374 ContentIdAndFileNames elem = contentIdAndFileNamesList.get(id); 375 List<String> files = elem.mLibraryToFileNameMap.computeIfAbsent( 376 libraryName, k -> new ArrayList()); 377 files.add(fileName); 378 if (TextUtils.isEmpty(libraryName)) { 379 writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName); 380 } else { 381 writer.format("<li><a href=\"#id%d\">%s - %s</a></li>\n", 382 id, fileName, libraryName); 383 } 384 } 385 } 386 } 387 writer.println(FILES_TAIL_STRING); 388 } 389 390 if (!contentIdAndFileNamesList.isEmpty()) { 391 writer.println(CONTENT_HEAD_STRING); 392 // Prints all contents of the license files in order of id. 393 for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) { 394 // Assigns an id to a newly referred license file content (should never happen here) 395 if (!contentIdToOrderMap.containsKey(contentIdAndFileNames.mContentId)) { 396 contentIdToOrderMap.put(contentIdAndFileNames.mContentId, count); 397 count++; 398 } 399 int id = contentIdToOrderMap.get(contentIdAndFileNames.mContentId); 400 writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", id); 401 for (Map.Entry<String, List<String>> libraryFiles : 402 contentIdAndFileNames.mLibraryToFileNameMap.entrySet()) { 403 String libraryName = libraryFiles.getKey(); 404 if (TextUtils.isEmpty(libraryName)) { 405 writer.println("<div class=\"label\">Notices for file(s):</div>"); 406 } else { 407 writer.format("<div class=\"label\"><strong>%s</strong> used by:</div>\n", 408 libraryName); 409 } 410 writer.println("<div class=\"file-list\">"); 411 for (String fileName : libraryFiles.getValue()) { 412 writer.format("%s <br/>\n", fileName); 413 } 414 writer.println("</div><!-- file-list -->"); 415 } 416 writer.println("<pre class=\"license-text\">"); 417 writer.println(contentIdToFileContentMap.get( 418 contentIdAndFileNames.mContentId)); 419 writer.println("</pre><!-- license-text -->"); 420 writer.println("</td></tr><!-- same-license -->"); 421 } 422 writer.println(CONTENT_TAIL_STRING); 423 } 424 425 if (!xmlFiles.isEmpty()) { 426 writer.println(IMAGES_HEAD_STRING); 427 for (File file : xmlFiles) { 428 writer.format(" <li>%s</li>\n", pathPrefix(file.getCanonicalPath())); 429 } 430 writer.println(IMAGES_TAIL_STRING); 431 } 432 433 if (!prefixToCount.isEmpty()) { 434 writer.println(PATH_COUNTS_HEAD_STRING); 435 for (Map.Entry<String, Integer> entry : prefixToCount.entrySet()) { 436 writer.format(" <tr><td>%s</td><td>%d</td></tr>\n", 437 entry.getKey(), entry.getValue()); 438 } 439 writer.println(PATH_COUNTS_TAIL_STRING); 440 } 441 442 writer.println(HTML_TAIL_STRING); 443 } 444 } 445