| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.manifmerger; |
| |
| import com.android.common.SdkConstants; |
| import com.android.common.annotations.NonNull; |
| import com.android.common.annotations.Nullable; |
| import com.android.manifmerger.IMergerLog.FileAndLine; |
| import com.android.manifmerger.IMergerLog.Severity; |
| import com.android.common.xml.AndroidXPathFactory; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.TreeMap; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.xml.xpath.XPath; |
| import javax.xml.xpath.XPathConstants; |
| import javax.xml.xpath.XPathExpressionException; |
| |
| /** |
| * Merges a library manifest into a main application manifest. |
| * <p/> |
| * To use, create with {@link ManifestMerger#ManifestMerger(IMergerLog, ICallback)} then |
| * call {@link ManifestMerger#process(File, File, File[])}. |
| * <p/> |
| * <pre> Merge operations: |
| * - root manifest: attributes ignored, warn if defined. |
| * - application: |
| * G- {@code @attributes}: most attributes are ignored in libs |
| * except: application:name if defined, it must match. |
| * except: application:agentBackup if defined, it must match. |
| * (these represent class names and we don't want a lib to assume their app or backup |
| * classes are being used when that will never be the case.) |
| * C- activity / activity-alias / service / receiver / provider |
| * => Merge as-is. Error if exists in the destination (same {@code @name}) |
| * unless the definitions are exactly the same. |
| * New elements are always merged at the end of the application element. |
| * => Indicate if there's a dup. |
| * D- uses-library |
| * => Merge. OK if already exists same {@code @name}. |
| * => Merge {@code @required}: true>false. |
| * A- instrumentation: |
| * => Do not merge. ignore the ones from libs. |
| * C- permission / permission-group / permission-tree: |
| * => Merge as-is. Error if exists in the destination (same {@code @name}) |
| * unless the definitions are exactly the same. |
| * C- uses-permission: |
| * => Add. OK if already defined. |
| * E- uses-sdk: |
| * {@code @minSdkVersion}: error if dest<lib. Never automatically change dest minsdk. |
| * Codenames are accepted if we can resolve their API level. |
| * {@code @targetSdkVersion}: warning if dest<lib. |
| * Never automatically change dest targetsdk. |
| * {@code @maxSdkVersion}: obsolete, ignored. Not used in comparisons and not merged. |
| * D- uses-feature with {@code @name}: |
| * => Merge with same {@code @name} |
| * => Merge {@code @required}: true>false. |
| * - Do not merge any {@code @glEsVersion} attribute at this point. |
| * F- uses-feature with {@code @glEsVersion}: |
| * => Error if defined in lib+dest with dest<lib. Never automatically change dest. |
| * B- uses-configuration: |
| * => There can be many. Error if source defines one that is not an exact match in dest. |
| * (e.g. right now app must manually define something that matches exactly each lib) |
| * B- supports-screens / compatible-screens: |
| * => Do not merge. |
| * => Error (warn?) if defined in lib and not strictly the same as in dest. |
| * B- supports-gl-texture: |
| * => Do not merge. Can have more than one. |
| * => Error (warn?) if defined in lib and not present as-is in dest. |
| * |
| * Strategies: |
| * A = Ignore, do not merge (no-op). |
| * B = Do not merge but if defined in both must match equally. |
| * C = Must not exist in dest or be exactly the same (key is the {@code @name} attribute). |
| * D = Add new or merge with same key {@code @name}, adjust {@code @required} true>false. |
| * E, F, G = Custom strategies; see above. |
| * |
| * What happens when merging libraries with conflicting information? |
| * Say for example a main manifest has a minSdkVersion of 3, whereas libraries have |
| * a minSdkVersion of 4 and 11. We could have 2 point of views: |
| * - Play it safe: If we have a library with a minSdkVersion of 11, it means this |
| * library code knows it can't work reliably on a lower API level. So the safest end |
| * result would be a merged manifest with the highest minSdkVersion of all libraries. |
| * - Trust the main manifest: When an app declares a given minSdkVersion, it also expects |
| * to run a given range of devices. If we change the final minSdkVersion, the app won't |
| * be available on as many devices as the developer might expect. And as a counterpoint |
| * to issue 1, the app may be careful and not call the library without checking the |
| * necessary features or APIs are available before hand. |
| * Both points of views are conflicting. The solution taken here is to be conservative |
| * and generate an error rather than merge and change a value that might be surprising. |
| * On the other hand this can be problematic and force a developer to keep the main |
| * manifest in sync with the libraries ones, in essence reducing the usefulness of the |
| * automated merge to pure trivial cases. The idea is to just start this way and enhance |
| * or revisit the mechanism later. |
| * </pre> |
| */ |
| public class ManifestMerger { |
| |
| /** Logger object. Never null. */ |
| private final IMergerLog mLog; |
| /** An optional callback that the merger can use to query the calling SDK. */ |
| private final ICallback mCallback; |
| private XPath mXPath; |
| private Document mMainDoc; |
| |
| private String NS_URI = SdkConstants.NS_RESOURCES; |
| private String NS_PREFIX = AndroidXPathFactory.DEFAULT_NS_PREFIX; |
| private int destMinSdk; |
| |
| /** |
| * Sets of element/attribute that need to be treated as class names. |
| * The attribute name must be the local name for the Android namespace. |
| * For example "application/name" maps to <application android:name=...>. |
| */ |
| private static final String[] sClassAttributes = { |
| "application/name", |
| "application/backupAgent", |
| "activity/name", |
| "receiver/name", |
| "service/name", |
| "provider/name", |
| "instrumentation/name" |
| }; |
| |
| /** |
| * Creates a new {@link ManifestMerger}. |
| * |
| * @param log A non-null merger log to capture all warnings, errors and their location. |
| * @param callback An optional callback that the merger can use to query the calling SDK. |
| */ |
| public ManifestMerger(@NonNull IMergerLog log, @Nullable ICallback callback) { |
| mLog = log; |
| mCallback = callback; |
| } |
| |
| /** |
| * Performs the merge operation. |
| * <p/> |
| * This does NOT stop on errors, in an attempt to accumulate as much |
| * info as possible to return to the user. |
| * Unless it failed to read the main manifest, a result file will be |
| * created. However if process() returns false, the file should not |
| * be used except for debugging purposes. |
| * |
| * @param outputFile The output path to generate. Can be the same as the main path. |
| * @param mainFile The main manifest paths to read. What we merge into. |
| * @param libraryFiles The library manifest paths to read. Must not be null. |
| * @return True if the merge was completed, false otherwise. |
| */ |
| public boolean process(File outputFile, File mainFile, File[] libraryFiles) { |
| Document mainDoc = XmlUtils.parseDocument(mainFile, mLog); |
| if (mainDoc == null) { |
| return false; |
| } |
| |
| boolean success = process(mainDoc, libraryFiles); |
| |
| if (!XmlUtils.printXmlFile(mainDoc, outputFile, mLog)) { |
| success = false; |
| } |
| return success; |
| } |
| |
| /** |
| * Performs the merge operation in-place in the given DOM. |
| * <p/> |
| * This does NOT stop on errors, in an attempt to accumulate as much |
| * info as possible to return to the user. |
| * <p/> |
| * The method might modify the input XML document in-place for its own processing. |
| * |
| * @param mainDoc The document to merge into. Will be modified in-place. |
| * @param libraryFiles The library manifest paths to read. Must not be null. |
| * These will be modified in-place. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| public boolean process(Document mainDoc, File[] libraryFiles) { |
| |
| boolean success = true; |
| mMainDoc = mainDoc; |
| XmlUtils.decorateDocument(mainDoc, IMergerLog.MAIN_MANIFEST); |
| |
| String prefix = XmlUtils.lookupNsPrefix(mainDoc, SdkConstants.NS_RESOURCES); |
| mXPath = AndroidXPathFactory.newXPath(prefix); |
| |
| expandFqcns(mainDoc); |
| for (File libFile : libraryFiles) { |
| Document libDoc = XmlUtils.parseDocument(libFile, mLog); |
| if (libDoc == null || !mergeLibDoc(libDoc)) { |
| success = false; |
| } |
| } |
| |
| mXPath = null; |
| mMainDoc = null; |
| return success; |
| } |
| |
| /** |
| * Performs the merge operation in-place in the given DOM. |
| * <p/> |
| * This does NOT stop on errors, in an attempt to accumulate as much |
| * info as possible to return to the user. |
| * <p/> |
| * The method might modify the input XML documents in-place for its own processing. |
| * |
| * @param mainDoc The document to merge into. Will be modified in-place. |
| * @param libraryDocs The library manifest documents to merge in. Must not be null. |
| * These will be modified in-place. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| public boolean process(@NonNull Document mainDoc, @NonNull Document... libraryDocs) { |
| |
| boolean success = true; |
| mMainDoc = mainDoc; |
| XmlUtils.decorateDocument(mainDoc, IMergerLog.MAIN_MANIFEST); |
| |
| String prefix = XmlUtils.lookupNsPrefix(mainDoc, SdkConstants.NS_RESOURCES); |
| mXPath = AndroidXPathFactory.newXPath(prefix); |
| |
| expandFqcns(mainDoc); |
| for (Document libDoc : libraryDocs) { |
| XmlUtils.decorateDocument(libDoc, IMergerLog.LIBRARY); |
| if (!mergeLibDoc(libDoc)) { |
| success = false; |
| } |
| } |
| |
| mXPath = null; |
| mMainDoc = null; |
| return success; |
| } |
| |
| // -------- |
| |
| /** |
| * Merges the given library manifest into the destination manifest. |
| * See {@link ManifestMerger} for merge details. |
| * |
| * @param libDoc The library document to merge from. Must not be null. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean mergeLibDoc(Document libDoc) { |
| |
| boolean err = false; |
| |
| expandFqcns(libDoc); |
| |
| // Strategy G (check <application> is compatible) |
| err |= !checkApplication(libDoc); |
| |
| // Strategy B |
| err |= !doNotMergeCheckEqual("/manifest/uses-configuration", libDoc); //$NON-NLS-1$ |
| err |= !doNotMergeCheckEqual("/manifest/supports-screens", libDoc); //$NON-NLS-1$ |
| err |= !doNotMergeCheckEqual("/manifest/compatible-screens", libDoc); //$NON-NLS-1$ |
| err |= !doNotMergeCheckEqual("/manifest/supports-gl-texture", libDoc); //$NON-NLS-1$ |
| |
| // Strategy C |
| err |= !mergeNewOrEqual( |
| "/manifest/application/activity", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| true); |
| err |= !mergeNewOrEqual( |
| "/manifest/application/activity-alias", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| true); |
| err |= !mergeNewOrEqual( |
| "/manifest/application/service", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| true); |
| err |= !mergeNewOrEqual( |
| "/manifest/application/receiver", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| true); |
| err |= !mergeNewOrEqual( |
| "/manifest/application/provider", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| true); |
| err |= !mergeNewOrEqual( |
| "/manifest/permission", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| false); |
| err |= !mergeNewOrEqual( |
| "/manifest/permission-group", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| false); |
| err |= !mergeNewOrEqual( |
| "/manifest/permission-tree", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| false); |
| err |= !mergeNewOrEqual( |
| "/manifest/uses-permission", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| libDoc, |
| false); |
| |
| // Strategy D |
| err |= !mergeAdjustRequired( |
| "/manifest/application/uses-library", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| "required", //$NON-NLS-1$ |
| libDoc, |
| null /*alternateKeyAttr*/); |
| err |= !mergeAdjustRequired( |
| "/manifest/uses-feature", //$NON-NLS-1$ |
| "name", //$NON-NLS-1$ |
| "required", //$NON-NLS-1$ |
| libDoc, |
| "glEsVersion" /*alternateKeyAttr*/); |
| |
| // Strategy E |
| err |= !checkSdkVersion(libDoc); |
| |
| // Strategy F |
| err |= !checkGlEsVersion(libDoc); |
| |
| return !err; |
| } |
| |
| /** |
| * Expand all possible class names attributes in the given document. |
| * <p/> |
| * Some manifest attributes represent class names. These can be specified as fully |
| * qualified class names or use a short notation consisting of just the terminal |
| * class simple name or a dot followed by a partial class name. Unfortunately this |
| * makes textual comparison of the attributes impossible. To simplify this, we can |
| * modify the document to fully expand all these class names. The list of elements |
| * and attributes to process is listed by {@link #sClassAttributes} and the expansion |
| * simply consists of appending the manifest' package if defined. |
| * |
| * @param doc The document in which to expand potential FQCNs. |
| */ |
| private void expandFqcns(Document doc) { |
| // Find the package attribute of the manifest. |
| String pkg = null; |
| Element manifest = findFirstElement(doc, "/manifest"); |
| if (manifest != null) { |
| pkg = manifest.getAttribute("package"); |
| } |
| |
| if (pkg == null || pkg.length() == 0) { |
| // We can't adjust FQCNs if we don't know the root package name. |
| // It's not a proper manifest if this is missing anyway. |
| assert manifest != null; |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(manifest), |
| "Missing 'package' attribute in manifest."); |
| return; |
| } |
| |
| for (String elementAttr : sClassAttributes) { |
| String[] names = elementAttr.split("/"); |
| if (names.length != 2) { |
| continue; |
| } |
| String elemName = names[0]; |
| String attrName = names[1]; |
| NodeList elements = doc.getElementsByTagName(elemName); |
| for (int i = 0; i < elements.getLength(); i++) { |
| Node elem = elements.item(i); |
| if (elem instanceof Element) { |
| Attr attr = ((Element) elem).getAttributeNodeNS(NS_URI, attrName); |
| if (attr != null) { |
| String value = attr.getNodeValue(); |
| |
| // We know it's a shortened FQCN if it starts with a dot |
| // or does not contain any dot. |
| if (value != null && value.length() > 0 && |
| (value.indexOf('.') == -1 || value.charAt(0) == '.')) { |
| if (value.charAt(0) == '.') { |
| value = pkg + value; |
| } else { |
| value = pkg + '.' + value; |
| } |
| attr.setNodeValue(value); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks (but does not merge) the application attributes using the following rules: |
| * <pre> |
| * - {@code @name}: Ignore if empty. Warning if its expanded FQCN doesn't match the main doc. |
| * - {@code @backupAgent}: Ignore if empty. Warning if its expanded FQCN doesn't match main doc. |
| * - All other attributes are ignored. |
| * </pre> |
| * The name and backupAgent represent classes and the merger will warn since if a lib has |
| * these defined they will never be used anyway. |
| * @param libDoc The library document to merge from. Must not be null. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean checkApplication(Document libDoc) { |
| |
| Element mainApp = findFirstElement(mMainDoc, "/manifest/application"); //$NON-NLS-1$ |
| Element libApp = findFirstElement(libDoc, "/manifest/application"); //$NON-NLS-1$ |
| |
| // A manifest does not necessarily define an application. |
| // If the lib has none, there's nothing to check for. |
| if (libApp == null) { |
| return true; |
| } |
| |
| for (String attrName : new String[] { "name", "backupAgent" }) { |
| String libValue = getAttributeValue(libApp, attrName); |
| if (libValue == null || libValue.length() == 0) { |
| // Nothing to do if the attribute is not defined in the lib. |
| continue; |
| } |
| // The main doc does not have to have an application node. |
| String mainValue = mainApp == null ? "" : getAttributeValue(mainApp, attrName); |
| if (!libValue.equals(mainValue)) { |
| assert mainApp != null; |
| mLog.conflict(Severity.WARNING, |
| xmlFileAndLine(mainApp), |
| xmlFileAndLine(libApp), |
| mainApp == null ? |
| "Library has <application android:%1$s='%3$s'> but main manifest has no application element." : |
| "Main manifest has <application android:%1$s='%2$s'> but library uses %1$s='%3$s'.", |
| attrName, |
| mainValue, |
| libValue); |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Do not merge anything. Instead it checks that the requested elements from the |
| * given library are all present and equal in the destination and prints a warning |
| * if it's not the case. |
| * <p/> |
| * For example if a library supports a given screen configuration, print a |
| * warning if the main manifest doesn't indicate the app supports the same configuration. |
| * We should not merge it since we don't want to silently give the impression an app |
| * supports a configuration just because it uses a library which does. |
| * On the other hand we don't want to silently ignore this fact. |
| * <p/> |
| * TODO there should be a way to silence this warning. |
| * The current behavior is certainly arbitrary and needs to be tweaked somehow. |
| * |
| * @param path The XPath of the elements to merge from the library. Must not be null. |
| * @param libDoc The library document to merge from. Must not be null. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean doNotMergeCheckEqual(String path, Document libDoc) { |
| |
| for (Element src : findElements(libDoc, path)) { |
| |
| boolean found = false; |
| |
| for (Element dest : findElements(mMainDoc, path)) { |
| if (compareElements(src, dest, false, null /*diff*/, null /*keyAttr*/)) { |
| found = true; |
| break; |
| } |
| } |
| |
| if (!found) { |
| mLog.conflict(Severity.WARNING, |
| xmlFileAndLine(mMainDoc), |
| xmlFileAndLine(src), |
| "%1$s defined in library, missing from main manifest:\n%2$s", |
| path, |
| XmlUtils.dump(src, false /*nextSiblings*/)); |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Merges the requested elements from the library in the main document. |
| * The key attribute name is used to identify the same elements. |
| * Merged elements must either not exist in the destination or be identical. |
| * <p/> |
| * When merging, append to the end of the application element. |
| * Also merges any preceding whitespace and up to one comment just prior to the merged element. |
| * |
| * @param path The XPath of the elements to merge from the library. Must not be null. |
| * @param keyAttr The Android-namespace attribute used as key to identify similar elements. |
| * E.g. "name" for "android:name" |
| * @param libDoc The library document to merge from. Must not be null. |
| * @param warnDups When true, will print a warning when a library definition is already |
| * present in the destination and is equal. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean mergeNewOrEqual( |
| String path, |
| String keyAttr, |
| Document libDoc, |
| boolean warnDups) { |
| |
| // The parent of XPath /p1/p2/p3 is /p1/p2. To find it, delete the last "/segment" |
| int pos = path.lastIndexOf('/'); |
| assert pos > 1; |
| String parentPath = path.substring(0, pos); |
| Element parent = findFirstElement(mMainDoc, parentPath); |
| assert parent != null; |
| if (parent == null) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(mMainDoc), |
| "Could not find element %1$s.", |
| parentPath); |
| return false; |
| } |
| |
| boolean success = true; |
| |
| nextSource: for (Element src : findElements(libDoc, path)) { |
| String name = getAttributeValue(src, keyAttr); |
| if (name.length() == 0) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(src), |
| "Undefined '%1$s' attribute in %2$s.", |
| keyAttr, path); |
| success = false; |
| continue; |
| } |
| |
| // Look for the same item in the destination |
| List<Element> dests = findElements(mMainDoc, path, keyAttr, name); |
| if (dests.size() > 1) { |
| // This should not be happening. We'll just use the first one found in this case. |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(dests.get(0)), |
| "Manifest has more than one %1$s[@%2$s=%3$s] element.", |
| path, keyAttr, name); |
| } |
| for (Element dest : dests) { |
| // If there's already a similar node in the destination, check it's identical. |
| StringBuilder diff = new StringBuilder(); |
| if (compareElements(src, dest, false, diff, keyAttr)) { |
| // Same element. Skip. |
| if (warnDups) { |
| mLog.conflict(Severity.INFO, |
| xmlFileAndLine(dest), |
| xmlFileAndLine(src), |
| "Skipping identical %1$s[@%2$s=%3$s] element.", |
| path, keyAttr, name); |
| } |
| continue nextSource; |
| } else { |
| // Print the diff we got from the comparison. |
| mLog.conflict(Severity.ERROR, |
| xmlFileAndLine(dest), |
| xmlFileAndLine(src), |
| "Trying to merge incompatible %1$s[@%2$s=%3$s] element:\n%4$s", |
| path, keyAttr, name, diff.toString()); |
| success = false; |
| continue nextSource; |
| } |
| } |
| |
| // Ready to merge element src. Select which previous siblings to merge. |
| Node start = selectPreviousSiblings(src); |
| |
| insertAtEndOf(parent, start, src); |
| } |
| |
| return success; |
| } |
| |
| /** |
| * Returns the value of the given "android:attribute" in the given element. |
| * |
| * @param element The non-null element where to extract the attribute. |
| * @param attrName The local name of the attribute. |
| * It must use the {@link #NS_URI} but no prefix should be specified here. |
| * @return The value of the attribute or a non-null empty string if not found. |
| */ |
| private String getAttributeValue(Element element, String attrName) { |
| Attr attr = element.getAttributeNodeNS(NS_URI, attrName); |
| String value = attr == null ? "" : attr.getNodeValue(); //$NON-NLS-1$ |
| return value; |
| } |
| |
| /** |
| * Merge elements as identified by their key name attribute. |
| * The element must have an option boolean "required" attribute which can be either "true" or |
| * "false". Default is true if the attribute is misisng. When merging, a "false" is superseded |
| * by a "true" (explicit or implicit). |
| * <p/> |
| * When merging, this does NOT merge any other attributes than {@code keyAttr} and |
| * {@code requiredAttr}. |
| * |
| * @param path The XPath of the elements to merge from the library. Must not be null. |
| * @param keyAttr The Android-namespace attribute used as key to identify similar elements. |
| * E.g. "name" for "android:name" |
| * @param requiredAttr The name of the Android-namespace boolean attribute that must be merged. |
| * Typically should be "required". |
| * @param libDoc The library document to merge from. Must not be null. |
| * @param alternateKeyAttr When non-null, this is an alternate valid key attribute. If the |
| * default key attribute is missing, we won't output a warning if the alternate one is |
| * present. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean mergeAdjustRequired( |
| String path, |
| String keyAttr, |
| String requiredAttr, |
| Document libDoc, |
| @Nullable String alternateKeyAttr) { |
| |
| // The parent of XPath /p1/p2/p3 is /p1/p2. To find it, delete the last "/segment" |
| int pos = path.lastIndexOf('/'); |
| assert pos > 1; |
| String parentPath = path.substring(0, pos); |
| Element parent = findFirstElement(mMainDoc, parentPath); |
| assert parent != null; |
| if (parent == null) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(mMainDoc), |
| "Could not find element %1$s.", |
| parentPath); |
| return false; |
| } |
| |
| boolean success = true; |
| |
| for (Element src : findElements(libDoc, path)) { |
| Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr); |
| String name = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ |
| if (name.length() == 0) { |
| if (alternateKeyAttr != null) { |
| attr = src.getAttributeNodeNS(NS_URI, alternateKeyAttr); |
| String s = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ |
| if (s.length() != 0) { |
| // This element lacks the keyAttr but has the alternateKeyAttr. Skip it. |
| continue; |
| } |
| } |
| |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(src), |
| "Undefined '%1$s' attribute in %2$s.", |
| keyAttr, path); |
| success = false; |
| continue; |
| } |
| |
| // Look for the same item in the destination |
| List<Element> dests = findElements(mMainDoc, path, keyAttr, name); |
| if (dests.size() > 1) { |
| // This should not be happening. We'll just use the first one found in this case. |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(dests.get(0)), |
| "Manifest has more than one %1$s[@%2$s=%3$s] element.", |
| path, keyAttr, name); |
| } |
| if (dests.size() > 0) { |
| attr = src.getAttributeNodeNS(NS_URI, requiredAttr); |
| String value = attr == null ? "true" : attr.getNodeValue(); //$NON-NLS-1$ |
| if (value == null || !(value.equals("true") || value.equals("false"))) { |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(src), |
| "Invalid attribute '%1$s' in %2$s[@%3$s=%4$s] element:\nExpected 'true' or 'false' but found '%5$s'.", |
| requiredAttr, path, keyAttr, name, value); |
| continue; |
| } |
| boolean boolE = Boolean.parseBoolean(value); |
| |
| for (Element dest : dests) { |
| // Destination node exists. Compare the required attributes. |
| |
| attr = dest.getAttributeNodeNS(NS_URI, requiredAttr); |
| value = attr == null ? "true" : attr.getNodeValue(); //$NON-NLS-1$ |
| if (value == null || !(value.equals("true") || value.equals("false"))) { |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(dest), |
| "Invalid attribute '%1$s' in %2$s[@%3$s=%4$s] element:\nExpected 'true' or 'false' but found '%5$s'.", |
| requiredAttr, path, keyAttr, name, value); |
| continue; |
| } |
| boolean boolD = Boolean.parseBoolean(value); |
| |
| if (!boolD && boolE) { |
| // Required attributes differ: destination is false and source was true |
| // so we need to change the destination to true. |
| |
| // If attribute was already in the destination, change it in place |
| if (attr != null) { |
| attr.setNodeValue("true"); //$NON-NLS-1$ |
| } else { |
| // Otherwise, do nothing. The destination doesn't have the |
| // required=true attribute, and true is the default value. |
| // Consequently not setting is the right thing to do. |
| |
| // -- code snippet for reference -- |
| // If we wanted to create a new attribute, we'd use the code |
| // below. There's a simpler call to d.setAttributeNS(ns, name, value) |
| // but experience shows that it would create a new prefix out of the |
| // blue instead of looking it up. |
| // |
| // Attr a=d.getOwnerDocument().createAttributeNS(NS_URI, requiredAttr); |
| // String prefix = d.lookupPrefix(NS_URI); |
| // if (prefix != null) { |
| // a.setPrefix(prefix); |
| // } |
| // a.setValue("true"); //$NON-NLS-1$ |
| // d.setAttributeNodeNS(attr); |
| } |
| } |
| } |
| } else { |
| // Destination doesn't exist. We simply merge the source element. |
| // Select which previous siblings to merge. |
| Node start = selectPreviousSiblings(src); |
| |
| Node node = insertAtEndOf(parent, start, src); |
| |
| NamedNodeMap attrs = node.getAttributes(); |
| if (attrs != null) { |
| for (int i = 0; i < attrs.getLength(); i++) { |
| Node a = attrs.item(i); |
| if (a.getNodeType() == Node.ATTRIBUTE_NODE) { |
| boolean keep = NS_URI.equals(a.getNamespaceURI()); |
| if (keep) { |
| name = a.getLocalName(); |
| keep = keyAttr.equals(name) || requiredAttr.equals(name); |
| } |
| if (!keep) { |
| attrs.removeNamedItemNS(NS_URI, name); |
| // Restart the loop from index 0 since there's no |
| // guarantee on the order of the nodes in the "map". |
| // This makes it O(n+2n) at most, where n is [2..3] in |
| // a typical case. |
| i = -1; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| return success; |
| } |
| |
| |
| |
| /** |
| * Checks (but does not merge) uses-feature glEsVersion attribute using the following rules: |
| * <pre> |
| * - Error if defined in lib+dest with dest<lib. |
| * - Never automatically change dest. |
| * - Default implied value is 1.0 (0x00010000). |
| * </pre> |
| * |
| * @param libDoc The library document to merge from. Must not be null. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean checkGlEsVersion(Document libDoc) { |
| |
| String parentPath = "/manifest"; //$NON-NLS-1$ |
| Element parent = findFirstElement(mMainDoc, parentPath); |
| assert parent != null; |
| if (parent == null) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(mMainDoc), |
| "Could not find element %1$s.", |
| parentPath); |
| return false; |
| } |
| |
| // Find the max glEsVersion on the destination side |
| String path = "/manifest/uses-feature"; //$NON-NLS-1$ |
| String keyAttr = "glEsVersion"; //$NON-NLS-1$ |
| long destGlEsVersion = 0x00010000L; // default minimum is 1.0 |
| Element destNode = null; |
| boolean result = true; |
| for (Element dest : findElements(mMainDoc, path)) { |
| Attr attr = dest.getAttributeNodeNS(NS_URI, keyAttr); |
| String value = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ |
| if (value.length() != 0) { |
| try { |
| // Note that the value can be an hex number such as 0x00020001 so we |
| // need Integer.decode instead of Integer.parseInt. |
| // Note: Integer.decode cannot handle "ffffffff", see JDK issue 6624867 |
| // so we just treat the version as a long and test like this, ignoring |
| // the fact that a value of 0xFFFF/.0xFFFF is probably invalid anyway |
| // in the context of glEsVersion. |
| long version = Long.decode(value); |
| if (version >= destGlEsVersion) { |
| destGlEsVersion = version; |
| destNode = dest; |
| } else if (version < 0x00010000) { |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(dest), |
| "Ignoring <uses-feature android:glEsVersion='%1$s'> because it's smaller than 1.0.", |
| value); |
| } |
| } catch (NumberFormatException e) { |
| // Note: NumberFormatException.toString() has no interesting information |
| // so we don't output it. |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(dest), |
| "Failed to parse <uses-feature android:glEsVersion='%1$s'>: must be an integer in the form 0x00020001.", |
| value); |
| result = false; |
| } |
| } |
| } |
| |
| // If we found at least one valid with no error, use that, otherwise bail out. |
| if (!result && destNode == null) { |
| return false; |
| } |
| |
| // Now find the max glEsVersion on the source side. |
| |
| long srcGlEsVersion = 0x00010000L; // default minimum is 1.0 |
| Element srcNode = null; |
| result = true; |
| for (Element src : findElements(libDoc, path)) { |
| Attr attr = src.getAttributeNodeNS(NS_URI, keyAttr); |
| String value = attr == null ? "" : attr.getNodeValue().trim(); //$NON-NLS-1$ |
| if (value.length() != 0) { |
| try { |
| // See comment on Long.decode above. |
| long version = Long.decode(value); |
| if (version >= srcGlEsVersion) { |
| srcGlEsVersion = version; |
| srcNode = src; |
| } else if (version < 0x00010000) { |
| mLog.error(Severity.WARNING, |
| xmlFileAndLine(src), |
| "Ignoring <uses-feature android:glEsVersion='%1$s'> because it's smaller than 1.0.", |
| value); |
| } |
| } catch (NumberFormatException e) { |
| // Note: NumberFormatException.toString() has no interesting information |
| // so we don't output it. |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(src), |
| "Failed to parse <uses-feature android:glEsVersion='%1$s'>: must be an integer in the form 0x00020001.", |
| value); |
| result = false; |
| } |
| } |
| } |
| |
| if (srcNode != null && destGlEsVersion < srcGlEsVersion) { |
| mLog.conflict(Severity.WARNING, |
| xmlFileAndLine(destNode == null ? mMainDoc : destNode), |
| xmlFileAndLine(srcNode), |
| "Main manifest has <uses-feature android:glEsVersion='0x%1$08x'> but library uses glEsVersion='0x%2$08x'%3$s", |
| destGlEsVersion, |
| srcGlEsVersion, |
| destNode != null ? "" : //$NON-NLS-1$ |
| "\nNote: main manifest lacks a <uses-feature android:glEsVersion> declaration, and thus defaults to glEsVersion=0x00010000." |
| ); |
| result = false; |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Checks (but does not merge) uses-sdk attributes using the following rules: |
| * <pre> |
| * - {@code @minSdkVersion}: error if dest<lib. Never automatically change dest minsdk. |
| * - {@code @targetSdkVersion}: warning if dest<lib. Never automatically change destination. |
| * - {@code @maxSdkVersion}: obsolete, ignored. Not used in comparisons and not merged. |
| * - The API level can be a codename if we have a callback that can convert it to an integer. |
| * </pre> |
| * @param libDoc The library document to merge from. Must not be null. |
| * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). |
| */ |
| private boolean checkSdkVersion(Document libDoc) { |
| |
| boolean result = true; |
| |
| Element destUsesSdk = findFirstElement(mMainDoc, "/manifest/uses-sdk"); //$NON-NLS-1$ |
| Element srcUsesSdk = findFirstElement(libDoc, "/manifest/uses-sdk"); //$NON-NLS-1$ |
| |
| AtomicInteger destValue = new AtomicInteger(1); |
| AtomicInteger srcValue = new AtomicInteger(1); |
| AtomicBoolean destImplied = new AtomicBoolean(true); |
| AtomicBoolean srcImplied = new AtomicBoolean(true); |
| |
| // Check minSdkVersion |
| destMinSdk = 1; |
| result = extractSdkVersionAttribute( |
| libDoc, |
| destUsesSdk, srcUsesSdk, |
| "min", //$NON-NLS-1$ |
| destValue, srcValue, |
| destImplied, srcImplied); |
| |
| if (result) { |
| // Make it an error for an application to use a library with a greater |
| // minSdkVersion. This means the library code may crash unexpectedly. |
| // TODO it would be nice to be able to work around this in case the |
| // user think s/he knows what s/he's doing. |
| // We could define a simple XML comment flag: <!-- @NoMinSdkVersionMergeError --> |
| |
| destMinSdk = destValue.get(); |
| |
| if (destMinSdk < srcValue.get()) { |
| mLog.conflict(Severity.ERROR, |
| xmlFileAndLine(destUsesSdk == null ? mMainDoc : destUsesSdk), |
| xmlFileAndLine(srcUsesSdk == null ? libDoc : srcUsesSdk), |
| "Main manifest has <uses-sdk android:minSdkVersion='%1$d'> but library uses minSdkVersion='%2$d'%3$s", |
| destMinSdk, |
| srcValue.get(), |
| !destImplied.get() ? "" : //$NON-NLS-1$ |
| "\nNote: main manifest lacks a <uses-sdk android:minSdkVersion> declaration, which defaults to value 1." |
| ); |
| result = false; |
| } |
| } |
| |
| // Check targetSdkVersion. |
| |
| // Note that destValue/srcValue purposely defaults to whatever minSdkVersion was last read |
| // since that's their definition when missing. |
| destImplied.set(true); |
| srcImplied.set(true); |
| |
| boolean result2 = extractSdkVersionAttribute( |
| libDoc, |
| destUsesSdk, srcUsesSdk, |
| "target", //$NON-NLS-1$ |
| destValue, srcValue, |
| destImplied, srcImplied); |
| |
| result &= result2; |
| if (result2) { |
| // Make it a warning for an application to use a library with a greater |
| // targetSdkVersion. |
| |
| int destTargetSdk = destImplied.get() ? destMinSdk : destValue.get(); |
| |
| if (destTargetSdk < srcValue.get()) { |
| mLog.conflict(Severity.WARNING, |
| xmlFileAndLine(destUsesSdk == null ? mMainDoc : destUsesSdk), |
| xmlFileAndLine(srcUsesSdk == null ? libDoc : srcUsesSdk), |
| "Main manifest has <uses-sdk android:targetSdkVersion='%1$d'> but library uses targetSdkVersion='%2$d'%3$s", |
| destTargetSdk, |
| srcValue.get(), |
| !destImplied.get() ? "" : //$NON-NLS-1$ |
| "\nNote: main manifest lacks a <uses-sdk android:targetSdkVersion> declaration, which defaults to value minSdkVersion or 1." |
| ); |
| result = false; |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Implementation detail for {@link #checkSdkVersion(Document)}. |
| * Note that the various atomic out-variables must be preset to their default before |
| * the call. |
| * <p/> |
| * destValue/srcValue will be filled with the integer value of the field, if present |
| * and a correct number, in which case destImplied/destImplied are also set to true. |
| * Otherwise the values and the implied variables are left untouched. |
| */ |
| private boolean extractSdkVersionAttribute( |
| Document libDoc, |
| Element destUsesSdk, |
| Element srcUsesSdk, |
| String attr, |
| AtomicInteger destValue, |
| AtomicInteger srcValue, |
| AtomicBoolean destImplied, |
| AtomicBoolean srcImplied) { |
| String s = destUsesSdk == null ? "" //$NON-NLS-1$ |
| : destUsesSdk.getAttributeNS(NS_URI, attr + "SdkVersion"); //$NON-NLS-1$ |
| |
| boolean result = true; |
| assert s != null; |
| s = s.trim(); |
| try { |
| if (s.length() > 0) { |
| destValue.set(Integer.parseInt(s)); |
| destImplied.set(false); |
| } |
| } catch (NumberFormatException e) { |
| boolean error = true; |
| if (mCallback != null) { |
| // Versions can contain codenames such as "JellyBean". |
| // We'll accept it only if have a callback that can give us the API level for it. |
| int apiLevel = mCallback.queryCodenameApiLevel(s); |
| if (apiLevel > ICallback.UNKNOWN_CODENAME) { |
| destValue.set(apiLevel); |
| destImplied.set(false); |
| error = false; |
| } |
| } |
| if (error) { |
| // Note: NumberFormatException.toString() has no interesting information |
| // so we don't output it. |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(destUsesSdk == null ? mMainDoc : destUsesSdk), |
| "Failed to parse <uses-sdk %1$sSdkVersion='%2$s'>: must be an integer number or codename.", |
| attr, |
| s); |
| result = false; |
| } |
| } |
| |
| s = srcUsesSdk == null ? "" //$NON-NLS-1$ |
| : srcUsesSdk.getAttributeNS(NS_URI, attr + "SdkVersion"); //$NON-NLS-1$ |
| assert s != null; |
| s = s.trim(); |
| try { |
| if (s.length() > 0) { |
| srcValue.set(Integer.parseInt(s)); |
| srcImplied.set(false); |
| } |
| } catch (NumberFormatException e) { |
| boolean error = true; |
| if (mCallback != null) { |
| // Versions can contain codenames such as "JellyBean". |
| // We'll accept it only if have a callback that can give us the API level for it. |
| int apiLevel = mCallback.queryCodenameApiLevel(s); |
| if (apiLevel > ICallback.UNKNOWN_CODENAME) { |
| srcValue.set(apiLevel); |
| srcImplied.set(false); |
| error = false; |
| } |
| } |
| if (error) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(srcUsesSdk == null ? libDoc : srcUsesSdk), |
| "Failed to parse <uses-sdk %1$sSdkVersion='%2$s'>: must be an integer number or codename.", |
| attr, |
| s); |
| result = false; |
| } |
| } |
| |
| return result; |
| } |
| |
| |
| // ----- |
| |
| |
| /** |
| * Given an element E, select which previous siblings we want to merge. |
| * We want to include any whitespace up to the closing of the previous element. |
| * We also want to include up preceding comment nodes and their preceding whitespace. |
| * <p/> |
| * This may returns either {@code end} or a previous sibling. Never returns null. |
| */ |
| @NonNull |
| private Node selectPreviousSiblings(Node end) { |
| |
| Node start = end; |
| Node prev = start.getPreviousSibling(); |
| while (prev != null) { |
| short t = prev.getNodeType(); |
| if (t == Node.TEXT_NODE) { |
| String text = prev.getNodeValue(); |
| if (text == null || text.trim().length() != 0) { |
| // Not whitespace, we don't want it. |
| break; |
| } |
| } else if (t == Node.COMMENT_NODE) { |
| // It's a comment. We'll take it. |
| } else { |
| // Not a comment node nor a whitespace text. We don't want it. |
| break; |
| } |
| start = prev; |
| prev = start.getPreviousSibling(); |
| } |
| |
| return start; |
| } |
| |
| /** |
| * Inserts all siblings from {@code start} to {@code end} at the end |
| * of the given destination element. |
| * <p/> |
| * Implementation detail: this clones the source nodes into the destination. |
| * |
| * @param dest The destination at the end of which to insert. Cannot be null. |
| * @param start The first element to insert. Must not be null. |
| * @param end The last element to insert (included). Must not be null. |
| * Must be a direct "next sibling" of the start node. |
| * Can be equal to the start node to insert just that one node. |
| * @return The copy of the {@code end} node in the destination document or null |
| * if no such copy was created and added to the destination. |
| */ |
| private Node insertAtEndOf(Element dest, Node start, Node end) { |
| // Check whether we'll need to adjust URI prefixes |
| String destPrefix = XmlUtils.lookupNsPrefix(mMainDoc, NS_URI); |
| String srcPrefix = XmlUtils.lookupNsPrefix(start.getOwnerDocument(), NS_URI); |
| boolean needPrefixChange = destPrefix != null && !destPrefix.equals(srcPrefix); |
| |
| // First let's figure out the insertion point. |
| // We want the end of the last 'content' element of the |
| // destination element and basically we want to insert right |
| // before the last whitespace of the destination element. |
| Node target = dest.getLastChild(); |
| while (target != null) { |
| if (target.getNodeType() == Node.TEXT_NODE) { |
| String text = target.getNodeValue(); |
| if (text == null || text.trim().length() != 0) { |
| // Not whitespace, insert after. |
| break; |
| } |
| } else { |
| // Not text. Insert after |
| break; |
| } |
| target = target.getPreviousSibling(); |
| } |
| if (target != null) { |
| target = target.getNextSibling(); |
| } |
| |
| // Destination and start..end must not be part of the same document |
| // because we try to import below. If they were, it would mess the |
| // structure. |
| assert dest.getOwnerDocument() == mMainDoc; |
| assert dest.getOwnerDocument() != start.getOwnerDocument(); |
| assert start.getOwnerDocument() == end.getOwnerDocument(); |
| |
| while (start != null) { |
| Node node = mMainDoc.importNode(start, true /*deep*/); |
| if (needPrefixChange) { |
| changePrefix(node, srcPrefix, destPrefix); |
| } |
| dest.insertBefore(node, target); |
| |
| if (start == end) { |
| return node; |
| } |
| start = start.getNextSibling(); |
| } |
| return null; |
| } |
| |
| /** |
| * Changes the namespace prefix of all nodes, recursively. |
| * |
| * @param node The node to process, as well as all it's descendants. Can be null. |
| * @param srcPrefix The prefix to match. |
| * @param destPrefix The new prefix to replace with. |
| */ |
| private void changePrefix(Node node, String srcPrefix, String destPrefix) { |
| for (; node != null; node = node.getNextSibling()) { |
| if (srcPrefix.equals(node.getPrefix())) { |
| node.setPrefix(destPrefix); |
| } |
| Node child = node.getFirstChild(); |
| if (child != null) { |
| changePrefix(child, srcPrefix, destPrefix); |
| } |
| } |
| } |
| |
| /** |
| * Compares two {@link Element}s recursively. They must be identical with the same |
| * structure and order. Whitespace and comments are ignored. |
| * |
| * @param e1 The first element to compare. |
| * @param e2 The second element to compare with. |
| * @param nextSiblings If true, will also compare the following siblings. |
| * If false, it will just compare the given node. |
| * @param diff An optional {@link StringBuilder} where to accumulate a diff output. |
| * @param keyAttr An optional key attribute to always add to elements when dumping a diff. |
| * @return True if {@code e1} and {@code e2} are equal. |
| */ |
| private boolean compareElements( |
| @NonNull Node e1, |
| @NonNull Node e2, |
| boolean nextSiblings, |
| @Nullable StringBuilder diff, |
| @Nullable String keyAttr) { |
| return compareElements(e1, e2, nextSiblings, diff, 0, keyAttr); |
| } |
| |
| /** |
| * Do not call directly. This is an implementation detail for |
| * {@link #compareElements(Node, Node, boolean, StringBuilder, String)}. |
| */ |
| private boolean compareElements( |
| @NonNull Node e1, |
| @NonNull Node e2, |
| boolean nextSiblings, |
| @Nullable StringBuilder diff, |
| int diffOffset, |
| @Nullable String keyAttr) { |
| while(true) { |
| // Find the next non-whitespace text or non-comment in e1. |
| while (e1 != null) { |
| short t = e1.getNodeType(); |
| |
| if (t == Node.COMMENT_NODE) { |
| e1 = e1.getNextSibling(); |
| } else if (t == Node.TEXT_NODE) { |
| String s = e1.getNodeValue().trim(); |
| if (s.length() == 0) { |
| e1 = e1.getNextSibling(); |
| } else { |
| break; |
| } |
| } else { |
| break; |
| } |
| } |
| |
| // Find the next non-whitespace text or non-comment in e2. |
| while (e2 != null) { |
| short t = e2.getNodeType(); |
| |
| if (t == Node.COMMENT_NODE) { |
| e2 = e2.getNextSibling(); |
| } else if (t == Node.TEXT_NODE) { |
| String s = e2.getNodeValue().trim(); |
| if (s.length() == 0) { |
| e2 = e2.getNextSibling(); |
| } else { |
| break; |
| } |
| } else { |
| break; |
| } |
| } |
| |
| // Same elements, or both null? |
| if (e1 == e2 || (e1 == null && e2 == null)) { |
| return true; |
| } |
| |
| // Is one null but not the other? |
| if ((e1 == null && e2 != null) || (e1 != null && e2 == null)) { |
| break; // dumpMismatchAndExit |
| } |
| |
| assert e1 != null; |
| assert e2 != null; |
| |
| // Same type? |
| short t = e1.getNodeType(); |
| if (t != e2.getNodeType()) { |
| break; // dumpMismatchAndExit |
| } |
| |
| // Same node name? Must both be null or have the same value. |
| String s1 = e1.getNodeName(); |
| String s2 = e2.getNodeName(); |
| if ( !( (s1 == null && s2 == null) || (s1 != null && s1.equals(s2)) ) ) { |
| break; // dumpMismatchAndExit |
| } |
| |
| // Same node value? Must both be null or have the same value once whitespace is trimmed. |
| s1 = e1.getNodeValue(); |
| s2 = e2.getNodeValue(); |
| if (s1 != null) { |
| s1 = s1.trim(); |
| } |
| if (s2 != null) { |
| s2 = s2.trim(); |
| } |
| if ( !( (s1 == null && s2 == null) || (s1 != null && s1.equals(s2)) ) ) { |
| break; // dumpMismatchAndExit |
| } |
| |
| if (diff != null) { |
| // So far e1 and e2 seem pretty much equal. Dump it to the diff. |
| // We need to print to the diff before dealing with the children or attributes. |
| // Note: diffOffset + 1 because we want to reserve 2 spaces to write -/+ |
| diff.append(XmlUtils.dump(e1, diffOffset + 1, |
| false /*nextSiblings*/, false /*deep*/, keyAttr)); |
| } |
| |
| // Now compare the attributes. When using the w3c.DOM this way, attributes are |
| // accessible via the Node/Element attributeMap and are not actually exposed |
| // as ATTR_NODEs in the node list. The downside is that we don't really |
| // have the proper attribute order but that's not an issue as far as the validity |
| // of the XML since attribute order should never matter. |
| List<Attr> a1 = XmlUtils.sortedAttributeList(e1.getAttributes()); |
| List<Attr> a2 = XmlUtils.sortedAttributeList(e2.getAttributes()); |
| if (a1.size() > 0 || a2.size() > 0) { |
| |
| int count1 = 0; |
| int count2 = 0; |
| Map<String, AttrDiff> map = new TreeMap<String, AttrDiff>(); |
| for (Attr a : a1) { |
| AttrDiff ad1 = new AttrDiff(a, "--"); //$NON-NLS-1$ |
| map.put(ad1.mKey, ad1); |
| count1++; |
| } |
| |
| for (Attr a : a2) { |
| AttrDiff ad2 = new AttrDiff(a, "++"); //$NON-NLS-1$ |
| AttrDiff ad1 = map.get(ad2.mKey); |
| if (ad1 != null) { |
| ad1.mSide = " "; //$NON-NLS-1$ |
| count1--; |
| } else { |
| map.put(ad2.mKey, ad2); |
| count2++; |
| } |
| } |
| |
| if (count1 != 0 || count2 != 0) { |
| // We found some items not matching in both sets. Dump the result. |
| if (diff != null) { |
| for (AttrDiff ad : map.values()) { |
| diff.append(ad.mSide) |
| .append(XmlUtils.dump(ad.mAttr, diffOffset, |
| false /*nextSiblings*/, false /*deep*/, |
| keyAttr)); |
| } |
| } |
| // Exit without dumping |
| return false; |
| } |
| } |
| |
| // Compare recursively for elements. |
| if (t == Node.ELEMENT_NODE && |
| !compareElements( |
| e1.getFirstChild(), e2.getFirstChild(), true, |
| diff, diffOffset + 1, keyAttr)) { |
| // Exit without dumping since the recursive call take cares of its own diff |
| return false; |
| } |
| |
| if (nextSiblings) { |
| e1 = e1.getNextSibling(); |
| e2 = e2.getNextSibling(); |
| continue; |
| } else { |
| return true; |
| } |
| } |
| |
| // <INTERCAL COME FROM dumpMismatchAndExit PLEASE> |
| if (diff != null) { |
| diff.append("--") |
| .append(XmlUtils.dump(e1, diffOffset, |
| false /*nextSiblings*/, false /*deep*/, keyAttr)); |
| diff.append("++") |
| .append(XmlUtils.dump(e2, diffOffset, |
| false /*nextSiblings*/, false /*deep*/, keyAttr)); |
| } |
| return false; |
| } |
| |
| private static class AttrDiff { |
| public final String mKey; |
| public final Attr mAttr; |
| public String mSide; |
| |
| public AttrDiff(Attr attr, String side) { |
| mKey = getKey(attr); |
| mAttr = attr; |
| mSide = side; |
| } |
| |
| String getKey(Attr attr) { |
| return String.format("%s=%s", attr.getNodeName(), attr.getNodeValue()); |
| } |
| } |
| |
| /** |
| * Finds the first element matching the given XPath expression in the given document. |
| * |
| * @param doc The document where to find the expression. |
| * @param path The XPath expression. It must yield an {@link Element} node type. |
| * @return The {@link Element} found or null. |
| */ |
| @Nullable |
| private Element findFirstElement( |
| @NonNull Document doc, |
| @NonNull String path) { |
| Node result; |
| try { |
| result = (Node) mXPath.evaluate(path, doc, XPathConstants.NODE); |
| if (result instanceof Element) { |
| return (Element) result; |
| } |
| |
| if (result != null) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(doc), |
| "Unexpected Node type %s when evaluating %s", //$NON-NLS-1$ |
| result.getClass().getName(), path); |
| } |
| } catch (XPathExpressionException e) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(doc), |
| "XPath error on expr %s: %s", //$NON-NLS-1$ |
| path, e.toString()); |
| } |
| return null; |
| } |
| |
| /** |
| * Finds zero or more elements matching the given XPath expression in the given document. |
| * |
| * @param doc The document where to find the expression. |
| * @param path The XPath expression. Only {@link Element}s nodes will be returned. |
| * @return A list of {@link Element} found, possibly empty but never null. |
| */ |
| private List<Element> findElements( |
| @NonNull Document doc, |
| @NonNull String path) { |
| return findElements(doc, path, null, null); |
| } |
| |
| |
| /** |
| * Finds zero or more elements matching the given XPath expression in the given document. |
| * <p/> |
| * Furthermore, the elements must have an attribute matching the given attribute name |
| * and value if provided. (If you don't need to match an attribute, use the other version.) |
| * <p/> |
| * Note that if you provide {@code attrName} as non-null then the {@code attrValue} |
| * must be non-null too. In this case the XPath expression will be modified to add |
| * the check by naively appending a "[name='value']" filter. |
| * |
| * @param doc The document where to find the expression. |
| * @param path The XPath expression. Only {@link Element}s nodes will be returned. |
| * @param attrName The name of the optional attribute to match. Can be null. |
| * @param attrValue The value of the optional attribute to match. |
| * Can be null if {@code attrName} is null, otherwise must be non-null. |
| * @return A list of {@link Element} found, possibly empty but never null. |
| * |
| * @see #findElements(Document, String) |
| */ |
| private List<Element> findElements( |
| @NonNull Document doc, |
| @NonNull String path, |
| @Nullable String attrName, |
| @Nullable String attrValue) { |
| List<Element> elements = new ArrayList<Element>(); |
| |
| if (attrName != null) { |
| assert attrValue != null; |
| // Generate expression /manifest/application/activity[@android:name='my.fqcn'] |
| path = String.format("%1$s[@%2$s:%3$s='%4$s']", //$NON-NLS-1$ |
| path, NS_PREFIX, attrName, attrValue); |
| } |
| |
| try { |
| NodeList results = (NodeList) mXPath.evaluate(path, doc, XPathConstants.NODESET); |
| if (results != null && results.getLength() > 0) { |
| for (int i = 0; i < results.getLength(); i++) { |
| Node n = results.item(i); |
| assert n instanceof Element; |
| if (n instanceof Element) { |
| elements.add((Element) n); |
| } else { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(doc), |
| "Unexpected Node type %s when evaluating %s", //$NON-NLS-1$ |
| n.getClass().getName(), path); |
| } |
| } |
| } |
| |
| } catch (XPathExpressionException e) { |
| mLog.error(Severity.ERROR, |
| xmlFileAndLine(doc), |
| "XPath error on expr %s: %s", //$NON-NLS-1$ |
| path, e.toString()); |
| } |
| |
| return elements; |
| } |
| |
| /** |
| * Returns a new {@link FileAndLine} structure that identifies |
| * the base filename & line number from which the XML node was parsed. |
| * <p/> |
| * When the line number is unknown (e.g. if a {@link Document} instance is given) |
| * then line number 0 will be used. |
| * |
| * @param node The node or document where the error occurs. Must not be null. |
| * @return A new non-null {@link FileAndLine} combining the file name and line number. |
| */ |
| private @NonNull FileAndLine xmlFileAndLine(@NonNull Node node) { |
| String name = XmlUtils.extractXmlFilename(node); |
| int line = XmlUtils.extractLineNumber(node); // 0 in case of error or unknown |
| return new FileAndLine(name, line); |
| } |
| |
| } |