blob: 5eb564358b19af2f65300d1b280df790119661e3 [file] [log] [blame]
/*
* 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.common.utils;
import static com.android.common.SdkConstants.AMP_ENTITY;
import static com.android.common.SdkConstants.ANDROID_NS_NAME;
import static com.android.common.SdkConstants.ANDROID_URI;
import static com.android.common.SdkConstants.APOS_ENTITY;
import static com.android.common.SdkConstants.APP_PREFIX;
import static com.android.common.SdkConstants.LT_ENTITY;
import static com.android.common.SdkConstants.QUOT_ENTITY;
import static com.android.common.SdkConstants.XMLNS;
import static com.android.common.SdkConstants.XMLNS_PREFIX;
import static com.android.common.SdkConstants.XMLNS_URI;
import com.android.common.SdkConstants;
import com.android.common.annotations.NonNull;
import com.android.common.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 {
/**
* 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. {@link SdkConstants#ANDROID_URI}
* @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. {@link SdkConstants#ANDROID_URI}
* @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(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;
}
/**
* Converts the given attribute value to an XML-text-safe value, meaning that
* less than and ampersand characters are escaped.
*
* @param textValue the text value to be escaped
* @return the escaped value
*/
@NonNull
public static String toXmlTextValue(@NonNull String textValue) {
for (int i = 0, n = textValue.length(); i < n; i++) {
char c = textValue.charAt(i);
if (c == '<' || c == '&') {
StringBuilder sb = new StringBuilder(2 * textValue.length());
appendXmlTextValue(sb, textValue);
return sb.toString();
}
}
return textValue;
}
/**
* 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);
}
}
}
}