aboutsummaryrefslogtreecommitdiffstats
path: root/manifmerger
diff options
context:
space:
mode:
authorRaphael Moll <ralf@android.com>2012-09-28 15:29:21 -0700
committerRaphael Moll <ralf@android.com>2012-09-28 15:46:39 -0700
commit2b426ae27c0be8ebd26bc69ad59007e45487568b (patch)
tree6fb2982a35a1a9a0bd4457d27f00b4857f1ed7e1 /manifmerger
parentc439a89ccdcf7123f52cb528d57d5ef78881e0da (diff)
downloadsdk-2b426ae27c0be8ebd26bc69ad59007e45487568b.zip
sdk-2b426ae27c0be8ebd26bc69ad59007e45487568b.tar.gz
sdk-2b426ae27c0be8ebd26bc69ad59007e45487568b.tar.bz2
ManifestMerger: ability to inject attributes.
Change-Id: Icbebe1dd3c8cf51f7d38b585a78264d01977e943
Diffstat (limited to 'manifmerger')
-rw-r--r--manifmerger/src/com/android/manifmerger/Main.java7
-rwxr-xr-xmanifmerger/src/com/android/manifmerger/ManifestMerger.java32
-rwxr-xr-xmanifmerger/src/com/android/manifmerger/XmlUtils.java125
-rwxr-xr-xmanifmerger/tests/src/com/android/manifmerger/ManifestMergerTest.java8
-rwxr-xr-xmanifmerger/tests/src/com/android/manifmerger/ManifestMergerTestCase.java37
-rwxr-xr-xmanifmerger/tests/src/com/android/manifmerger/data/03_inject_attributes.xml53
-rwxr-xr-xmanifmerger/tests/src/com/android/manifmerger/data/04_inject_attributes.xml63
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'