diff options
author | Raphael Moll <ralf@android.com> | 2012-09-28 15:29:21 -0700 |
---|---|---|
committer | Raphael Moll <ralf@android.com> | 2012-09-28 15:46:39 -0700 |
commit | 2b426ae27c0be8ebd26bc69ad59007e45487568b (patch) | |
tree | 6fb2982a35a1a9a0bd4457d27f00b4857f1ed7e1 /manifmerger | |
parent | c439a89ccdcf7123f52cb528d57d5ef78881e0da (diff) | |
download | sdk-2b426ae27c0be8ebd26bc69ad59007e45487568b.zip sdk-2b426ae27c0be8ebd26bc69ad59007e45487568b.tar.gz sdk-2b426ae27c0be8ebd26bc69ad59007e45487568b.tar.bz2 |
ManifestMerger: ability to inject attributes.
Change-Id: Icbebe1dd3c8cf51f7d38b585a78264d01977e943
Diffstat (limited to 'manifmerger')
7 files changed, 308 insertions, 17 deletions
diff --git a/manifmerger/src/com/android/manifmerger/Main.java b/manifmerger/src/com/android/manifmerger/Main.java index c48033f..2a6460e 100644 --- a/manifmerger/src/com/android/manifmerger/Main.java +++ b/manifmerger/src/com/android/manifmerger/Main.java @@ -20,6 +20,7 @@ import com.android.utils.ILogger; import com.android.utils.StdLogger; import java.io.File; +import java.util.Map; /** * Command-line entry point of the Manifest Merger. @@ -33,7 +34,8 @@ import java.io.File; * Usage: <br/> * {@code $ manifmerger merge --main main_manifest.xml --libs lib1.xml lib2.xml --out result.xml} * <p/> - * When used as a library, please call {@link ManifestMerger#process(File, File, File[])} directly. + * When used as a library, please call {@link ManifestMerger#process(File, File, File[], Map)} + * directly. */ public class Main { @@ -68,7 +70,8 @@ public class Main { boolean ok = mm.process( new File(mArgvParser.getParamOut()), new File(mArgvParser.getParamMain()), - libFiles + libFiles, + null /*injectAttributes*/ ); System.exit(ok ? 0 : 1); } diff --git a/manifmerger/src/com/android/manifmerger/ManifestMerger.java b/manifmerger/src/com/android/manifmerger/ManifestMerger.java index 688cecd..bbe6e25 100755 --- a/manifmerger/src/com/android/manifmerger/ManifestMerger.java +++ b/manifmerger/src/com/android/manifmerger/ManifestMerger.java @@ -46,7 +46,7 @@ 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[])}. + * call {@link ManifestMerger#process(File, File, File[], Map)}. * <p/> * <pre> Merge operations: * - root manifest: attributes ignored, warn if defined. @@ -170,15 +170,25 @@ public class ManifestMerger { * @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. + * @param injectAttributes A map of attributes to inject in the form [pseudo-xpath] => value. + * The key is "/manifest/elements...|attribute-ns-uri attribute-local-name", + * for example "/manifest/uses-sdk|http://schemas.android.com/apk/res/android minSdkVersion". + * (note the space separator between the attribute URI and its local name.) + * The elements will be created if they don't exists. Existing attributes will be modified. + * The replacement is done on the main document <em>before</em> merging. * @return True if the merge was completed, false otherwise. */ - public boolean process(File outputFile, File mainFile, File[] libraryFiles) { + public boolean process( + File outputFile, + File mainFile, + File[] libraryFiles, + Map<String, String> injectAttributes) { Document mainDoc = XmlUtils.parseDocument(mainFile, mLog); if (mainDoc == null) { return false; } - boolean success = process(mainDoc, libraryFiles); + boolean success = process(mainDoc, libraryFiles, injectAttributes); if (!XmlUtils.printXmlFile(mainDoc, outputFile, mLog)) { success = false; @@ -197,13 +207,23 @@ public class ManifestMerger { * @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. + * @param injectAttributes A map of attributes to inject in the form [pseudo-xpath] => value. + * The key is "/manifest/elements...|attribute-ns-uri attribute-local-name", + * for example "/manifest/uses-sdk|http://schemas.android.com/apk/res/android minSdkVersion". + * (note the space separator between the attribute URI and its local name.) + * The elements will be created if they don't exists. Existing attributes will be modified. + * The replacement is done on the main document <em>before</em> merging. * @return True on success, false if any error occurred (printed to the {@link IMergerLog}). */ - public boolean process(Document mainDoc, File[] libraryFiles) { + public boolean process( + Document mainDoc, + File[] libraryFiles, + Map<String, String> injectAttributes) { boolean success = true; mMainDoc = mainDoc; XmlUtils.decorateDocument(mainDoc, IMergerLog.MAIN_MANIFEST); + XmlUtils.injectAttributes(mainDoc, injectAttributes, mLog); String prefix = XmlUtils.lookupNsPrefix(mainDoc, SdkConstants.NS_RESOURCES); mXPath = AndroidXPathFactory.newXPath(prefix); @@ -1343,9 +1363,7 @@ public class ManifestMerger { * @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); + return XmlUtils.xmlFileAndLine(node); } diff --git a/manifmerger/src/com/android/manifmerger/XmlUtils.java b/manifmerger/src/com/android/manifmerger/XmlUtils.java index 71aac91..b17c7d4 100755 --- a/manifmerger/src/com/android/manifmerger/XmlUtils.java +++ b/manifmerger/src/com/android/manifmerger/XmlUtils.java @@ -24,6 +24,7 @@ import com.android.utils.ILogger; 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.xml.sax.ErrorHandler; @@ -177,6 +178,23 @@ class XmlUtils { } /** + * 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. + */ + @NonNull + static FileAndLine xmlFileAndLine(@NonNull Node node) { + String name = extractXmlFilename(node); + int line = extractLineNumber(node); // 0 in case of error or unknown + return new FileAndLine(name, line); + } + + /** * Extracts the origin {@link File} that {@link #parseDocument(File, IMergerLog)} * added to the XML document or the string added by * @@ -504,6 +522,111 @@ class XmlUtils { }; } + /** + * Inject attributes into an existing document. + * <p/> + * The map keys are "/manifest/elements...|attribute-ns-uri attribute-local-name", + * for example "/manifest/uses-sdk|http://schemas.android.com/apk/res/android minSdkVersion". + * (note the space separator between the attribute URI and its local name.) + * The elements will be created if they don't exists. Existing attributes will be modified. + * The replacement is done on the main document <em>before</em> merging. + * The value can be null to remove an existing attribute. + * + * @param doc The document to modify in-place. + * @param attributeMap A map of attributes to inject in the form [pseudo-xpath] => value. + * @param log A log in case of error. + */ + static void injectAttributes( + @Nullable Document doc, + @Nullable Map<String, String> attributeMap, + @NonNull IMergerLog log) { + if (doc == null || attributeMap == null || attributeMap.isEmpty()) { + return; + } + + // 1=path 2=URI 3=local name + final Pattern keyRx = Pattern.compile("^/([^\\|]+)\\|([^ ]*) +(.+)$"); //$NON-NLS-1$ + final FileAndLine docInfo = xmlFileAndLine(doc); + + nextAttribute: for (Entry<String, String> entry : attributeMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key == null || key.isEmpty()) { + continue; + } + + Matcher m = keyRx.matcher(key); + if (!m.matches()) { + log.error(Severity.WARNING, docInfo, "Invalid injected attribute key: %s", key); + continue; + } + String path = m.group(1); + String attrNsUri = m.group(2); + String attrName = m.group(3); + + String[] segment = path.split(Pattern.quote("/")); //$NON-NLS-1$ + + // Get the path elements. Create them as needed if they don't exist. + Node element = doc; + nextSegment: for (int i = 0; i < segment.length; i++) { + // Find a child with the segment's name + String name = segment[i]; + for (Node child = element.getFirstChild(); + child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + child.getNamespaceURI() == null && + child.getNodeName().equals(name)) { + // Found it. Continue to the next inner segment. + element = child; + continue nextSegment; + } + } + // No such element. Create it. + if (value == null) { + // If value is null, we want to remove, not create and if can't find the + // element, then we're done: there's no such attribute to remove. + break nextAttribute; + } + + Element child = doc.createElement(name); + element = element.insertBefore(child, element.getFirstChild()); + } + + if (element == null) { + log.error(Severity.WARNING, docInfo, "Invalid injected attribute path: %s", path); + return; + } + + NamedNodeMap attrs = element.getAttributes(); + if (attrs != null) { + + + if (attrNsUri != null && attrNsUri.isEmpty()) { + attrNsUri = null; + } + Node attr = attrs.getNamedItemNS(attrNsUri, attrName); + + if (value == null) { + // We want to remove the attribute from the attribute map. + if (attr != null) { + attrs.removeNamedItemNS(attrNsUri, attrName); + } + + } else { + // We want to add or replace the attribute. + if (attr == null) { + attr = doc.createAttributeNS(attrNsUri, attrName); + attr.setPrefix( + com.android.utils.XmlUtils.lookupNamespacePrefix(element, attrNsUri)); + attrs.setNamedItemNS(attr); + } + attr.setNodeValue(value); + } + } + } + } + // ------- /** @@ -534,7 +657,7 @@ class XmlUtils { sb.append(node.getLocalName()); printAttributes(sb, node, nsPrefix, prefix); sb.append(">\n"); //$NON-NLS-1$ - printChildren(sb, node.getFirstChild(), true, nsPrefix, prefix + " "); //$NON-NLS-1$ + printChildren(sb, node.getFirstChild(), true, nsPrefix, prefix + " "); //$NON-NLS-1$ sb.append(prefix).append("</"); //$NON-NLS-1$ if (uri != null) { diff --git a/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java index 564fc6d..63b76b8 100755 --- a/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java +++ b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java @@ -46,6 +46,14 @@ public class ManifestMergerTest extends ManifestMergerTestCase { processTestFiles(); } + public void test03_inject_attributes() throws Exception { + processTestFiles(); + } + + public void test04_inject_attributes() throws Exception { + processTestFiles(); + } + public void test10_activity_merge() throws Exception { processTestFiles(); } diff --git a/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java index 8fca091..b2cdfab 100755 --- a/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java +++ b/manifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java @@ -31,7 +31,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import junit.framework.TestCase; @@ -59,6 +61,10 @@ abstract class ManifestMergerTestCase extends TestCase { */ private static final String DELIM_MAIN = "main"; /** + * Delimiter that starts the inject attribute section. + */ + private static final String DELIM_INJECT_ATTR = "inject"; + /** * Delimiter that starts the resulting XML content, whatever is generated by the merge. */ private static final String DELIM_RESULT = "result"; @@ -72,6 +78,7 @@ abstract class ManifestMergerTestCase extends TestCase { static class TestFiles { private final File mMain; private final File[] mLibs; + private final Map<String, String> mInjectAttributes; private final File mActualResult; private final String mExpectedResult; private final String mExpectedErrors; @@ -82,12 +89,14 @@ abstract class ManifestMergerTestCase extends TestCase { boolean shouldFail, @NonNull File main, @NonNull File[] libs, + @NonNull Map<String, String> injectAttributes, @NonNull File actualResult, @NonNull String expectedResult, @NonNull String expectedErrors) { mShouldFail = shouldFail; mMain = main; mLibs = libs; + mInjectAttributes = injectAttributes; mActualResult = actualResult; mExpectedResult = expectedResult; mExpectedErrors = expectedErrors; @@ -107,6 +116,10 @@ abstract class ManifestMergerTestCase extends TestCase { return mLibs; } + public Map<String, String> getInjectAttributes() { + return mInjectAttributes; + } + @NonNull public File getActualResult() { return mActualResult; @@ -230,6 +243,7 @@ abstract class ManifestMergerTestCase extends TestCase { boolean skipEmpty = true; boolean shouldFail = false; + Map<String, String> injectAttributes = new HashMap<String, String>(); StringBuilder expectedResult = new StringBuilder(); StringBuilder expectedErrors = new StringBuilder(); File mainFile = null; @@ -249,10 +263,11 @@ abstract class ManifestMergerTestCase extends TestCase { assertTrue( "Unknown delimiter @" + delimiter + " in " + filename, delimiter.startsWith(DELIM_LIB) || - delimiter.equals(DELIM_MAIN) || - delimiter.equals(DELIM_RESULT) || - delimiter.equals(DELIM_ERRORS) || - delimiter.equals(DELIM_FAILS)); + delimiter.equals(DELIM_MAIN) || + delimiter.equals(DELIM_RESULT) || + delimiter.equals(DELIM_ERRORS) || + delimiter.equals(DELIM_FAILS) || + delimiter.equals(DELIM_INJECT_ATTR)); skipEmpty = true; @@ -266,7 +281,8 @@ abstract class ManifestMergerTestCase extends TestCase { if (delimiter.equals(DELIM_FAILS)) { shouldFail = true; - } else if (!delimiter.equals(DELIM_ERRORS)) { + } else if (!delimiter.equals(DELIM_ERRORS) && + !delimiter.equals(DELIM_INJECT_ATTR)) { tempFile = new File(tempDir, String.format("%1$s%2$d_%3$s.xml", this.getClass().getSimpleName(), tempIndex++, @@ -309,6 +325,11 @@ abstract class ManifestMergerTestCase extends TestCase { expectedResult.append(line).append('\n'); } else if (DELIM_ERRORS.equals(delimiter)) { expectedErrors.append(line).append('\n'); + } else if (DELIM_INJECT_ATTR.equals(delimiter)) { + String[] in = line.split("="); + if (in != null && in.length == 2) { + injectAttributes.put(in[0], "null".equals(in[1]) ? null : in[1]); + } } } @@ -322,6 +343,7 @@ abstract class ManifestMergerTestCase extends TestCase { shouldFail, mainFile, libFiles.toArray(new File[libFiles.size()]), + injectAttributes, actualResultFile, expectedResult.toString(), expectedErrors.toString()); @@ -362,7 +384,7 @@ abstract class ManifestMergerTestCase extends TestCase { /** * Processes the data from the given {@link TestFiles} by - * invoking {@link ManifestMerger#process(File, File, File[])}: + * invoking {@link ManifestMerger#process(File, File, File[], Map)}: * the given library files are applied consecutively to the main XML * document and the output is generated. * <p/> @@ -390,7 +412,8 @@ abstract class ManifestMergerTestCase extends TestCase { }); boolean processOK = merger.process(testFiles.getActualResult(), testFiles.getMain(), - testFiles.getLibs()); + testFiles.getLibs(), + testFiles.getInjectAttributes()); String expectedErrors = testFiles.getExpectedErrors().trim(); StringBuilder actualErrors = new StringBuilder(); diff --git a/manifmerger/tests/src/com/android/manifmerger/data/03_inject_attributes.xml b/manifmerger/tests/src/com/android/manifmerger/data/03_inject_attributes.xml new file mode 100755 index 0000000..0a7057c --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/03_inject_attributes.xml @@ -0,0 +1,53 @@ +# +# Test: +# - Inject attributes in a main manifest. +# + +@inject +/manifest|http://schemas.android.com/apk/res/android versionCode=101 +/manifest|http://schemas.android.com/apk/res/android versionName=1.0.1 +/manifest/uses-sdk|http://schemas.android.com/apk/res/android minSdkVersion=10 +/manifest/uses-sdk|http://schemas.android.com/apk/res/android targetSdkVersion=14 +/manifest/application|http://schemas.android.com/apk/res/android label=null +/manifest/application|http://schemas.android.com/apk/res/android icon=null + +@main + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.app1" + android:versionCode="100" + android:versionName="1.0.0"> + + <application + android:label="@string/app_name" + android:icon="@drawable/app_icon" + android:backupAgent="com.example.app.BackupAgentClass" + android:restoreAnyVersion="true" + android:allowBackup="true" + android:killAfterRestore="true" + android:name="com.example.TheApp" > + </application> + +</manifest> + +@result + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.app1" + android:versionCode="101" + android:versionName="1.0.1"><uses-sdk android:minSdkVersion="10" android:targetSdkVersion="14"/> + + <application + android:backupAgent="com.example.app.BackupAgentClass" + android:restoreAnyVersion="true" + android:allowBackup="true" + android:killAfterRestore="true" + android:name="com.example.TheApp" > + </application> + +</manifest> + +@errors + diff --git a/manifmerger/tests/src/com/android/manifmerger/data/04_inject_attributes.xml b/manifmerger/tests/src/com/android/manifmerger/data/04_inject_attributes.xml new file mode 100755 index 0000000..57f4e84 --- /dev/null +++ b/manifmerger/tests/src/com/android/manifmerger/data/04_inject_attributes.xml @@ -0,0 +1,63 @@ +# +# Test: +# - Inject attributes in a main manifest. +# The attributes are injected and then the merge is done. In this case the app +# starts with a minSdkVersion of 20, which is higher than the lib1's 15 value. +# However the injection replaces it by 10, which is now lower than the lib's +# version and thus a warning will be generated. +# + +@fails + +@inject +/manifest/uses-sdk|http://schemas.android.com/apk/res/android minSdkVersion=10 +/manifest/uses-sdk|http://schemas.android.com/apk/res/android targetSdkVersion=14 +/manifest/application|http://schemas.android.com/apk/res/android label=null +/manifest/application|http://schemas.android.com/apk/res/android icon=null + +@main + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.app1" + android:versionCode="100" + android:versionName="1.0.0"> + + <uses-sdk android:minSdkVersion="20" android:targetSdkVersion="21"/> + + <application android:name="com.example.TheApp" /> + +</manifest> + +@lib1 + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.app1" + android:versionCode="100" + android:versionName="1.0.0"> + + <uses-sdk android:minSdkVersion="15" android:targetSdkVersion="16"/> + + <application android:name="com.example.TheApp" /> + +</manifest> + +@result + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.app1" + android:versionCode="100" + android:versionName="1.0.0"> + + <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="14"/> + + <application android:name="com.example.TheApp" /> + +</manifest> + +@errors + +E [ManifestMergerTest0_main.xml:3, ManifestMergerTest1_lib1.xml:3] Main manifest has <uses-sdk android:minSdkVersion='10'> but library uses minSdkVersion='15' +W [ManifestMergerTest0_main.xml:3, ManifestMergerTest1_lib1.xml:3] Main manifest has <uses-sdk android:targetSdkVersion='14'> but library uses targetSdkVersion='16' |