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 import com.android.sdklib.SdkConstants;
     23 import com.android.sdklib.xml.AndroidXPathFactory;
     24 
     25 import org.w3c.dom.Attr;
     26 import org.w3c.dom.Document;
     27 import org.w3c.dom.Element;
     28 import org.w3c.dom.NamedNodeMap;
     29 import org.w3c.dom.Node;
     30 import org.w3c.dom.NodeList;
     31 
     32 import java.io.File;
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 import java.util.Map;
     36 import java.util.TreeMap;
     37 import java.util.concurrent.atomic.AtomicBoolean;
     38 import java.util.concurrent.atomic.AtomicInteger;
     39 
     40 import javax.xml.xpath.XPath;
     41 import javax.xml.xpath.XPathConstants;
     42 import javax.xml.xpath.XPathExpressionException;
     43 
     44 /**
     45  * Merges a library manifest into a main application manifest.
     46  * <p/>
     47  * To use, create with {@link ManifestMerger#ManifestMerger(ISdkLog)} then
     48  * call {@link ManifestMerger#process(File, File, File[])}.
     49  * <p/>
     50  * <pre> Merge operations:
     51  * - root manifest: attributes ignored, warn if defined.
     52  * - application:
     53  *      {@code @attributes}: ignored in libs
     54  *      C- activity / activity-alias / service / receiver / provider
     55  *          => Merge as-is. Error if exists in the destination (same {@code @name})
     56  *             unless the definitions are exactly the same.
     57  *             New elements are always merged at the end of the application element.
     58  *          => Indicate if there's a dup.
     59  *      D- uses-library
     60  *          => Merge. OK if already exists same {@code @name}.
     61  *          => Merge {@code @required}: true>false.
     62  * A- instrumentation:
     63  *      => Do not merge. ignore the ones from libs.
     64  * C- permission / permission-group / permission-tree:
     65  *      => Merge as-is. Error if exists in the destination (same {@code @name})
     66  *         unless the definitions are exactly the same.
     67  * C- uses-permission:
     68  *      => Add. OK if already defined.
     69  * E- uses-sdk:
     70  *      {@code @minSdkVersion}: error if dest&lt;lib. Never automatically change dest minsdk.
     71  *      {@code @targetSdkVersion}: warning if dest&lt;lib.
     72  *                                 Never automatically change dest targetsdk.
     73  *      {@code @maxSdkVersion}: obsolete, ignored. Not used in comparisons and not merged.
     74  * D- uses-feature with {@code @name}:
     75  *      => Merge with same {@code @name}
     76  *      => Merge {@code @required}: true>false.
     77  *      - Do not merge any {@code @glEsVersion} attribute at this point.
     78  * F- uses-feature with {@code @glEsVersion}:
     79  *      => Error if defined in lib+dest with dest&lt;lib. Never automatically change dest.
     80  * B- uses-configuration:
     81  *      => There can be many. Error if source defines one that is not an exact match in dest.
     82  *      (e.g. right now app must manually define something that matches exactly each lib)
     83  * B- supports-screens / compatible-screens:
     84  *      => Do not merge.
     85  *      => Error (warn?) if defined in lib and not strictly the same as in dest.
     86  * B- supports-gl-texture:
     87  *      => Do not merge. Can have more than one.
     88  *      => Error (warn?) if defined in lib and not present as-is in dest.
     89  *
     90  * Strategies:
     91  * A = Ignore, do not merge (no-op).
     92  * B = Do not merge but if defined in both must match equally.
     93  * C = Must not exist in dest or be exactly the same (key is the {@code @name} attribute).
     94  * D = Add new or merge with same key {@code @name}, adjust {@code @required} true>false.
     95  * E, F = Custom strategies; see above.
     96  *
     97  * What happens when merging libraries with conflicting information?
     98  * Say for example a main manifest has a minSdkVersion of 3, whereas libraries have
     99  * a minSdkVersion of 4 and 11. We could have 2 point of views:
    100  * - Play it safe: If we have a library with a minSdkVersion of 11, it means this
    101  *   library code knows it can't work reliably on a lower API level. So the safest end
    102  *   result would be a merged manifest with the highest minSdkVersion of all libraries.
    103  * - Trust the main manifest: When an app declares a given minSdkVersion, it also expects
    104  *   to run a given range of devices. If we change the final minSdkVersion, the app won't
    105  *   be available on as many devices as the developer might expect. And as a counterpoint
    106  *   to issue 1, the app may be careful and not call the library without checking the
    107  *   necessary features or APIs are available before hand.
    108  * Both points of views are conflicting. The solution taken here is to be conservative
    109  * and generate an error rather than merge and change a value that might be surprising.
    110  * On the other hand this can be problematic and force a developer to keep the main
    111  * manifest in sync with the libraries ones, in essence reducing the usefulness of the
    112  * automated merge to pure trivial cases. The idea is to just start this way and enhance
    113  * or revisit the mechanism later.
    114  * </pre>
    115  */
    116 public class ManifestMerger {
    117 
    118     /** Logger object. Never null. */
    119     private ISdkLog mSdkLog;
    120     private XPath mXPath;
    121     private Document mMainDoc;
    122 
    123     private String NS_URI = SdkConstants.NS_RESOURCES;
    124     private String NS_PREFIX = AndroidXPathFactory.DEFAULT_NS_PREFIX;
    125     private int destMinSdk;
    126 
    127     public ManifestMerger(ISdkLog log) {
    128         mSdkLog = log;
    129     }
    130 
    131     /**
    132      * Performs the merge operation.
    133      * <p/>
    134      * This does NOT stop on errors, in an attempt to accumulate as much
    135      * info as possible to return to the user.
    136      * Unless it failed to read the main manifest, a result file will be
    137      * created. However if process() returns false, the file should not
    138      * be used except for debugging purposes.
    139      *
    140      * @param outputFile The output path to generate. Can be the same as the main path.
    141      * @param mainFile The main manifest paths to read. What we merge into.
    142      * @param libraryFiles The library manifest paths to read. Must not be null.
    143      * @return True if the merge was completed, false otherwise.
    144      */
    145     public boolean process(File outputFile, File mainFile, File[] libraryFiles) {
    146         Document mainDoc = XmlUtils.parseDocument(mainFile, mSdkLog);
    147         if (mainDoc == null) {
    148             return false;
    149         }
    150 
    151         boolean success = process(mainDoc, libraryFiles);
    152 
    153         if (!XmlUtils.printXmlFile(mainDoc, outputFile, mSdkLog)) {
    154             success = false;
    155         }
    156         return success;
    157     }
    158 
    159     /**
    160      * Performs the merge operation in-place in the given DOM.
    161      * <p/>
    162      * This does NOT stop on errors, in an attempt to accumulate as much
    163      * info as possible to return to the user.
    164      *
    165      * @param mainDoc The document to merge into. Will be modified in-place.
    166      * @param libraryFiles The library manifest paths to read. Must not be null.
    167      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    168      */
    169     public boolean process(Document mainDoc, File[] libraryFiles) {
    170 
    171         boolean success = true;
    172         mMainDoc = mainDoc;
    173 
    174         String prefix = XmlUtils.lookupNsPrefix(mainDoc, SdkConstants.NS_RESOURCES);
    175         mXPath = AndroidXPathFactory.newXPath(prefix);
    176 
    177         for (File libFile : libraryFiles) {
    178             Document libDoc = XmlUtils.parseDocument(libFile, mSdkLog);
    179             if (libDoc == null || !mergeLibDoc(libDoc)) {
    180                 success = false;
    181             }
    182         }
    183 
    184         mXPath = null;
    185         mMainDoc = null;
    186         return success;
    187     }
    188 
    189     // --------
    190 
    191     /**
    192      * Merges the given library manifest into the destination manifest.
    193      * See {@link ManifestMerger} for merge details.
    194      *
    195      * @param libDoc The library document to merge from. Must not be null.
    196      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    197      */
    198     private boolean mergeLibDoc(Document libDoc) {
    199 
    200         boolean err = false;
    201 
    202         // Strategy B
    203         err |= !doNotMergeCheckEqual("/manifest/uses-configuration",  libDoc);     //$NON-NLS-1$
    204         err |= !doNotMergeCheckEqual("/manifest/supports-screens",    libDoc);     //$NON-NLS-1$
    205         err |= !doNotMergeCheckEqual("/manifest/compatible-screens",  libDoc);     //$NON-NLS-1$
    206         err |= !doNotMergeCheckEqual("/manifest/supports-gl-texture", libDoc);     //$NON-NLS-1$
    207 
    208         // Strategy C
    209         err |= !mergeNewOrEqual(
    210                     "/manifest/application/activity",                               //$NON-NLS-1$
    211                     "name",                                                         //$NON-NLS-1$
    212                     libDoc,
    213                     true);
    214         err |= !mergeNewOrEqual(
    215                     "/manifest/application/activity-alias",                         //$NON-NLS-1$
    216                     "name",                                                         //$NON-NLS-1$
    217                     libDoc,
    218                     true);
    219         err |= !mergeNewOrEqual(
    220                     "/manifest/application/service",                                //$NON-NLS-1$
    221                     "name",                                                         //$NON-NLS-1$
    222                     libDoc,
    223                     true);
    224         err |= !mergeNewOrEqual(
    225                     "/manifest/application/receiver",                               //$NON-NLS-1$
    226                     "name",                                                         //$NON-NLS-1$
    227                     libDoc,
    228                     true);
    229         err |= !mergeNewOrEqual(
    230                     "/manifest/application/provider",                               //$NON-NLS-1$
    231                     "name",                                                         //$NON-NLS-1$
    232                     libDoc,
    233                     true);
    234         err |= !mergeNewOrEqual(
    235                     "/manifest/permission",                                         //$NON-NLS-1$
    236                     "name",                                                         //$NON-NLS-1$
    237                     libDoc,
    238                     false);
    239         err |= !mergeNewOrEqual(
    240                     "/manifest/permission-group",                                   //$NON-NLS-1$
    241                     "name",                                                         //$NON-NLS-1$
    242                     libDoc,
    243                     false);
    244         err |= !mergeNewOrEqual(
    245                     "/manifest/permission-tree",                                    //$NON-NLS-1$
    246                     "name",                                                         //$NON-NLS-1$
    247                     libDoc,
    248                     false);
    249         err |= !mergeNewOrEqual(
    250                     "/manifest/uses-permission",                                    //$NON-NLS-1$
    251                     "name",                                                         //$NON-NLS-1$
    252                     libDoc,
    253                     false);
    254 
    255         // Strategy D
    256         err |= !mergeAdjustRequired(
    257                     "/manifest/application/uses-library",                           //$NON-NLS-1$
    258                     "name",                                                         //$NON-NLS-1$
    259                     "required",                                                     //$NON-NLS-1$
    260                     libDoc,
    261                     null /*alternateKeyAttr*/);
    262         err |= !mergeAdjustRequired(
    263                     "/manifest/uses-feature",                                       //$NON-NLS-1$
    264                     "name",                                                         //$NON-NLS-1$
    265                     "required",                                                     //$NON-NLS-1$
    266                     libDoc,
    267                     "glEsVersion" /*alternateKeyAttr*/);
    268 
    269         // Strategy E
    270         err |= !checkSdkVersion(libDoc);
    271 
    272         // Strategy F
    273         err |= !checkGlEsVersion(libDoc);
    274 
    275         return !err;
    276     }
    277 
    278     /**
    279      * Do not merge anything. Instead it checks that the requested elements from the
    280      * given library are all present and equal in the destination and prints a warning
    281      * if it's not the case.
    282      * <p/>
    283      * For example if a library supports a given screen configuration, print a
    284      * warning if the main manifest doesn't indicate the app supports the same configuration.
    285      * We should not merge it since we don't want to silently give the impression an app
    286      * supports a configuration just because it uses a library which does.
    287      * On the other hand we don't want to silently ignore this fact.
    288      * <p/>
    289      * TODO there should be a way to silence this warning.
    290      * The current behavior is certainly arbitrary and needs to be tweaked somehow.
    291      *
    292      * @param path The XPath of the elements to merge from the library. Must not be null.
    293      * @param libDoc The library document to merge from. Must not be null.
    294      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    295      */
    296     private boolean doNotMergeCheckEqual(String path, Document libDoc) {
    297 
    298         for (Element src : findElements(libDoc, path)) {
    299 
    300             boolean found = false;
    301 
    302             for (Element dest : findElements(mMainDoc, path)) {
    303                 if (compareElements(src, dest, false, null /*diff*/, null /*keyAttr*/)) {
    304                     found = true;
    305                     break;
    306                 }
    307             }
    308 
    309             if (!found) {
    310                 mSdkLog.warning("[%1$s] %2$s missing from %3$s:\n%4$s",
    311                         fileLineInfo(src, "library"),
    312                         path,
    313                         xmlFileName(mMainDoc, "main manifest"),
    314                         XmlUtils.dump(src, false /*nextSiblings*/));
    315             }
    316         }
    317 
    318         return true;
    319     }
    320 
    321     /**
    322      * Merges the requested elements from the library in the main document.
    323      * The key attribute name is used to identify the same elements.
    324      * Merged elements must either not exist in the destination or be identical.
    325      * <p/>
    326      * When merging, append to the end of the application element.
    327      * Also merges any preceding whitespace and up to one comment just prior to the merged element.
    328      *
    329      * @param path The XPath of the elements to merge from the library. Must not be null.
    330      * @param keyAttr The Android-namespace attribute used as key to identify similar elements.
    331      *   E.g. "name" for "android:name"
    332      * @param libDoc The library document to merge from. Must not be null.
    333      * @param warnDups When true, will print a warning when a library definition is already
    334      *   present in the destination and is equal.
    335      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    336      */
    337     private boolean mergeNewOrEqual(
    338             String path,
    339             String keyAttr,
    340             Document libDoc,
    341             boolean warnDups) {
    342 
    343         // The parent of XPath /p1/p2/p3 is /p1/p2. To find it, delete the last "/segment"
    344         int pos = path.lastIndexOf('/');
    345         assert pos > 1;
    346         String parentPath = path.substring(0, pos);
    347         Element parent = findFirstElement(mMainDoc, parentPath);
    348         assert parent != null;
    349         if (parent == null) {
    350             mSdkLog.error(null, "[%1$s] Could not find element %2$s.",
    351                     xmlFileName(mMainDoc, "main manifest"),
    352                     parentPath);
    353             return false;
    354         }
    355 
    356         boolean success = true;
    357 
    358         nextSource: for (Element src : findElements(libDoc, path)) {
    359             Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr);
    360             String name = attr == null ? "" : attr.getNodeValue();  //$NON-NLS-1$
    361             if (name.length() == 0) {
    362                 mSdkLog.error(null, "[%1$s] Undefined '%2$s' attribute in %3$s.",
    363                         fileLineInfo(src, "library"),
    364                         keyAttr, path);
    365                 success = false;
    366                 continue;
    367             }
    368 
    369             // Look for the same item in the destination
    370             List<Element> dests = findElements(mMainDoc, path, keyAttr, name);
    371             if (dests.size() > 1) {
    372                 // This should not be happening. We'll just use the first one found in this case.
    373                 mSdkLog.warning("[%1$s] has more than one %2$s[@%3$s=%4$s] element.",
    374                         fileLineInfo(dests.get(0), "main manifest"),
    375                         path, keyAttr, name);
    376             }
    377             for (Element dest : dests) {
    378                 // If there's already a similar node in the destination, check it's identical.
    379                 StringBuilder diff = new StringBuilder();
    380                 if (compareElements(src, dest, false, diff, keyAttr)) {
    381                     // Same element. Skip.
    382                     if (warnDups) {
    383                         mSdkLog.printf("[%1$s, %2$s] Skipping identical %3$s[@%4$s=%5$s] element.",
    384                                 fileLineInfo(src, "library"),
    385                                 fileLineInfo(dest, "main manifest"),
    386                                 path, keyAttr, name);
    387                     }
    388                     continue nextSource;
    389                 } else {
    390                     // Print the diff we got from the comparison.
    391                     mSdkLog.error(null,
    392                             "[%1$s, %2$s] Trying to merge incompatible %3$s[@%4$s=%5$s] element:\n%6$s",
    393                             fileLineInfo(src, "library"),
    394                             fileLineInfo(dest, "main manifest"),
    395                             path, keyAttr, name, diff.toString());
    396                     success = false;
    397                     continue nextSource;
    398                 }
    399             }
    400 
    401             // Ready to merge element src. Select which previous siblings to merge.
    402             Node start = selectPreviousSiblings(src);
    403 
    404             insertAtEndOf(parent, start, src);
    405         }
    406 
    407         return success;
    408     }
    409 
    410     /**
    411      * Merge elements as identified by their key name attribute.
    412      * The element must have an option boolean "required" attribute which can be either "true" or
    413      * "false". Default is true if the attribute is misisng. When merging, a "false" is superseded
    414      * by a "true" (explicit or implicit).
    415      * <p/>
    416      * When merging, this does NOT merge any other attributes than {@code keyAttr} and
    417      * {@code requiredAttr}.
    418      *
    419      * @param path The XPath of the elements to merge from the library. Must not be null.
    420      * @param keyAttr The Android-namespace attribute used as key to identify similar elements.
    421      *   E.g. "name" for "android:name"
    422      * @param requiredAttr The name of the Android-namespace boolean attribute that must be merged.
    423      *   Typically should be "required".
    424      * @param libDoc The library document to merge from. Must not be null.
    425      * @param alternateKeyAttr When non-null, this is an alternate valid key attribute. If the
    426      *   default key attribute is missing, we won't output a warning if the alternate one is
    427      *   present.
    428      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    429      */
    430     private boolean mergeAdjustRequired(
    431             String path,
    432             String keyAttr,
    433             String requiredAttr,
    434             Document libDoc,
    435             @Nullable String alternateKeyAttr) {
    436 
    437         // The parent of XPath /p1/p2/p3 is /p1/p2. To find it, delete the last "/segment"
    438         int pos = path.lastIndexOf('/');
    439         assert pos > 1;
    440         String parentPath = path.substring(0, pos);
    441         Element parent = findFirstElement(mMainDoc, parentPath);
    442         assert parent != null;
    443         if (parent == null) {
    444             mSdkLog.error(null, "[%1$s] Could not find element %2$s.",
    445                     xmlFileName(mMainDoc, "main manifest"),
    446                     parentPath);
    447             return false;
    448         }
    449 
    450         boolean success = true;
    451 
    452         for (Element src : findElements(libDoc, path)) {
    453             Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr);
    454             String name = attr == null ? "" : attr.getNodeValue().trim();  //$NON-NLS-1$
    455             if (name.length() == 0) {
    456                 if (alternateKeyAttr != null) {
    457                     attr = src.getAttributeNodeNS(NS_URI, alternateKeyAttr);
    458                     String s = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$
    459                     if (s.length() != 0) {
    460                         // This element lacks the keyAttr but has the alternateKeyAttr. Skip it.
    461                         continue;
    462                     }
    463                 }
    464 
    465                 mSdkLog.error(null, "[%1$s] Undefined '%2$s' attribute in %3$s.",
    466                         fileLineInfo(src, "library"),
    467                         keyAttr, path);
    468                 success = false;
    469                 continue;
    470             }
    471 
    472             // Look for the same item in the destination
    473             List<Element> dests = findElements(mMainDoc, path, keyAttr, name);
    474             if (dests.size() > 1) {
    475                 // This should not be happening. We'll just use the first one found in this case.
    476                 mSdkLog.warning("[%1$s] has more than one %2$s[@%3$s=%4$s] element.",
    477                         fileLineInfo(dests.get(0), "main manifest"),
    478                         path, keyAttr, name);
    479             }
    480             if (dests.size() > 0) {
    481                 attr = src.getAttributeNodeNS(NS_URI, requiredAttr);
    482                 String value = attr == null ? "true" : attr.getNodeValue();    //$NON-NLS-1$
    483                 if (value == null || !(value.equals("true") || value.equals("false"))) {
    484                     mSdkLog.warning("[%1$s] Invalid attribute '%2$s' in %3$s[@%4$s=%5$s] element:\nExpected 'true' or 'false' but found '%6$s'.",
    485                             fileLineInfo(src, "library"),
    486                             requiredAttr, path, keyAttr, name, value);
    487                     continue;
    488                 }
    489                 boolean boolE = Boolean.parseBoolean(value);
    490 
    491                 for (Element dest : dests) {
    492                     // Destination node exists. Compare the required attributes.
    493 
    494                     attr = dest.getAttributeNodeNS(NS_URI, requiredAttr);
    495                     value = attr == null ? "true" : attr.getNodeValue();    //$NON-NLS-1$
    496                     if (value == null || !(value.equals("true") || value.equals("false"))) {
    497                         mSdkLog.warning("[%1$s] Invalid attribute '%2$s' in %3$s[@%4$s=%5$s] element:\nExpected 'true' or 'false' but found '%6$s'.",
    498                                 fileLineInfo(dest, "main manifest"),
    499                                 requiredAttr, path, keyAttr, name, value);
    500                         continue;
    501                     }
    502                     boolean boolD = Boolean.parseBoolean(value);
    503 
    504                     if (!boolD && boolE) {
    505                         // Required attributes differ: destination is false and source was true
    506                         // so we need to change the destination to true.
    507 
    508                         // If attribute was already in the destination, change it in place
    509                         if (attr != null) {
    510                             attr.setNodeValue("true");                        //$NON-NLS-1$
    511                         } else {
    512                             // Otherwise, do nothing. The destination doesn't have the
    513                             // required=true attribute, and true is the default value.
    514                             // Consequently not setting is the right thing to do.
    515 
    516                             // -- code snippet for reference --
    517                             // If we wanted to create a new attribute, we'd use the code
    518                             // below. There's a simpler call to d.setAttributeNS(ns, name, value)
    519                             // but experience shows that it would create a new prefix out of the
    520                             // blue instead of looking it up.
    521                             //
    522                             // Attr a = d.getOwnerDocument().createAttributeNS(NS_URI, requiredAttr);
    523                             // String prefix = d.lookupPrefix(NS_URI);
    524                             // if (prefix != null) {
    525                             //     a.setPrefix(prefix);
    526                             // }
    527                             // a.setValue("true");  //$NON-NLS-1$
    528                             // d.setAttributeNodeNS(attr);
    529                         }
    530                     }
    531                 }
    532             } else {
    533                 // Destination doesn't exist. We simply merge the source element.
    534                 // Select which previous siblings to merge.
    535                 Node start = selectPreviousSiblings(src);
    536 
    537                 Node node = insertAtEndOf(parent, start, src);
    538 
    539                 NamedNodeMap attrs = node.getAttributes();
    540                 if (attrs != null) {
    541                     for (int i = 0; i < attrs.getLength(); i++) {
    542                         Node a = attrs.item(i);
    543                         if (a.getNodeType() == Node.ATTRIBUTE_NODE) {
    544                             boolean keep = NS_URI.equals(a.getNamespaceURI());
    545                             if (keep) {
    546                                 name = a.getLocalName();
    547                                 keep = keyAttr.equals(name) || requiredAttr.equals(name);
    548                             }
    549                             if (!keep) {
    550                                 attrs.removeNamedItemNS(NS_URI, name);
    551                                 // Restart the loop from index 0 since there's no
    552                                 // guarantee on the order of the nodes in the "map".
    553                                 // This makes it O(n+2n) at most, where n is [2..3] in
    554                                 // a typical case.
    555                                 i = -1;
    556                             }
    557                         }
    558                     }
    559                 }
    560             }
    561         }
    562 
    563         return success;
    564     }
    565 
    566 
    567 
    568     /**
    569      * Checks (but does not merge) uses-feature glEsVersion attribute using the following rules:
    570      * <pre>
    571      * - Error if defined in lib+dest with dest&lt;lib.
    572      * - Never automatically change dest.
    573      * - Default implied value is 1.0 (0x00010000).
    574      * </pre>
    575      *
    576      * @param libDoc The library document to merge from. Must not be null.
    577      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    578      */
    579     private boolean checkGlEsVersion(Document libDoc) {
    580 
    581         String parentPath = "/manifest";                                    //$NON-NLS-1$
    582         Element parent = findFirstElement(mMainDoc, parentPath);
    583         assert parent != null;
    584         if (parent == null) {
    585             mSdkLog.error(null, "[%1$s] Could not find element %2$s.",
    586                     xmlFileName(mMainDoc, "main manifest"),
    587                     parentPath);
    588             return false;
    589         }
    590 
    591         // Find the max glEsVersion on the destination side
    592         String path = "/manifest/uses-feature";                             //$NON-NLS-1$
    593         String keyAttr = "glEsVersion";                                     //$NON-NLS-1$
    594         long destGlEsVersion = 0x00010000L; // default minimum is 1.0
    595         Element destNode = null;
    596         boolean result = true;
    597         for (Element dest : findElements(mMainDoc, path)) {
    598             Attr attr = dest.getAttributeNodeNS(NS_URI, keyAttr);
    599             String value = attr == null ? "" : attr.getNodeValue().trim();   //$NON-NLS-1$
    600             if (value.length() != 0) {
    601                 try {
    602                     // Note that the value can be an hex number such as 0x00020001 so we
    603                     // need Integer.decode instead of Integer.parseInt.
    604                     // Note: Integer.decode cannot handle "ffffffff", see JDK issue 6624867
    605                     // so we just treat the version as a long and test like this, ignoring
    606                     // the fact that a value of 0xFFFF/.0xFFFF is probably invalid anyway
    607                     // in the context of glEsVersion.
    608                     long version = Long.decode(value);
    609                     if (version >= destGlEsVersion) {
    610                         destGlEsVersion = version;
    611                         destNode = dest;
    612                     } else if (version < 0x00010000) {
    613                         mSdkLog.warning("[%1$s] Ignoring <uses-feature android:glEsVersion='%2$s'> because it's smaller than 1.0.",
    614                                 fileLineInfo(dest, "main manifest"),
    615                                 value);
    616                     }
    617                 } catch (NumberFormatException e) {
    618                     // Note: NumberFormatException.toString() has no interesting information
    619                     // so we don't output it.
    620                     mSdkLog.error(null,
    621                             "[%1$s] Failed to parse <uses-feature android:glEsVersion='%2$s'>: must be an integer in the form 0x00020001.",
    622                             fileLineInfo(dest, "main manifest"),
    623                             value);
    624                     result = false;
    625                 }
    626             }
    627         }
    628 
    629         // If we found at least one valid with no error, use that, otherwise bail out.
    630         if (!result && destNode == null) {
    631             return false;
    632         }
    633 
    634         // Now find the max glEsVersion on the source side.
    635 
    636         long srcGlEsVersion = 0x00010000L; // default minimum is 1.0
    637         Element srcNode = null;
    638         result = true;
    639         for (Element src : findElements(libDoc, path)) {
    640             Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr);
    641             String value = attr == null ? "" : attr.getNodeValue().trim();   //$NON-NLS-1$
    642             if (value.length() != 0) {
    643                 try {
    644                     // See comment on Long.decode above.
    645                     long version = Long.decode(value);
    646                     if (version >= srcGlEsVersion) {
    647                         srcGlEsVersion = version;
    648                         srcNode = src;
    649                     } else if (version < 0x00010000) {
    650                         mSdkLog.warning("[%1$s] Ignoring <uses-feature android:glEsVersion='%2$s'> because it's smaller than 1.0.",
    651                                 fileLineInfo(src, "library"),
    652                                 value);
    653                     }
    654                 } catch (NumberFormatException e) {
    655                     // Note: NumberFormatException.toString() has no interesting information
    656                     // so we don't output it.
    657                     mSdkLog.error(null,
    658                             "[%1$s] Failed to parse <uses-feature android:glEsVersion='%2$s'>: must be an integer in the form 0x00020001.",
    659                             fileLineInfo(src, "library"),
    660                             value);
    661                     result = false;
    662                 }
    663             }
    664         }
    665 
    666         if (srcNode != null && destGlEsVersion < srcGlEsVersion) {
    667             mSdkLog.warning(
    668                     "[%1$s, %2$s] Main manifest has <uses-feature android:glEsVersion='0x%3$08x'> but library uses glEsVersion='0x%4$08x'%5$s",
    669                     fileLineInfo(srcNode, "library"),
    670                     fileLineInfo(destNode == null ? mMainDoc : destNode, "main manifest"),
    671                     destGlEsVersion,
    672                     srcGlEsVersion,
    673                     destNode != null ? "" :   //$NON-NLS-1$
    674                         "\nNote: main manifest lacks a <uses-feature android:glEsVersion> declaration, and thus defaults to glEsVersion=0x00010000."
    675                     );
    676             result = false;
    677         }
    678 
    679         return result;
    680     }
    681 
    682     /**
    683      * Checks (but does not merge) uses-sdk attribues using the following rules:
    684      * <pre>
    685      * - {@code @minSdkVersion}: error if dest&lt;lib. Never automatically change dest minsdk.
    686      * - {@code @targetSdkVersion}: warning if dest&lt;lib. Never automatically change destination.
    687      * - {@code @maxSdkVersion}: obsolete, ignored. Not used in comparisons and not merged.
    688      * </pre>
    689      * @param libDoc The library document to merge from. Must not be null.
    690      * @return True on success, false if any error occurred (printed to the {@link ISdkLog}).
    691      */
    692     private boolean checkSdkVersion(Document libDoc) {
    693 
    694         boolean result = true;
    695 
    696         Element destUsesSdk = findFirstElement(mMainDoc, "/manifest/uses-sdk");  //$NON-NLS-1$
    697         Element srcUsesSdk  = findFirstElement(libDoc,   "/manifest/uses-sdk");  //$NON-NLS-1$
    698 
    699         AtomicInteger destValue = new AtomicInteger(1);
    700         AtomicInteger srcValue  = new AtomicInteger(1);
    701         AtomicBoolean destImplied = new AtomicBoolean(true);
    702         AtomicBoolean srcImplied = new AtomicBoolean(true);
    703 
    704         // Check minSdkVersion
    705         destMinSdk = 1;
    706         result = extractSdkVersionAttribute(
    707                     libDoc,
    708                     destUsesSdk, srcUsesSdk,
    709                     "min",  //$NON-NLS-1$
    710                     destValue, srcValue,
    711                     destImplied, srcImplied);
    712 
    713         if (result) {
    714             // Make it an error for an application to use a library with a greater
    715             // minSdkVersion. This means the library code may crash unexpectedly.
    716             // TODO it would be nice to be able to work around this in case the
    717             // user think s/he knows what s/he's doing.
    718             // We could define a simple XML comment flag: <!-- @NoMinSdkVersionMergeError -->
    719 
    720             destMinSdk = destValue.get();
    721 
    722             if (destMinSdk < srcValue.get()) {
    723                 mSdkLog.error(null,
    724                         "[%1$s, %2$s] Main manifest has <uses-sdk android:minSdkVersion='%3$d'> but library uses minSdkVersion='%4$d'%5$s",
    725                         fileLineInfo(srcUsesSdk == null ? libDoc : srcUsesSdk, "library"),
    726                         fileLineInfo(destUsesSdk == null ? mMainDoc : destUsesSdk, "main manifest"),
    727                         destMinSdk,
    728                         srcValue.get(),
    729                         !destImplied.get() ? "" :   //$NON-NLS-1$
    730                             "\nNote: main manifest lacks a <uses-sdk android:minSdkVersion> declaration, which defaults to value 1."
    731                         );
    732                 result = false;
    733             }
    734         }
    735 
    736         // Check targetSdkVersion.
    737 
    738         // Note that destValue/srcValue purposely defaults to whatever minSdkVersion was last read
    739         // since that's their definition when missing.
    740         destImplied.set(true);
    741         srcImplied.set(true);
    742 
    743         boolean result2 = extractSdkVersionAttribute(
    744                     libDoc,
    745                     destUsesSdk, srcUsesSdk,
    746                     "target",  //$NON-NLS-1$
    747                     destValue, srcValue,
    748                     destImplied, srcImplied);
    749 
    750         result &= result2;
    751         if (result2) {
    752             // Make it a warning for an application to use a library with a greater
    753             // targetSdkVersion.
    754 
    755             int destTargetSdk = destImplied.get() ? destMinSdk : destValue.get();
    756 
    757             if (destTargetSdk < srcValue.get()) {
    758                 mSdkLog.warning(
    759                         "[%1$s, %2$s] Main manifest has <uses-sdk android:targetSdkVersion='%3$d'> but library uses targetSdkVersion='%4$d'%5$s",
    760                         fileLineInfo(srcUsesSdk == null ? libDoc : srcUsesSdk, "library"),
    761                         fileLineInfo(destUsesSdk == null ? mMainDoc : destUsesSdk, "main manifest"),
    762                         destTargetSdk,
    763                         srcValue.get(),
    764                         !destImplied.get() ? "" :   //$NON-NLS-1$
    765                             "\nNote: main manifest lacks a <uses-sdk android:targetSdkVersion> declaration, which defaults to value minSdkVersion or 1."
    766                         );
    767                 result = false;
    768             }
    769         }
    770 
    771         return result;
    772     }
    773 
    774     /**
    775      * Implementation detail for {@link #checkSdkVersion(Document)}.
    776      * Note that the various atomic out-variables must be preset to their default before
    777      * the call.
    778      * <p/>
    779      * destValue/srcValue will be filled with the integer value of the field, if present
    780      * and a correct number, in which case destImplied/destImplied are also set to true.
    781      * Otherwise the values and the implied variables are left untouched.
    782      */
    783     private boolean extractSdkVersionAttribute(
    784             Document libDoc,
    785             Element destUsesSdk,
    786             Element srcUsesSdk,
    787             String attr,
    788             AtomicInteger destValue,
    789             AtomicInteger srcValue,
    790             AtomicBoolean destImplied,
    791             AtomicBoolean srcImplied) {
    792         String s = destUsesSdk == null ? ""                                      //$NON-NLS-1$
    793                      : destUsesSdk.getAttributeNS(NS_URI, attr + "SdkVersion");  //$NON-NLS-1$
    794 
    795         assert s != null;
    796         s = s.trim();
    797         try {
    798             if (s.length() > 0) {
    799                 destValue.set(Integer.parseInt(s));
    800                 destImplied.set(false);
    801             }
    802         } catch (NumberFormatException e) {
    803             // Note: NumberFormatException.toString() has no interesting information
    804             // so we don't output it.
    805             mSdkLog.error(null,
    806                     "[%1$s] Failed to parse <uses-sdk %2$sSdkVersion='%3$s'>: must be an integer number.",
    807                     fileLineInfo(destUsesSdk == null ? mMainDoc : destUsesSdk, "main manifest"),
    808                     attr,
    809                     s);
    810             return false;
    811         }
    812 
    813         s = srcUsesSdk == null ? ""                                      //$NON-NLS-1$
    814               : srcUsesSdk.getAttributeNS(NS_URI, attr + "SdkVersion");  //$NON-NLS-1$
    815         assert s != null;
    816         s = s.trim();
    817         try {
    818             if (s.length() > 0) {
    819                 srcValue.set(Integer.parseInt(s));
    820                 srcImplied.set(false);
    821             }
    822         } catch (NumberFormatException e) {
    823             mSdkLog.error(null,
    824                     "[%1$s] Failed to parse <uses-sdk %2$sSdkVersion='%3$s'>: must be an integer number.",
    825                     fileLineInfo(srcUsesSdk == null ? libDoc : srcUsesSdk, "library"),
    826                     attr,
    827                     s);
    828             return false;
    829         }
    830 
    831         return true;
    832     }
    833 
    834 
    835     // -----
    836 
    837 
    838     /**
    839      * Given an element E, select which previous siblings we want to merge.
    840      * We want to include any whitespace up to the closing of the previous element.
    841      * We also want to include up preceding comment nodes and their preceding whitespace.
    842      * <p/>
    843      * This may returns either {@code end} or a previous sibling. Never returns null.
    844      */
    845     @NonNull
    846     private Node selectPreviousSiblings(Node end) {
    847 
    848         Node start = end;
    849         Node prev = start.getPreviousSibling();
    850         while (prev != null) {
    851             short t = prev.getNodeType();
    852             if (t == Node.TEXT_NODE) {
    853                 String text = prev.getNodeValue();
    854                 if (text == null || text.trim().length() != 0) {
    855                     // Not whitespace, we don't want it.
    856                     break;
    857                 }
    858             } else if (t == Node.COMMENT_NODE) {
    859                 // It's a comment. We'll take it.
    860             } else {
    861                 // Not a comment node nor a whitespace text. We don't want it.
    862                 break;
    863             }
    864             start = prev;
    865             prev = start.getPreviousSibling();
    866         }
    867 
    868         return start;
    869     }
    870 
    871     /**
    872      * Inserts all siblings from {@code start} to {@code end} at the end
    873      * of the given destination element.
    874      * <p/>
    875      * Implementation detail: this clones the source nodes into the destination.
    876      *
    877      * @param dest The destination at the end of which to insert. Cannot be null.
    878      * @param start The first element to insert. Must not be null.
    879      * @param end The last element to insert (included). Must not be null.
    880      *   Must be a direct "next sibling" of the start node.
    881      *   Can be equal to the start node to insert just that one node.
    882      * @return The copy of the {@code end} node in the destination document or null
    883      *   if no such copy was created and added to the destination.
    884      */
    885     private Node insertAtEndOf(Element dest, Node start, Node end) {
    886         // Check whether we'll need to adjust URI prefixes
    887         String destPrefix = mMainDoc.lookupPrefix(NS_URI);
    888         String srcPrefix  = start.getOwnerDocument().lookupPrefix(NS_URI);
    889         boolean needPrefixChange = destPrefix != null && !destPrefix.equals(srcPrefix);
    890 
    891         // First let's figure out the insertion point.
    892         // We want the end of the last 'content' element of the
    893         // destination element and basically we want to insert right
    894         // before the last whitespace of the destination element.
    895         Node target = dest.getLastChild();
    896         while (target != null) {
    897             if (target.getNodeType() == Node.TEXT_NODE) {
    898                 String text = target.getNodeValue();
    899                 if (text == null || text.trim().length() != 0) {
    900                     // Not whitespace, insert after.
    901                     break;
    902                 }
    903             } else {
    904                 // Not text. Insert after
    905                 break;
    906             }
    907             target = target.getPreviousSibling();
    908         }
    909         if (target != null) {
    910             target = target.getNextSibling();
    911         }
    912 
    913         // Destination and start..end must not be part of the same document
    914         // because we try to import below. If they were, it would mess the
    915         // structure.
    916         assert dest.getOwnerDocument() == mMainDoc;
    917         assert dest.getOwnerDocument() != start.getOwnerDocument();
    918         assert start.getOwnerDocument() == end.getOwnerDocument();
    919 
    920         while (start != null) {
    921             Node node = mMainDoc.importNode(start, true /*deep*/);
    922             if (needPrefixChange) {
    923                 changePrefix(node, srcPrefix, destPrefix);
    924             }
    925             dest.insertBefore(node, target);
    926 
    927             if (start == end) {
    928                 return node;
    929             }
    930             start = start.getNextSibling();
    931         }
    932         return null;
    933     }
    934 
    935     /**
    936      * Changes the namespace prefix of all nodes, recursively.
    937      *
    938      * @param node The node to process, as well as all it's descendants. Can be null.
    939      * @param srcPrefix The prefix to match.
    940      * @param destPrefix The new prefix to replace with.
    941      */
    942     private void changePrefix(Node node, String srcPrefix, String destPrefix) {
    943         for (; node != null; node = node.getNextSibling()) {
    944             if (srcPrefix.equals(node.getPrefix())) {
    945                 node.setPrefix(destPrefix);
    946             }
    947             Node child = node.getFirstChild();
    948             if (child != null) {
    949                 changePrefix(child, srcPrefix, destPrefix);
    950             }
    951         }
    952     }
    953 
    954     /**
    955      * Compares two {@link Element}s recursively. They must be identical with the same
    956      * structure and order. Whitespace and comments are ignored.
    957      *
    958      * @param e1 The first element to compare.
    959      * @param e2 The second element to compare with.
    960      * @param nextSiblings If true, will also compare the following siblings.
    961      *   If false, it will just compare the given node.
    962      * @param diff An optional {@link StringBuilder} where to accumulate a diff output.
    963      * @param keyAttr An optional key attribute to always add to elements when dumping a diff.
    964      * @return True if {@code e1} and {@code e2} are equal.
    965      */
    966     private boolean compareElements(
    967             @NonNull Node e1,
    968             @NonNull Node e2,
    969             boolean nextSiblings,
    970             @Nullable StringBuilder diff,
    971             @Nullable String keyAttr) {
    972         return compareElements(e1, e2, nextSiblings, diff, 0, keyAttr);
    973     }
    974 
    975     /**
    976      * Do not call directly. This is an implementation detail for
    977      * {@link #compareElements(Node, Node, boolean, StringBuilder, String)}.
    978      */
    979     private boolean compareElements(
    980             @NonNull Node e1,
    981             @NonNull Node e2,
    982             boolean nextSiblings,
    983             @Nullable StringBuilder diff,
    984             int diffOffset,
    985             @Nullable String keyAttr) {
    986         while(true) {
    987             // Find the next non-whitespace text or non-comment in e1.
    988             while (e1 != null) {
    989                 short t = e1.getNodeType();
    990 
    991                 if (t == Node.COMMENT_NODE) {
    992                     e1 = e1.getNextSibling();
    993                 } else if (t == Node.TEXT_NODE) {
    994                     String s = e1.getNodeValue().trim();
    995                     if (s.length() == 0) {
    996                         e1 = e1.getNextSibling();
    997                     } else {
    998                         break;
    999                     }
   1000                 } else {
   1001                     break;
   1002                 }
   1003             }
   1004 
   1005             // Find the next non-whitespace text or non-comment in e2.
   1006             while (e2 != null) {
   1007                 short t = e2.getNodeType();
   1008 
   1009                 if (t == Node.COMMENT_NODE) {
   1010                     e2 = e2.getNextSibling();
   1011                 } else if (t == Node.TEXT_NODE) {
   1012                     String s = e2.getNodeValue().trim();
   1013                     if (s.length() == 0) {
   1014                         e2 = e2.getNextSibling();
   1015                     } else {
   1016                         break;
   1017                     }
   1018                 } else {
   1019                     break;
   1020                 }
   1021             }
   1022 
   1023             // Same elements, or both null?
   1024             if (e1 == e2 || (e1 == null && e2 == null)) {
   1025                 return true;
   1026             }
   1027 
   1028             // Is one null but not the other?
   1029             if ((e1 == null && e2 != null) || (e1 != null && e2 == null)) {
   1030                 break;  // dumpMismatchAndExit
   1031             }
   1032 
   1033             assert e1 != null;
   1034             assert e2 != null;
   1035 
   1036             // Same type?
   1037             short t = e1.getNodeType();
   1038             if (t != e2.getNodeType()) {
   1039                 break;  // dumpMismatchAndExit
   1040             }
   1041 
   1042             // Same node name? Must both be null or have the same value.
   1043             String s1 = e1.getNodeName();
   1044             String s2 = e2.getNodeName();
   1045             if ( !( (s1 == null && s2 == null) || (s1 != null && s1.equals(s2)) ) ) {
   1046                 break;  // dumpMismatchAndExit
   1047             }
   1048 
   1049             // Same node value? Must both be null or have the same value once whitespace is trimmed.
   1050             s1 = e1.getNodeValue();
   1051             s2 = e2.getNodeValue();
   1052             if (s1 != null) {
   1053                 s1 = s1.trim();
   1054             }
   1055             if (s2 != null) {
   1056                 s2 = s2.trim();
   1057             }
   1058             if ( !( (s1 == null && s2 == null) || (s1 != null && s1.equals(s2)) ) ) {
   1059                 break;  // dumpMismatchAndExit
   1060             }
   1061 
   1062             if (diff != null) {
   1063                 // So far e1 and e2 seem pretty much equal. Dump it to the diff.
   1064                 // We need to print to the diff before dealing with the children or attributes.
   1065                 // Note: diffOffset + 1 because we want to reserve 2 spaces to write -/+
   1066                 diff.append(XmlUtils.dump(e1, diffOffset + 1,
   1067                                           false /*nextSiblings*/, false /*deep*/, keyAttr));
   1068             }
   1069 
   1070             // Now compare the attributes. When using the w3c.DOM this way, attributes are
   1071             // accessible via the Node/Element attributeMap and are not actually exposed
   1072             // as ATTR_NODEs in the node list. The downside is that we don't really
   1073             // have the proper attribute order but that's not an issue as far as the validity
   1074             // of the XML since attribute order should never matter.
   1075             List<Attr> a1 = XmlUtils.sortedAttributeList(e1.getAttributes());
   1076             List<Attr> a2 = XmlUtils.sortedAttributeList(e2.getAttributes());
   1077             if (a1.size() > 0 || a2.size() > 0) {
   1078 
   1079                   int count1 = 0;
   1080                   int count2 = 0;
   1081                 Map<String, AttrDiff> map = new TreeMap<String, AttrDiff>();
   1082                 for (Attr a : a1) {
   1083                     AttrDiff ad1 = new AttrDiff(a, "--");       //$NON-NLS-1$
   1084                     map.put(ad1.mKey, ad1);
   1085                     count1++;
   1086                 }
   1087 
   1088                 for (Attr a : a2) {
   1089                     AttrDiff ad2 = new AttrDiff(a, "++");       //$NON-NLS-1$
   1090                     AttrDiff ad1 = map.get(ad2.mKey);
   1091                     if (ad1 != null) {
   1092                         ad1.mSide = "  ";                       //$NON-NLS-1$
   1093                         count1--;
   1094                     } else {
   1095                         map.put(ad2.mKey, ad2);
   1096                         count2++;
   1097                     }
   1098                 }
   1099 
   1100                 if (count1 != 0 || count2 != 0) {
   1101                     // We found some items not matching in both sets. Dump the result.
   1102                     if (diff != null) {
   1103                         for (AttrDiff ad : map.values()) {
   1104                             diff.append(ad.mSide)
   1105                                 .append(XmlUtils.dump(ad.mAttr, diffOffset,
   1106                                                       false /*nextSiblings*/, false /*deep*/,
   1107                                                       keyAttr));
   1108                         }
   1109                     }
   1110                     // Exit without dumping
   1111                     return false;
   1112                 }
   1113             }
   1114 
   1115             // Compare recursively for elements.
   1116             if (t == Node.ELEMENT_NODE &&
   1117                     !compareElements(
   1118                             e1.getFirstChild(), e2.getFirstChild(), true,
   1119                             diff, diffOffset + 1, keyAttr)) {
   1120                 // Exit without dumping since the recursive call take cares of its own diff
   1121                 return false;
   1122             }
   1123 
   1124             if (nextSiblings) {
   1125                 e1 = e1.getNextSibling();
   1126                 e2 = e2.getNextSibling();
   1127                 continue;
   1128             } else {
   1129                 return true;
   1130             }
   1131         }
   1132 
   1133         // <INTERCAL COME FROM dumpMismatchAndExit PLEASE>
   1134         if (diff != null) {
   1135             diff.append("--")
   1136                 .append(XmlUtils.dump(e1, diffOffset,
   1137                                       false /*nextSiblings*/, false /*deep*/, keyAttr));
   1138             diff.append("++")
   1139                 .append(XmlUtils.dump(e2, diffOffset,
   1140                                       false /*nextSiblings*/, false /*deep*/, keyAttr));
   1141         }
   1142         return false;
   1143     }
   1144 
   1145     private static class AttrDiff {
   1146         public final String mKey;
   1147         public final Attr mAttr;
   1148         public String mSide;
   1149 
   1150         public AttrDiff(Attr attr, String side) {
   1151             mKey = getKey(attr);
   1152             mAttr = attr;
   1153             mSide = side;
   1154         }
   1155 
   1156         String getKey(Attr attr) {
   1157             return String.format("%s=%s", attr.getNodeName(), attr.getNodeValue());
   1158         }
   1159     }
   1160 
   1161     /**
   1162      * Finds the first element matching the given XPath expression in the given document.
   1163      *
   1164      * @param doc The document where to find the expression.
   1165      * @param path The XPath expression. It must yield an {@link Element} node type.
   1166      * @return The {@link Element} found or null.
   1167      */
   1168     @Nullable
   1169     private Element findFirstElement(
   1170             @NonNull Document doc,
   1171             @NonNull String path) {
   1172         Node result;
   1173         try {
   1174             result = (Node) mXPath.evaluate(path, doc, XPathConstants.NODE);
   1175             if (result instanceof Element) {
   1176                 return (Element) result;
   1177             }
   1178 
   1179             if (result != null) {
   1180                 mSdkLog.error(null,
   1181                         "Unexpected Node type %s when evaluating %s",   //$NON-NLS-1$
   1182                         result.getClass().getName(), path);
   1183             }
   1184         } catch (XPathExpressionException e) {
   1185             mSdkLog.error(e, "XPath error on expr %s", path);           //$NON-NLS-1$
   1186         }
   1187         return null;
   1188     }
   1189 
   1190     /**
   1191      * Finds zero or more elements matching the given XPath expression in the given document.
   1192      *
   1193      * @param doc The document where to find the expression.
   1194      * @param path The XPath expression. Only {@link Element}s nodes will be returned.
   1195      * @return A list of {@link Element} found, possibly empty but never null.
   1196      */
   1197     private List<Element> findElements(
   1198             @NonNull Document doc,
   1199             @NonNull String path) {
   1200         return findElements(doc, path, null, null);
   1201     }
   1202 
   1203 
   1204     /**
   1205      * Finds zero or more elements matching the given XPath expression in the given document.
   1206      * <p/>
   1207      * Furthermore, the elements must have an attribute matching the given attribute name
   1208      * and value if provided. (If you don't need to match an attribute, use the other version.)
   1209      * <p/>
   1210      * Note that if you provide {@code attrName} as non-null then the {@code attrValue}
   1211      * must be non-null too. In this case the XPath expression will be modified to add
   1212      * the check by naively appending a "[name='value']" filter.
   1213      *
   1214      * @param doc The document where to find the expression.
   1215      * @param path The XPath expression. Only {@link Element}s nodes will be returned.
   1216      * @param attrName The name of the optional attribute to match. Can be null.
   1217      * @param attrValue The value of the optiona attribute to match.
   1218      *   Can be null if {@code attrName} is null, otherwise must be non-null.
   1219      * @return A list of {@link Element} found, possibly empty but never null.
   1220      *
   1221      * @see #findElements(Document, String)
   1222      */
   1223     private List<Element> findElements(
   1224             @NonNull Document doc,
   1225             @NonNull String path,
   1226             @Nullable String attrName,
   1227             @Nullable String attrValue) {
   1228         List<Element> elements = new ArrayList<Element>();
   1229 
   1230         if (attrName != null) {
   1231             assert attrValue != null;
   1232             // Generate expression /manifest/application/activity[@android:name='my.fqcn']
   1233             path = String.format("%1$s[@%2$s:%3$s='%4$s']",                     //$NON-NLS-1$
   1234                     path, NS_PREFIX, attrName, attrValue);
   1235         }
   1236 
   1237         try {
   1238             NodeList results = (NodeList) mXPath.evaluate(path, doc, XPathConstants.NODESET);
   1239             if (results != null && results.getLength() > 0) {
   1240                 for (int i = 0; i < results.getLength(); i++) {
   1241                     Node n = results.item(i);
   1242                     assert n instanceof Element;
   1243                     if (n instanceof Element) {
   1244                         elements.add((Element) n);
   1245                     } else {
   1246                         mSdkLog.error(null,
   1247                                 "Unexpected Node type %s when evaluating %s",   //$NON-NLS-1$
   1248                                 n.getClass().getName(), path);
   1249                     }
   1250                 }
   1251             }
   1252 
   1253         } catch (XPathExpressionException e) {
   1254             mSdkLog.error(e, "XPath error on expr %s", path);                   //$NON-NLS-1$
   1255         }
   1256 
   1257         return elements;
   1258     }
   1259 
   1260     /**
   1261      * Tries to returns the base filename used from which the XML was parsed.
   1262      * @param node Any node from a document parsed by {@link XmlUtils#parseDocument(File, ISdkLog)}.
   1263      * @param defaultName The string to return if the XML filename cannot be determined.
   1264      * @return The base filename used from which the XML was parsed or the default name.
   1265      */
   1266     private String xmlFileName(Node node, String defaultName) {
   1267         File f = XmlUtils.extractXmlFilename(node);
   1268         if (f != null) {
   1269             return f.getName();
   1270         } else {
   1271             return defaultName;
   1272         }
   1273     }
   1274 
   1275     /**
   1276      * Tries to returns the base filename & line number from which the XML node was parsed.
   1277      *
   1278      * @param node Any node from a document parsed by {@link XmlUtils#parseDocument(File, ISdkLog)}.
   1279      * @param defaultName The string to return if the XML filename cannot be determined.
   1280      * @return The base filename used from which the XML was parsed with the line number
   1281      *   (if available) or the default name.
   1282      */
   1283     private String fileLineInfo(Node node, String defaultName) {
   1284         String name = xmlFileName(node, defaultName);
   1285         int line = XmlUtils.extractLineNumber(node);
   1286         if (line <= 0) {
   1287             return name;
   1288         } else {
   1289             return name + ':' + line;
   1290         }
   1291     }
   1292 
   1293 }
   1294