diff options
author | Tor Norbye <tnorbye@google.com> | 2012-05-20 12:32:34 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-05-20 12:34:27 -0700 |
commit | a881b0b34678ad76c9f5eba62fac7a00a22ac606 (patch) | |
tree | 5329da2d31cd25796a754d0821b6362d119b30dc /common/src/com | |
parent | 78a3b351ffb925f3fdcc8efad0eaaed7b8e58a11 (diff) | |
download | sdk-a881b0b34678ad76c9f5eba62fac7a00a22ac606.zip sdk-a881b0b34678ad76c9f5eba62fac7a00a22ac606.tar.gz sdk-a881b0b34678ad76c9f5eba62fac7a00a22ac606.tar.bz2 |
Move XML code to the common library
The ManifestMerger library needs to look up the prefix to use for the
Android namespace, and the Document.lookupPrefix method is not
implemented by the Eclipse DOM implementation (which throws an
exception). However, we have an implementation of this in the ADT
plugin.
This changeset creates a new XmlUtils class in the common/ library
(which is accessible by both ADT and the manifest merger, and the
anttasks where the manifest merger is used), and moves the namespace
prefix lookup code in there. It also moves the XML escape methods
into that class. It also adds a new method to the ManifestMerger for
merging directly from documents (rather than files), and makes sure
that all the merging code goes via the prefix utility method rather
than calling the document.lookupPrefix method.
Finally, it moves the various string constants associated with XML
namespaces into the single XmlUtils class, since these were spread
across several different classes before (and many of them are needed
in the XmlUtils class).
The vast majority of the diffs in this changeset are related to simple
import statement changes to reflect the new locations of these
constants.
Change-Id: Ib8f3d0e5c89e47e61ea509a23925af7b6580abee
Diffstat (limited to 'common/src/com')
-rw-r--r-- | common/src/com/android/util/XmlUtils.java | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/common/src/com/android/util/XmlUtils.java b/common/src/com/android/util/XmlUtils.java new file mode 100644 index 0000000..11db031 --- /dev/null +++ b/common/src/com/android/util/XmlUtils.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2012 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.util; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.HashSet; + +/** XML Utilities */ +public class XmlUtils { + /** Namespace used in XML files for Android attributes */ + public static final String ANDROID_URI = + "http://schemas.android.com/apk/res/android"; //$NON-NLS-1$ + /** Namespace used in XML files for Android Tooling attributes */ + public static final String TOOLS_URI = + "http://schemas.android.com/tools"; //$NON-NLS-1$ + /** URI of the reserved "xmlns" prefix */ + public final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; //$NON-NLS-1$ + /** The "xmlns" attribute name */ + public static final String XMLNS = "xmlns"; //$NON-NLS-1$ + /** The default prefix used for the {@link #XMLNS_URI} */ + public static final String XMLNS_PREFIX = "xmlns:"; //$NON-NLS-1$ + /** Qualified name of the xmlns android declaration element */ + public static final String XMLNS_ANDROID = "xmlns:android"; //$NON-NLS-1$ + /** The default prefix used for the {@link #ANDROID_URI} name space */ + public static final String ANDROID_NS_NAME = "android"; //$NON-NLS-1$ + /** The default prefix used for the {@link #ANDROID_URI} name space including the colon */ + public static final String ANDROID_NS_NAME_PREFIX = "android:"; //$NON-NLS-1$ + /** The default prefix used for the app */ + private static final String APP_PREFIX = "app"; //$NON-NLS-1$ + /** The "xmlns:" attribute prefix used for namespace declarations */ + public static final String XMLNS_COLON = "xmlns:"; //$NON-NLS-1$ + /** The entity for the ampersand character */ + public static final String AMP_ENTITY = "&"; //$NON-NLS-1$ + /** The entity for the quote character */ + public static final String QUOT_ENTITY = """; //$NON-NLS-1$ + /** The entity for the apostrophe character */ + public static final String APOS_ENTITY = "'"; //$NON-NLS-1$ + /** The entity for the less than character */ + public static final String LT_ENTITY = "<"; //$NON-NLS-1$ + /** The entity for the greater than character */ + public static final String GT_ENTITY = ">"; //$NON-NLS-1$ + + /** + * Returns the namespace prefix matching the requested namespace URI. + * If no such declaration is found, returns the default "android" prefix for + * the Android URI, and "app" for other URI's. + * + * @param node The current node. Must not be null. + * @param nsUri The namespace URI of which the prefix is to be found, + * e.g. SdkConstants.NS_RESOURCES + * @return The first prefix declared or the default "android" prefix + * (or "app" for non-Android URIs) + */ + @NonNull + public static String lookupNamespacePrefix(@NonNull Node node, @NonNull String nsUri) { + String defaultPrefix = ANDROID_URI.equals(nsUri) ? ANDROID_NS_NAME : APP_PREFIX; + return lookupNamespacePrefix(node, nsUri, defaultPrefix); + } + + /** + * Returns the namespace prefix matching the requested namespace URI. + * If no such declaration is found, returns the default "android" prefix. + * + * @param node The current node. Must not be null. + * @param nsUri The namespace URI of which the prefix is to be found, + * e.g. SdkConstants.NS_RESOURCES + * @param defaultPrefix The default prefix (root) to use if the namespace + * is not found. If null, do not create a new namespace + * if this URI is not defined for the document. + * @return The first prefix declared or the provided prefix (possibly with + * a number appended to avoid conflicts with existing prefixes. + */ + public static String lookupNamespacePrefix( + @Nullable Node node, @Nullable String nsUri, @Nullable String defaultPrefix) { + // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java + // The following code emulates this simple call: + // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES); + + // if the requested URI is null, it denotes an attribute with no namespace. + if (nsUri == null) { + return null; + } + + // per XML specification, the "xmlns" URI is reserved + if (XMLNS_URI.equals(nsUri)) { + return XMLNS; + } + + HashSet<String> visited = new HashSet<String>(); + Document doc = node == null ? null : node.getOwnerDocument(); + + // Ask the document about it. This method may not be implemented by the Document. + String nsPrefix = null; + try { + nsPrefix = doc != null ? doc.lookupPrefix(nsUri) : null; + if (nsPrefix != null) { + return nsPrefix; + } + } catch (Throwable t) { + // ignore + } + + // If that failed, try to look it up manually. + // This also gathers prefixed in use in the case we want to generate a new one below. + for (; node != null && node.getNodeType() == Node.ELEMENT_NODE; + node = node.getParentNode()) { + NamedNodeMap attrs = node.getAttributes(); + for (int n = attrs.getLength() - 1; n >= 0; --n) { + Node attr = attrs.item(n); + if (XMLNS.equals(attr.getPrefix())) { + String uri = attr.getNodeValue(); + nsPrefix = attr.getLocalName(); + // Is this the URI we are looking for? If yes, we found its prefix. + if (nsUri.equals(uri)) { + return nsPrefix; + } + visited.add(nsPrefix); + } + } + } + + // Failed the find a prefix. Generate a new sensible default prefix, unless + // defaultPrefix was null in which case the caller does not want the document + // modified. + if (defaultPrefix == null) { + return null; + } + + // + // We need to make sure the prefix is not one that was declared in the scope + // visited above. Pick a unique prefix from the provided default prefix. + String prefix = defaultPrefix; + String base = prefix; + for (int i = 1; visited.contains(prefix); i++) { + prefix = base + Integer.toString(i); + } + // Also create & define this prefix/URI in the XML document as an attribute in the + // first element of the document. + if (doc != null) { + node = doc.getFirstChild(); + while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { + node = node.getNextSibling(); + } + if (node != null) { + // This doesn't work: + //Attr attr = doc.createAttributeNS(XMLNS_URI, prefix); + //attr.setPrefix(XMLNS); + // + // Xerces throws + //org.w3c.dom.DOMException: NAMESPACE_ERR: An attempt is made to create or + // change an object in a way which is incorrect with regard to namespaces. + // + // Instead pass in the concatenated prefix. (This is covered by + // the UiElementNodeTest#testCreateNameSpace() test.) + Attr attr = doc.createAttributeNS(XMLNS_URI, XMLNS_PREFIX + prefix); + attr.setValue(nsUri); + node.getAttributes().setNamedItemNS(attr); + } + } + + return prefix; + } + + /** + * Converts the given attribute value to an XML-attribute-safe value, meaning that + * single and double quotes are replaced with their corresponding XML entities. + * + * @param attrValue the value to be escaped + * @return the escaped value + */ + @NonNull + public static String toXmlAttributeValue(@NonNull String attrValue) { + for (int i = 0, n = attrValue.length(); i < n; i++) { + char c = attrValue.charAt(i); + if (c == '"' || c == '\'' || c == '<' || c == '&') { + StringBuilder sb = new StringBuilder(2 * attrValue.length()); + appendXmlAttributeValue(sb, attrValue); + return sb.toString(); + } + } + + return attrValue; + } + + /** + * Appends text to the given {@link StringBuilder} and escapes it as required for a + * DOM attribute node. + * + * @param sb the string builder + * @param attrValue the attribute value to be appended and escaped + */ + public static void appendXmlAttributeValue(@NonNull StringBuilder sb, + @NonNull String attrValue) { + int n = attrValue.length(); + // &, ", ' and < are illegal in attributes; see http://www.w3.org/TR/REC-xml/#NT-AttValue + // (' legal in a " string and " is legal in a ' string but here we'll stay on the safe + // side) + for (int i = 0; i < n; i++) { + char c = attrValue.charAt(i); + if (c == '"') { + sb.append(QUOT_ENTITY); + } else if (c == '<') { + sb.append(LT_ENTITY); + } else if (c == '\'') { + sb.append(APOS_ENTITY); + } else if (c == '&') { + sb.append(AMP_ENTITY); + } else { + sb.append(c); + } + } + } + + /** + * Appends text to the given {@link StringBuilder} and escapes it as required for a + * DOM text node. + * + * @param sb the string builder + * @param textValue the text value to be appended and escaped + */ + public static void appendXmlTextValue(@NonNull StringBuilder sb, @NonNull String textValue) { + for (int i = 0, n = textValue.length(); i < n; i++) { + char c = textValue.charAt(i); + if (c == '<') { + sb.append(LT_ENTITY); + } else if (c == '&') { + sb.append(AMP_ENTITY); + } else { + sb.append(c); + } + } + } +} |