1 /* 2 * Copyright (C) 2011 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.manifmerger; 18 19 import com.android.annotations.NonNull; 20 import com.android.annotations.Nullable; 21 import com.android.sdklib.ISdkLog; 22 23 import org.w3c.dom.Attr; 24 import org.w3c.dom.Document; 25 import org.w3c.dom.NamedNodeMap; 26 import org.w3c.dom.Node; 27 import org.xml.sax.ErrorHandler; 28 import org.xml.sax.InputSource; 29 import org.xml.sax.SAXParseException; 30 31 import java.io.File; 32 import java.io.FileNotFoundException; 33 import java.io.FileReader; 34 import java.io.StringReader; 35 import java.io.StringWriter; 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.Comparator; 39 import java.util.List; 40 41 import javax.xml.parsers.DocumentBuilder; 42 import javax.xml.parsers.DocumentBuilderFactory; 43 import javax.xml.transform.OutputKeys; 44 import javax.xml.transform.Transformer; 45 import javax.xml.transform.TransformerException; 46 import javax.xml.transform.TransformerFactory; 47 import javax.xml.transform.dom.DOMSource; 48 import javax.xml.transform.stream.StreamResult; 49 50 /** 51 * A few XML handling utilities. 52 */ 53 class XmlUtils { 54 55 private static final String DATA_ORIGIN_FILE = "origin_file"; //$NON-NLS-1$ 56 private static final String DATA_LINE_NUMBER = "line#"; //$NON-NLS-1$ 57 58 /** 59 * Parses the given XML file as a DOM document. 60 * The parser does not validate the DTD nor any kind of schema. 61 * It is namespace aware. 62 * <p/> 63 * This adds a user tag with the original {@link File} to the returned document. 64 * You can retrieve this file later by using {@link #extractXmlFilename(Node)}. 65 * 66 * @param xmlFile The XML {@link File} to parse. Must not be null. 67 * @param log An {@link ISdkLog} for reporting errors. Must not be null. 68 * @return A new DOM {@link Document}, or null. 69 */ 70 @Nullable 71 static Document parseDocument(@NonNull final File xmlFile, @NonNull final ISdkLog log) { 72 try { 73 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 74 InputSource is = new InputSource(new FileReader(xmlFile)); 75 factory.setNamespaceAware(true); 76 factory.setValidating(false); 77 DocumentBuilder builder = factory.newDocumentBuilder(); 78 79 // We don't want the default handler which prints errors to stderr. 80 builder.setErrorHandler(new ErrorHandler() { 81 @Override 82 public void warning(SAXParseException e) { 83 log.printf("Warning when parsing %s: %s", xmlFile.getName(), e.toString()); 84 } 85 @Override 86 public void fatalError(SAXParseException e) { 87 log.printf("Fatal error when parsing %s: %s", xmlFile.getName(), e.toString()); 88 } 89 @Override 90 public void error(SAXParseException e) { 91 log.printf("Error when parsing %s: %s", xmlFile.getName(), e.toString()); 92 } 93 }); 94 95 Document doc = builder.parse(is); 96 doc.setUserData(DATA_ORIGIN_FILE, xmlFile, null /*handler*/); 97 findLineNumbers(doc, 1); 98 99 return doc; 100 101 } catch (FileNotFoundException e) { 102 log.error(null, "XML file not found: %s", xmlFile.getName()); 103 104 } catch (Exception e) { 105 log.error(e, "Failed to parse XML file: %s", xmlFile.getName()); 106 } 107 108 return null; 109 } 110 111 /** 112 * Parses the given XML string as a DOM document. 113 * The parser does not validate the DTD nor any kind of schema. 114 * It is namespace aware. 115 * 116 * @param xml The XML string to parse. Must not be null. 117 * @param log An {@link ISdkLog} for reporting errors. Must not be null. 118 * @return A new DOM {@link Document}, or null. 119 */ 120 @Nullable 121 static Document parseDocument(@NonNull String xml, @NonNull ISdkLog log) { 122 try { 123 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 124 InputSource is = new InputSource(new StringReader(xml)); 125 factory.setNamespaceAware(true); 126 factory.setValidating(false); 127 DocumentBuilder builder = factory.newDocumentBuilder(); 128 Document doc = builder.parse(is); 129 findLineNumbers(doc, 1); 130 return doc; 131 } catch (Exception e) { 132 log.error(e, "Failed to parse XML string"); 133 } 134 135 return null; 136 } 137 138 /** 139 * Extracts the origin {@link File} that {@link #parseDocument(File, ISdkLog)} 140 * added to the XML document. 141 * 142 * @param xmlNode Any node from a document returned by {@link #parseDocument(File, ISdkLog)}. 143 * @return The {@link File} object used to create the document or null. 144 */ 145 @Nullable 146 static File extractXmlFilename(@Nullable Node xmlNode) { 147 if (xmlNode != null && xmlNode.getNodeType() != Node.DOCUMENT_NODE) { 148 xmlNode = xmlNode.getOwnerDocument(); 149 } 150 if (xmlNode != null) { 151 Object data = xmlNode.getUserData(DATA_ORIGIN_FILE); 152 if (data instanceof File) { 153 return (File) data; 154 } 155 } 156 157 return null; 158 } 159 160 /** 161 * This is a CRUDE INEXACT HACK to decorate the DOM with some kind of line number 162 * information for elements. It's inexact because by the time we get the DOM we 163 * already have lost all the information about whitespace between attributes. 164 * <p/> 165 * Also we don't even try to deal with \n vs \r vs \r\n insanity. This only counts 166 * the \n occuring in text nodes to determine line advances, which is clearly flawed. 167 * <p/> 168 * However it's good enough for testing, and we'll replace it by a PositionXmlParser 169 * once it's moved into com.android.util. 170 */ 171 private static int findLineNumbers(Node node, int line) { 172 for (; node != null; node = node.getNextSibling()) { 173 node.setUserData(DATA_LINE_NUMBER, Integer.valueOf(line), null /*handler*/); 174 175 if (node.getNodeType() == Node.TEXT_NODE) { 176 String text = node.getNodeValue(); 177 if (text.length() > 0) { 178 for (int pos = 0; (pos = text.indexOf('\n', pos)) != -1; pos++) { 179 ++line; 180 } 181 } 182 } 183 184 Node child = node.getFirstChild(); 185 if (child != null) { 186 line = findLineNumbers(child, line); 187 } 188 } 189 return line; 190 } 191 192 /** 193 * Extracts the line number that {@link #findLineNumbers} added to the XML nodes. 194 * 195 * @param xmlNode Any node from a document returned by {@link #parseDocument(File, ISdkLog)}. 196 * @return The line number if found or 0. 197 */ 198 @Nullable 199 static int extractLineNumber(@Nullable Node xmlNode) { 200 if (xmlNode != null) { 201 Object data = xmlNode.getUserData(DATA_LINE_NUMBER); 202 if (data instanceof Integer) { 203 return ((Integer) data).intValue(); 204 } 205 } 206 207 return 0; 208 } 209 210 /** 211 * Find the prefix for the given NS_URI in the document. 212 * 213 * @param doc The document root. 214 * @param nsUri The Namespace URI to look for. 215 * @return The namespace prefix if found or null. 216 */ 217 static String lookupNsPrefix(Document doc, String nsUri) { 218 // Note: if this is not available, there's an alternate implementation at 219 // com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.lookupNamespacePrefix(Node, String) 220 return doc.lookupPrefix(nsUri); 221 } 222 223 /** 224 * Outputs the given XML {@link Document} to the file {@code outFile}. 225 * 226 * TODO right now reformats the document. Needs to output as-is, respecting white-space. 227 * 228 * @param doc The document to output. Must not be null. 229 * @param outFile The {@link File} where to write the document. 230 * @param log A log in case of error. 231 * @return True if the file was written, false in case of error. 232 */ 233 static boolean printXmlFile( 234 @NonNull Document doc, 235 @NonNull File outFile, 236 @NonNull ISdkLog log) { 237 // Quick thing based on comments from http://stackoverflow.com/questions/139076 238 try { 239 Transformer tf = TransformerFactory.newInstance().newTransformer(); 240 tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ 241 tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$ 242 tf.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ 243 tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", //$NON-NLS-1$ 244 "4"); //$NON-NLS-1$ 245 tf.transform(new DOMSource(doc), new StreamResult(outFile)); 246 return true; 247 } catch (TransformerException e) { 248 log.error(e, "Failed to write XML file: %s", outFile); 249 return false; 250 } 251 } 252 253 /** 254 * Outputs the given XML {@link Document} as a string. 255 * 256 * TODO right now reformats the document. Needs to output as-is, respecting white-space. 257 * 258 * @param doc The document to output. Must not be null. 259 * @param log A log in case of error. 260 * @return A string representation of the XML. Null in case of error. 261 */ 262 static String printXmlString( 263 @NonNull Document doc, 264 @NonNull ISdkLog log) { 265 try { 266 Transformer tf = TransformerFactory.newInstance().newTransformer(); 267 tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$ 268 tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$ 269 tf.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ 270 tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", //$NON-NLS-1$ 271 "4"); //$NON-NLS-1$ 272 StringWriter sw = new StringWriter(); 273 tf.transform(new DOMSource(doc), new StreamResult(sw)); 274 return sw.toString(); 275 } catch (TransformerException e) { 276 log.error(e, "Failed to write XML file"); 277 return null; 278 } 279 } 280 281 /** 282 * Dumps the structure of the DOM to a simple text string. 283 * 284 * @param node The first node to dump (recursively). Can be null. 285 * @param nextSiblings If true, will also dump the following siblings. 286 * If false, it will just process the given node. 287 * @return A string representation of the Node structure, useful for debugging. 288 */ 289 @NonNull 290 static String dump(@Nullable Node node, boolean nextSiblings) { 291 return dump(node, 0 /*offset*/, nextSiblings, true /*deep*/, null /*keyAttr*/); 292 } 293 294 295 /** 296 * Dumps the structure of the DOM to a simple text string. 297 * Each line is terminated with a \n separator. 298 * 299 * @param node The first node to dump. Can be null. 300 * @param offsetIndex The offset to add at the begining of each line. Each offset is 301 * converted into 2 space characters. 302 * @param nextSiblings If true, will also dump the following siblings. 303 * If false, it will just process the given node. 304 * @param deep If true, this will recurse into children. 305 * @param keyAttr An optional attribute *local* name to insert when writing an element. 306 * For example when writing an Activity, it helps to always insert "name" attribute. 307 * @return A string representation of the Node structure, useful for debugging. 308 */ 309 @NonNull 310 static String dump( 311 @Nullable Node node, 312 int offsetIndex, 313 boolean nextSiblings, 314 boolean deep, 315 @Nullable String keyAttr) { 316 StringBuilder sb = new StringBuilder(); 317 318 String offset = ""; //$NON-NLS-1$ 319 for (int i = 0; i < offsetIndex; i++) { 320 offset += " "; //$NON-NLS-1$ 321 } 322 323 if (node == null) { 324 sb.append(offset).append("(end reached)\n"); 325 326 } else { 327 for (; node != null; node = node.getNextSibling()) { 328 String type = null; 329 short t = node.getNodeType(); 330 switch(t) { 331 case Node.ELEMENT_NODE: 332 String attr = ""; 333 if (keyAttr != null) { 334 NamedNodeMap attrs = node.getAttributes(); 335 if (attrs != null) { 336 for (int i = 0; i < attrs.getLength(); i++) { 337 Node a = attrs.item(i); 338 if (a != null && keyAttr.equals(a.getLocalName())) { 339 attr = String.format(" %1$s=%2$s", 340 a.getNodeName(), a.getNodeValue()); 341 break; 342 } 343 } 344 } 345 } 346 sb.append(String.format("%1$s<%2$s%3$s>\n", 347 offset, node.getNodeName(), attr)); 348 break; 349 case Node.COMMENT_NODE: 350 sb.append(String.format("%1$s<!-- %2$s -->\n", 351 offset, node.getNodeValue())); 352 break; 353 case Node.TEXT_NODE: 354 String txt = node.getNodeValue().trim(); 355 if (txt.length() == 0) { 356 // Keep this for debugging. TODO make it a flag 357 // to dump whitespace on debugging. Otherwise ignore it. 358 // txt = "[whitespace]"; 359 break; 360 } 361 sb.append(String.format("%1$s%2$s\n", offset, txt)); 362 break; 363 case Node.ATTRIBUTE_NODE: 364 sb.append(String.format("%1$s @%2$s = %3$s\n", 365 offset, node.getNodeName(), node.getNodeValue())); 366 break; 367 case Node.CDATA_SECTION_NODE: 368 type = "cdata"; //$NON-NLS-1$ 369 break; 370 case Node.DOCUMENT_NODE: 371 type = "document"; //$NON-NLS-1$ 372 break; 373 case Node.PROCESSING_INSTRUCTION_NODE: 374 type = "PI"; //$NON-NLS-1$ 375 break; 376 default: 377 type = Integer.toString(t); 378 } 379 380 if (type != null) { 381 sb.append(String.format("%1$s[%2$s] <%3$s> %4$s\n", 382 offset, type, node.getNodeName(), node.getNodeValue())); 383 } 384 385 if (deep) { 386 List<Attr> attrs = sortedAttributeList(node.getAttributes()); 387 for (Attr attr : attrs) { 388 sb.append(String.format("%1$s @%2$s = %3$s\n", 389 offset, attr.getNodeName(), attr.getNodeValue())); 390 } 391 392 Node child = node.getFirstChild(); 393 if (child != null) { 394 sb.append(dump(child, offsetIndex+1, true, true, keyAttr)); 395 } 396 } 397 398 if (!nextSiblings) { 399 break; 400 } 401 } 402 } 403 return sb.toString(); 404 } 405 406 /** 407 * Returns a sorted list of attributes. 408 * The list is never null and does not contain null items. 409 * 410 * @param attrMap A Node map as returned by {@link Node#getAttributes()}. 411 * Can be null, in which case an empty list is returned. 412 * @return A non-null, possible empty, list of all nodes that are actual {@link Attr}, 413 * sorted by increasing attribute name. 414 */ 415 @NonNull 416 static List<Attr> sortedAttributeList(@Nullable NamedNodeMap attrMap) { 417 List<Attr> list = new ArrayList<Attr>(); 418 419 if (attrMap != null) { 420 for (int i = 0; i < attrMap.getLength(); i++) { 421 Node attr = attrMap.item(i); 422 if (attr instanceof Attr) { 423 list.add((Attr) attr); 424 } 425 } 426 } 427 428 if (list.size() > 1) { 429 // Sort it by attribute name 430 Collections.sort(list, getAttrComparator()); 431 } 432 433 return list; 434 } 435 436 /** 437 * Returns a comparator for {@link Attr}, alphabetically sorted by name. 438 * The "name" attribute is special and always sorted to the front. 439 */ 440 @NonNull 441 static Comparator<? super Attr> getAttrComparator() { 442 return new Comparator<Attr>() { 443 @Override 444 public int compare(Attr a1, Attr a2) { 445 String s1 = a1 == null ? "" : a1.getNodeName(); //$NON-NLS-1$ 446 String s2 = a2 == null ? "" : a2.getNodeValue(); //$NON-NLS-1$ 447 448 int prio1 = s1.equals("name") ? 0 : 1; //$NON-NLS-1$ 449 int prio2 = s2.equals("name") ? 0 : 1; //$NON-NLS-1$ 450 if (prio1 == 0 || prio2 == 0) { 451 return prio1 - prio2; 452 } 453 454 return s1.compareTo(s2); 455 } 456 }; 457 } 458 } 459