Jaekyun Seok | 7481287 | 2017-04-18 15:22:01 +0900 | [diff] [blame] | 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.settings; |
| 18 | |
| 19 | import android.support.annotation.VisibleForTesting; |
| 20 | import android.text.TextUtils; |
| 21 | import android.util.Log; |
| 22 | import android.util.Xml; |
| 23 | |
| 24 | import org.xmlpull.v1.XmlPullParser; |
| 25 | import org.xmlpull.v1.XmlPullParserException; |
| 26 | |
| 27 | import java.io.File; |
| 28 | import java.io.FileInputStream; |
| 29 | import java.io.FileNotFoundException; |
| 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.List; |
| 38 | import java.util.Map; |
| 39 | import java.util.zip.GZIPInputStream; |
| 40 | |
| 41 | /** |
| 42 | * The utility class that generate a license html file from xml files. |
| 43 | * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py. |
| 44 | * |
| 45 | * TODO: Remove duplicate codes once backward support ends. |
| 46 | */ |
| 47 | class LicenseHtmlGeneratorFromXml { |
| 48 | private static final String TAG = "LicenseHtmlGeneratorFromXml"; |
| 49 | |
| 50 | private static final String TAG_ROOT = "licenses"; |
| 51 | private static final String TAG_FILE_NAME = "file-name"; |
| 52 | private static final String TAG_FILE_CONTENT = "file-content"; |
| 53 | private static final String ATTR_CONTENT_ID = "contentId"; |
| 54 | |
| 55 | private static final String HTML_HEAD_STRING = |
| 56 | "<html><head>\n" + |
| 57 | "<style type=\"text/css\">\n" + |
| 58 | "body { padding: 0; font-family: sans-serif; }\n" + |
| 59 | ".same-license { background-color: #eeeeee;\n" + |
| 60 | " border-top: 20px solid white;\n" + |
| 61 | " padding: 10px; }\n" + |
| 62 | ".label { font-weight: bold; }\n" + |
| 63 | ".file-list { margin-left: 1em; color: blue; }\n" + |
| 64 | "</style>\n" + |
| 65 | "</head>" + |
| 66 | "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n" + |
| 67 | "<div class=\"toc\">\n" + |
| 68 | "<ul>"; |
| 69 | |
| 70 | private static final String HTML_MIDDLE_STRING = |
| 71 | "</ul>\n" + |
| 72 | "</div><!-- table of contents -->\n" + |
| 73 | "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">"; |
| 74 | |
| 75 | private static final String HTML_REAR_STRING = |
| 76 | "</table></body></html>"; |
| 77 | |
| 78 | private final List<File> mXmlFiles; |
| 79 | |
| 80 | /* |
| 81 | * A map from a file name to a content id (MD5 sum of file content) for its license. |
| 82 | * For example, "/system/priv-app/TeleService/TeleService.apk" maps to |
| 83 | * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum |
| 84 | * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2. |
| 85 | */ |
| 86 | private final Map<String, String> mFileNameToContentIdMap = new HashMap(); |
| 87 | |
| 88 | /* |
| 89 | * A map from a content id (MD5 sum of file content) to a license file content. |
| 90 | * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of |
| 91 | * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595" |
| 92 | * is a MD5 sum of the file content. |
| 93 | */ |
| 94 | private final Map<String, String> mContentIdToFileContentMap = new HashMap(); |
| 95 | |
| 96 | static class ContentIdAndFileNames { |
| 97 | final String mContentId; |
| 98 | final List<String> mFileNameList = new ArrayList(); |
| 99 | |
| 100 | ContentIdAndFileNames(String contentId) { |
| 101 | mContentId = contentId; |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) { |
| 106 | mXmlFiles = xmlFiles; |
| 107 | } |
| 108 | |
| 109 | public static boolean generateHtml(List<File> xmlFiles, File outputFile) { |
| 110 | LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles); |
| 111 | return genertor.generateHtml(outputFile); |
| 112 | } |
| 113 | |
| 114 | private boolean generateHtml(File outputFile) { |
| 115 | for (File xmlFile : mXmlFiles) { |
| 116 | parse(xmlFile); |
| 117 | } |
| 118 | |
| 119 | if (mFileNameToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) { |
| 120 | return false; |
| 121 | } |
| 122 | |
| 123 | PrintWriter writer = null; |
| 124 | try { |
| 125 | writer = new PrintWriter(outputFile); |
| 126 | |
| 127 | generateHtml(mFileNameToContentIdMap, mContentIdToFileContentMap, writer); |
| 128 | |
| 129 | writer.flush(); |
| 130 | writer.close(); |
| 131 | return true; |
| 132 | } catch (FileNotFoundException | SecurityException e) { |
| 133 | Log.e(TAG, "Failed to generate " + outputFile, e); |
| 134 | |
| 135 | if (writer != null) { |
| 136 | writer.close(); |
| 137 | } |
| 138 | return false; |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | private void parse(File xmlFile) { |
| 143 | if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) { |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | InputStreamReader in = null; |
| 148 | try { |
| 149 | if (xmlFile.getName().endsWith(".gz")) { |
| 150 | in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile))); |
| 151 | } else { |
| 152 | in = new FileReader(xmlFile); |
| 153 | } |
| 154 | |
| 155 | parse(in, mFileNameToContentIdMap, mContentIdToFileContentMap); |
| 156 | |
| 157 | in.close(); |
| 158 | } catch (XmlPullParserException | IOException e) { |
| 159 | Log.e(TAG, "Failed to parse " + xmlFile, e); |
| 160 | if (in != null) { |
| 161 | try { |
| 162 | in.close(); |
| 163 | } catch (IOException ie) { |
| 164 | Log.w(TAG, "Failed to close " + xmlFile); |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | /* |
| 171 | * Parses an input stream and fills a map from a file name to a content id for its license |
| 172 | * and a map from a content id to a license file content. |
| 173 | * |
| 174 | * Following xml format is expected from the input stream. |
| 175 | * |
| 176 | * <licenses> |
| 177 | * <file-name contentId="content_id_of_license1">file1</file-name> |
| 178 | * <file-name contentId="content_id_of_license2">file2</file-name> |
| 179 | * ... |
| 180 | * <file-content contentId="content_id_of_license1">license1 file contents</file-content> |
| 181 | * <file-content contentId="content_id_of_license2">license2 file contents</file-content> |
| 182 | * ... |
| 183 | * </licenses> |
| 184 | */ |
| 185 | @VisibleForTesting |
| 186 | static void parse(InputStreamReader in, Map<String, String> outFileNameToContentIdMap, |
| 187 | Map<String, String> outContentIdToFileContentMap) |
| 188 | throws XmlPullParserException, IOException { |
| 189 | Map<String, String> fileNameToContentIdMap = new HashMap<String, String>(); |
| 190 | Map<String, String> contentIdToFileContentMap = new HashMap<String, String>(); |
| 191 | |
| 192 | XmlPullParser parser = Xml.newPullParser(); |
| 193 | parser.setInput(in); |
| 194 | parser.nextTag(); |
| 195 | |
| 196 | parser.require(XmlPullParser.START_TAG, "", TAG_ROOT); |
| 197 | |
| 198 | int state = parser.getEventType(); |
| 199 | while (state != XmlPullParser.END_DOCUMENT) { |
| 200 | if (state == XmlPullParser.START_TAG) { |
| 201 | if (TAG_FILE_NAME.equals(parser.getName())) { |
| 202 | String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID); |
| 203 | if (!TextUtils.isEmpty(contentId)) { |
| 204 | String fileName = readText(parser).trim(); |
| 205 | if (!TextUtils.isEmpty(fileName)) { |
| 206 | fileNameToContentIdMap.put(fileName, contentId); |
| 207 | } |
| 208 | } |
| 209 | } else if (TAG_FILE_CONTENT.equals(parser.getName())) { |
| 210 | String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID); |
| 211 | if (!TextUtils.isEmpty(contentId) && |
| 212 | !outContentIdToFileContentMap.containsKey(contentId) && |
| 213 | !contentIdToFileContentMap.containsKey(contentId)) { |
| 214 | String fileContent = readText(parser); |
| 215 | if (!TextUtils.isEmpty(fileContent)) { |
| 216 | contentIdToFileContentMap.put(contentId, fileContent); |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | state = parser.next(); |
| 223 | } |
| 224 | outFileNameToContentIdMap.putAll(fileNameToContentIdMap); |
| 225 | outContentIdToFileContentMap.putAll(contentIdToFileContentMap); |
| 226 | } |
| 227 | |
| 228 | private static String readText(XmlPullParser parser) |
| 229 | throws IOException, XmlPullParserException { |
| 230 | StringBuffer result = new StringBuffer(); |
| 231 | int state = parser.next(); |
| 232 | while (state == XmlPullParser.TEXT) { |
| 233 | result.append(parser.getText()); |
| 234 | state = parser.next(); |
| 235 | } |
| 236 | return result.toString(); |
| 237 | } |
| 238 | |
| 239 | @VisibleForTesting |
| 240 | static void generateHtml(Map<String, String> fileNameToContentIdMap, |
| 241 | Map<String, String> contentIdToFileContentMap, PrintWriter writer) { |
| 242 | List<String> fileNameList = new ArrayList(); |
| 243 | fileNameList.addAll(fileNameToContentIdMap.keySet()); |
| 244 | Collections.sort(fileNameList); |
| 245 | |
| 246 | writer.println(HTML_HEAD_STRING); |
| 247 | |
| 248 | int count = 0; |
| 249 | Map<String, Integer> contentIdToOrderMap = new HashMap(); |
| 250 | List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList(); |
| 251 | |
| 252 | // Prints all the file list with a link to its license file content. |
| 253 | for (String fileName : fileNameList) { |
| 254 | String contentId = fileNameToContentIdMap.get(fileName); |
| 255 | // Assigns an id to a newly referred license file content. |
| 256 | if (!contentIdToOrderMap.containsKey(contentId)) { |
| 257 | contentIdToOrderMap.put(contentId, count); |
| 258 | |
| 259 | // An index in contentIdAndFileNamesList is the order of each element. |
| 260 | contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId)); |
| 261 | count++; |
| 262 | } |
| 263 | |
| 264 | int id = contentIdToOrderMap.get(contentId); |
| 265 | contentIdAndFileNamesList.get(id).mFileNameList.add(fileName); |
| 266 | writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName); |
| 267 | } |
| 268 | |
| 269 | writer.println(HTML_MIDDLE_STRING); |
| 270 | |
| 271 | count = 0; |
| 272 | // Prints all contents of the license files in order of id. |
| 273 | for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) { |
| 274 | writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", count); |
| 275 | writer.println("<div class=\"label\">Notices for file(s):</div>"); |
| 276 | writer.println("<div class=\"file-list\">"); |
| 277 | for (String fileName : contentIdAndFileNames.mFileNameList) { |
| 278 | writer.format("%s <br/>\n", fileName); |
| 279 | } |
| 280 | writer.println("</div><!-- file-list -->"); |
| 281 | writer.println("<pre class=\"license-text\">"); |
| 282 | writer.println(contentIdToFileContentMap.get( |
| 283 | contentIdAndFileNames.mContentId)); |
| 284 | writer.println("</pre><!-- license-text -->"); |
| 285 | writer.println("</td></tr><!-- same-license -->"); |
| 286 | |
| 287 | count++; |
| 288 | } |
| 289 | |
| 290 | writer.println(HTML_REAR_STRING); |
| 291 | } |
| 292 | } |