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<lib. Never automatically change dest minsdk. 71 * {@code @targetSdkVersion}: warning if dest<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<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<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<lib. Never automatically change dest minsdk. 686 * - {@code @targetSdkVersion}: warning if dest<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