Home | History | Annotate | Download | only in manifmerger
      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