diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java | 698 |
1 files changed, 698 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java new file mode 100644 index 000000000..1330c50f3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java @@ -0,0 +1,698 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.common.resources.platform; + +import static com.android.SdkConstants.DOT_LAYOUT_PARAMS; +import static com.android.ide.eclipse.adt.AdtConstants.DOC_HIDE; + +import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.utils.ILogger; +import com.google.common.collect.Maps; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * Parser for attributes description files. + */ +public final class AttrsXmlParser { + + public static final String ANDROID_MANIFEST_STYLEABLE = "AndroidManifest"; //$NON-NLS-1$ + + private Document mDocument; + private String mOsAttrsXmlPath; + + // all attributes that have the same name are supposed to have the same + // parameters so we'll keep a cache of them to avoid processing them twice. + private Map<String, AttributeInfo> mAttributeMap; + + /** Map of all attribute names for a given element */ + private final Map<String, DeclareStyleableInfo> mStyleMap = + new HashMap<String, DeclareStyleableInfo>(); + + /** Map from format name (lower case) to the uppercase version */ + private Map<String, Format> mFormatNames = new HashMap<String, Format>(10); + + /** + * Map of all (constant, value) pairs for attributes of format enum or flag. + * E.g. for attribute name=gravity, this tells us there's an enum/flag called "center" + * with value 0x11. + */ + private Map<String, Map<String, Integer>> mEnumFlagValues; + + /** + * A logger object. Must not be null. + */ + private final ILogger mLog; + + /** + * Creates a new {@link AttrsXmlParser}, set to load things from the given + * XML file. Nothing has been parsed yet. Callers should call {@link #preload()} + * next. + * + * @param osAttrsXmlPath The path of the <code>attrs.xml</code> file to parse. + * Must not be null. Should point to an existing valid XML document. + * @param log A logger object. Must not be null. + * @param expectedAttributeCount expected number of attributes in the file + */ + public AttrsXmlParser(String osAttrsXmlPath, ILogger log, int expectedAttributeCount) { + this(osAttrsXmlPath, null /* inheritableAttributes */, log, expectedAttributeCount); + } + + /** + * Returns the parsed map of attribute infos + * + * @return a map from string name to {@link AttributeInfo} + */ + public Map<String, AttributeInfo> getAttributeMap() { + return mAttributeMap; + } + + /** + * Creates a new {@link AttrsXmlParser} set to load things from the given + * XML file. + * <p/> + * If inheritableAttributes is non-null, it must point to a preloaded + * {@link AttrsXmlParser} which attributes will be used for this one. Since + * already defined attributes are not modifiable, they are thus "inherited". + * + * @param osAttrsXmlPath The path of the <code>attrs.xml</code> file to parse. + * Must not be null. Should point to an existing valid XML document. + * @param inheritableAttributes An optional parser with attributes to inherit. Can be null. + * If not null, the parser must have had its {@link #preload()} method + * invoked prior to being used here. + * @param log A logger object. Must not be null. + * @param expectedAttributeCount expected number of attributes in the file + */ + public AttrsXmlParser( + String osAttrsXmlPath, + AttrsXmlParser inheritableAttributes, + ILogger log, + int expectedAttributeCount) { + mOsAttrsXmlPath = osAttrsXmlPath; + mLog = log; + + assert osAttrsXmlPath != null; + assert log != null; + + mAttributeMap = Maps.newHashMapWithExpectedSize(expectedAttributeCount); + if (inheritableAttributes == null) { + mEnumFlagValues = new HashMap<String, Map<String,Integer>>(); + } else { + mAttributeMap.putAll(inheritableAttributes.mAttributeMap); + mEnumFlagValues = new HashMap<String, Map<String,Integer>>( + inheritableAttributes.mEnumFlagValues); + } + + // Pre-compute the set of format names such that we don't have to compute the uppercase + // version of the same format string names again and again + for (Format f : Format.values()) { + mFormatNames.put(f.name().toLowerCase(Locale.US), f); + } + } + + /** + * Returns the OS path of the attrs.xml file parsed. + */ + public String getOsAttrsXmlPath() { + return mOsAttrsXmlPath; + } + + /** + * Preloads the document, parsing all attributes and declared styles. + * + * @return Self, for command chaining. + */ + public AttrsXmlParser preload() { + Document doc = getDocument(); + + if (doc == null) { + mLog.warning("Failed to find %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + return this; + } + + Node res = doc.getFirstChild(); + while (res != null && + res.getNodeType() != Node.ELEMENT_NODE && + !res.getNodeName().equals("resources")) { //$NON-NLS-1$ + res = res.getNextSibling(); + } + + if (res == null) { + mLog.warning("Failed to find a <resources> node in %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + return this; + } + + parseResources(res); + return this; + } + + /** + * Loads all attributes & javadoc for the view class info based on the class name. + */ + public void loadViewAttributes(ViewClassInfo info) { + if (getDocument() != null) { + String xmlName = info.getShortClassName(); + DeclareStyleableInfo style = mStyleMap.get(xmlName); + if (style != null) { + String definedBy = info.getFullClassName(); + AttributeInfo[] attributes = style.getAttributes(); + for (AttributeInfo attribute : attributes) { + if (attribute.getDefinedBy() == null) { + attribute.setDefinedBy(definedBy); + } + } + info.setAttributes(attributes); + info.setJavaDoc(style.getJavaDoc()); + } + } + } + + /** + * Loads all attributes for the layout data info based on the class name. + */ + public void loadLayoutParamsAttributes(LayoutParamsInfo info) { + if (getDocument() != null) { + // Transforms "LinearLayout" and "LayoutParams" into "LinearLayout_Layout". + ViewClassInfo viewLayoutClass = info.getViewLayoutClass(); + String xmlName = String.format("%1$s_%2$s", //$NON-NLS-1$ + viewLayoutClass.getShortClassName(), + info.getShortClassName()); + xmlName = AdtUtils.stripSuffix(xmlName, "Params"); //$NON-NLS-1$ + + DeclareStyleableInfo style = mStyleMap.get(xmlName); + if (style != null) { + // For defined by, use the actual class name, e.g. + // android.widget.LinearLayout.LayoutParams + String definedBy = viewLayoutClass.getFullClassName() + DOT_LAYOUT_PARAMS; + AttributeInfo[] attributes = style.getAttributes(); + for (AttributeInfo attribute : attributes) { + if (attribute.getDefinedBy() == null) { + attribute.setDefinedBy(definedBy); + } + } + info.setAttributes(attributes); + } + } + } + + /** + * Returns a list of all <code>declare-styleable</code> found in the XML file. + */ + public Map<String, DeclareStyleableInfo> getDeclareStyleableList() { + return Collections.unmodifiableMap(mStyleMap); + } + + /** + * Returns a map of all enum and flag constants sorted by parent attribute name. + * The map is attribute_name => (constant_name => integer_value). + */ + public Map<String, Map<String, Integer>> getEnumFlagValues() { + return mEnumFlagValues; + } + + //------------------------- + + /** + * Creates an XML document from the attrs.xml OS path. + * May return null if the file doesn't exist or cannot be parsed. + */ + private Document getDocument() { + if (mDocument == null) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + mDocument = builder.parse(new File(mOsAttrsXmlPath)); + } catch (ParserConfigurationException e) { + mLog.error(e, "Failed to create XML document builder for %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + } catch (SAXException e) { + mLog.error(e, "Failed to parse XML document %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + } catch (IOException e) { + mLog.error(e, "Failed to read XML document %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + } + } + return mDocument; + } + + /** + * Finds all the <declare-styleable> and <attr> nodes + * in the top <resources> node. + */ + private void parseResources(Node res) { + + Map<String, String> unknownParents = new HashMap<String, String>(); + + Node lastComment = null; + for (Node node = res.getFirstChild(); node != null; node = node.getNextSibling()) { + switch (node.getNodeType()) { + case Node.COMMENT_NODE: + lastComment = node; + break; + case Node.ELEMENT_NODE: + if (node.getNodeName().equals("declare-styleable")) { //$NON-NLS-1$ + Node nameNode = node.getAttributes().getNamedItem("name"); //$NON-NLS-1$ + if (nameNode != null) { + String name = nameNode.getNodeValue(); + + Node parentNode = node.getAttributes().getNamedItem("parent"); //$NON-NLS-1$ + String parents = parentNode == null ? null : parentNode.getNodeValue(); + + if (name != null && !mStyleMap.containsKey(name)) { + DeclareStyleableInfo style = parseDeclaredStyleable(name, node); + if (parents != null) { + String[] parentsArray = + parseStyleableParents(parents, mStyleMap, unknownParents); + style.setParents(parentsArray); + } + mStyleMap.put(name, style); + unknownParents.remove(name); + if (lastComment != null) { + String nodeValue = lastComment.getNodeValue(); + if (nodeValue.contains(DOC_HIDE)) { + mStyleMap.remove(name); + } else { + style.setJavaDoc(parseJavadoc(nodeValue)); + } + } + } + } + } else if (node.getNodeName().equals("attr")) { //$NON-NLS-1$ + parseAttr(node, lastComment); + } + lastComment = null; + break; + } + } + + // If we have any unknown parent, re-create synthetic styleable for them. + for (Entry<String, String> entry : unknownParents.entrySet()) { + String name = entry.getKey(); + String parent = entry.getValue(); + + DeclareStyleableInfo style = new DeclareStyleableInfo(name, (AttributeInfo[])null); + if (parent != null) { + style.setParents(new String[] { parent }); + } + mStyleMap.put(name, style); + + // Simplify parents names. See SDK Bug 3125910. + // Implementation detail: that since we want to delete and add to the map, + // we can't just use an iterator. + for (String key : new ArrayList<String>(mStyleMap.keySet())) { + if (key.startsWith(name) && !key.equals(name)) { + // We found a child which name starts with the full name of the + // parent. Simplify the children name. + String newName = ANDROID_MANIFEST_STYLEABLE + key.substring(name.length()); + + DeclareStyleableInfo newStyle = + new DeclareStyleableInfo(newName, mStyleMap.get(key)); + mStyleMap.remove(key); + mStyleMap.put(newName, newStyle); + } + } + } + } + + /** + * Parses the "parents" attribute from a <declare-styleable>. + * <p/> + * The syntax is the following: + * <pre> + * parent[.parent]* [[space|,] parent[.parent]* ] + * </pre> + * <p/> + * In English: </br> + * - There can be one or more parents, separated by whitespace or commas. </br> + * - Whitespace is ignored and trimmed. </br> + * - A parent name is actually composed of one or more identifiers joined by a dot. + * <p/> + * Styleables do not usually need to declare their parent chain (e.g. the grand-parents + * of a parent.) Parent names are unique, so in most cases a styleable will only declare + * its immediate parent. + * <p/> + * However it is possible for a styleable's parent to not exist, e.g. if you have a + * styleable "A" that is the root and then styleable "C" declares its parent to be "A.B". + * In this case we record "B" as the parent, even though it is unknown and will never be + * known. Any parent that is currently not in the knownParent map is thus added to the + * unknownParent set. The caller will remove the name from the unknownParent set when it + * sees a declaration for it. + * + * @param parents The parents string to parse. Must not be null or empty. + * @param knownParents The map of all declared styles known so far. + * @param unknownParents A map of all unknown parents collected here. + * @return The array of terminal parent names parsed from the parents string. + */ + private String[] parseStyleableParents(String parents, + Map<String, DeclareStyleableInfo> knownParents, + Map<String, String> unknownParents) { + + ArrayList<String> result = new ArrayList<String>(); + + for (String parent : parents.split("[ \t\n\r\f,|]")) { //$NON-NLS-1$ + parent = parent.trim(); + if (parent.length() == 0) { + continue; + } + if (parent.indexOf('.') >= 0) { + // This is a grand-parent/parent chain. Make sure we know about the + // parents and only record the terminal one. + String last = null; + for (String name : parent.split("\\.")) { //$NON-NLS-1$ + if (name.length() > 0) { + if (!knownParents.containsKey(name)) { + // Record this unknown parent and its grand parent. + unknownParents.put(name, last); + } + last = name; + } + } + parent = last; + } + + result.add(parent); + } + + return result.toArray(new String[result.size()]); + } + + /** + * Parses an <attr> node and convert it into an {@link AttributeInfo} if it is valid. + */ + private AttributeInfo parseAttr(Node attrNode, Node lastComment) { + AttributeInfo info = null; + Node nameNode = attrNode.getAttributes().getNamedItem("name"); //$NON-NLS-1$ + if (nameNode != null) { + String name = nameNode.getNodeValue(); + if (name != null) { + info = mAttributeMap.get(name); + // If the attribute is unknown yet, parse it. + // If the attribute is know but its format is unknown, parse it too. + if (info == null || info.getFormats().size() == 0) { + info = parseAttributeTypes(attrNode, name); + if (info != null) { + mAttributeMap.put(name, info); + } + } else if (lastComment != null) { + info = new AttributeInfo(info); + } + if (info != null) { + if (lastComment != null) { + String nodeValue = lastComment.getNodeValue(); + if (nodeValue.contains(DOC_HIDE)) { + return null; + } + info.setJavaDoc(parseJavadoc(nodeValue)); + info.setDeprecatedDoc(parseDeprecatedDoc(nodeValue)); + } + } + } + } + return info; + } + + /** + * Finds all the attributes for a particular style node, + * e.g. a declare-styleable of name "TextView" or "LinearLayout_Layout". + * + * @param styleName The name of the declare-styleable node + * @param declareStyleableNode The declare-styleable node itself + */ + private DeclareStyleableInfo parseDeclaredStyleable(String styleName, + Node declareStyleableNode) { + ArrayList<AttributeInfo> attrs = new ArrayList<AttributeInfo>(); + Node lastComment = null; + for (Node node = declareStyleableNode.getFirstChild(); + node != null; + node = node.getNextSibling()) { + + switch (node.getNodeType()) { + case Node.COMMENT_NODE: + lastComment = node; + break; + case Node.ELEMENT_NODE: + if (node.getNodeName().equals("attr")) { //$NON-NLS-1$ + AttributeInfo info = parseAttr(node, lastComment); + if (info != null) { + attrs.add(info); + } + } + lastComment = null; + break; + } + + } + + return new DeclareStyleableInfo(styleName, attrs.toArray(new AttributeInfo[attrs.size()])); + } + + /** + * Returns the {@link AttributeInfo} for a specific <attr> XML node. + * This gets the javadoc, the type, the name and the enum/flag values if any. + * <p/> + * The XML node is expected to have the following attributes: + * <ul> + * <li>"name", which is mandatory. The node is skipped if this is missing.</li> + * <li>"format".</li> + * </ul> + * The format may be one type or two types (e.g. "reference|color"). + * An extra format can be implied: "enum" or "flag" are not specified in the "format" attribute, + * they are implicitly stated by the presence of sub-nodes <enum> or <flag>. + * <p/> + * By design, attr nodes of the same name MUST have the same type. + * Attribute nodes are thus cached by name and reused as much as possible. + * When reusing a node, it is duplicated and its javadoc reassigned. + */ + private AttributeInfo parseAttributeTypes(Node attrNode, String name) { + EnumSet<Format> formats = null; + String[] enumValues = null; + String[] flagValues = null; + + Node attrFormat = attrNode.getAttributes().getNamedItem("format"); //$NON-NLS-1$ + if (attrFormat != null) { + for (String f : attrFormat.getNodeValue().split("\\|")) { //$NON-NLS-1$ + Format format = mFormatNames.get(f); + if (format == null) { + mLog.info( + "Unknown format name '%s' in <attr name=\"%s\">, file '%s'.", //$NON-NLS-1$ + f, name, getOsAttrsXmlPath()); + } else if (format != AttributeInfo.Format.ENUM && + format != AttributeInfo.Format.FLAG) { + if (formats == null) { + formats = format.asSet(); + } else { + if (formats.size() == 1) { + formats = EnumSet.copyOf(formats); + } + formats.add(format); + } + } + } + } + + // does this <attr> have <enum> children? + enumValues = parseEnumFlagValues(attrNode, "enum", name); //$NON-NLS-1$ + if (enumValues != null) { + if (formats == null) { + formats = Format.ENUM_SET; + } else { + if (formats.size() == 1) { + formats = EnumSet.copyOf(formats); + } + formats.add(Format.ENUM); + } + } + + // does this <attr> have <flag> children? + flagValues = parseEnumFlagValues(attrNode, "flag", name); //$NON-NLS-1$ + if (flagValues != null) { + if (formats == null) { + formats = Format.FLAG_SET; + } else { + if (formats.size() == 1) { + formats = EnumSet.copyOf(formats); + } + formats.add(Format.FLAG); + } + } + + if (formats == null) { + formats = Format.NONE; + } + + AttributeInfo info = new AttributeInfo(name, formats); + info.setEnumValues(enumValues); + info.setFlagValues(flagValues); + return info; + } + + /** + * Given an XML node that represents an <attr> node, this method searches + * if the node has any children nodes named "target" (e.g. "enum" or "flag"). + * Such nodes must have a "name" attribute. + * <p/> + * If "attrNode" is null, look for any <attr> that has the given attrNode + * and the requested children nodes. + * <p/> + * This method collects all the possible names of these children nodes and + * return them. + * + * @param attrNode The <attr> XML node + * @param filter The child node to look for, either "enum" or "flag". + * @param attrName The value of the name attribute of <attr> + * + * @return Null if there are no such children nodes, otherwise an array of length >= 1 + * of all the names of these children nodes. + */ + private String[] parseEnumFlagValues(Node attrNode, String filter, String attrName) { + ArrayList<String> names = null; + for (Node child = attrNode.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(filter)) { + Node nameNode = child.getAttributes().getNamedItem("name"); //$NON-NLS-1$ + if (nameNode == null) { + mLog.warning( + "Missing name attribute in <attr name=\"%s\"><%s></attr>", //$NON-NLS-1$ + attrName, filter); + } else { + if (names == null) { + names = new ArrayList<String>(); + } + String name = nameNode.getNodeValue(); + names.add(name); + + Node valueNode = child.getAttributes().getNamedItem("value"); //$NON-NLS-1$ + if (valueNode == null) { + mLog.warning( + "Missing value attribute in <attr name=\"%s\"><%s name=\"%s\"></attr>", //$NON-NLS-1$ + attrName, filter, name); + } else { + String value = valueNode.getNodeValue(); + try { + // Integer.decode cannot handle "ffffffff", see JDK issue 6624867 + int i = (int) (long) Long.decode(value); + + Map<String, Integer> map = mEnumFlagValues.get(attrName); + if (map == null) { + map = new HashMap<String, Integer>(); + mEnumFlagValues.put(attrName, map); + } + map.put(name, Integer.valueOf(i)); + + } catch(NumberFormatException e) { + mLog.error(e, + "Value in <attr name=\"%s\"><%s name=\"%s\" value=\"%s\"></attr> is not a valid decimal or hexadecimal", //$NON-NLS-1$ + attrName, filter, name, value); + } + } + } + } + } + return names == null ? null : names.toArray(new String[names.size()]); + } + + /** + * Parses the javadoc comment. + * Only keeps the first sentence. + * <p/> + * This does not remove nor simplify links and references. + */ + private String parseJavadoc(String comment) { + if (comment == null) { + return null; + } + + // sanitize & collapse whitespace + comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + // Explicitly remove any @deprecated tags since they are handled separately. + comment = comment.replaceAll("(?:\\{@deprecated[^}]*\\}|@deprecated[^@}]*)", ""); + + // take everything up to the first dot that is followed by a space or the end of the line. + // I love regexps :-). For the curious, the regexp is: + // - start of line + // - ignore whitespace + // - group: + // - everything, not greedy + // - non-capturing group (?: ) + // - end of string + // or + // - not preceded by a letter, a dot and another letter (for "i.e" and "e.g" ) + // (<! non-capturing zero-width negative look-behind) + // - a dot + // - followed by a space (?= non-capturing zero-width positive look-ahead) + // - anything else is ignored + comment = comment.replaceFirst("^\\s*(.*?(?:$|(?<![a-zA-Z]\\.[a-zA-Z])\\.(?=\\s))).*", "$1"); //$NON-NLS-1$ //$NON-NLS-2$ + + return comment; + } + + + /** + * Parses the javadoc and extract the first @deprecated tag, if any. + * Returns null if there's no @deprecated tag. + * The deprecated tag can be of two forms: + * - {+@deprecated ...text till the next bracket } + * Note: there should be no space or + between { and @. I need one in this comment otherwise + * this method will be tagged as deprecated ;-) + * - @deprecated ...text till the next @tag or end of the comment. + * In both cases the comment can be multi-line. + */ + private String parseDeprecatedDoc(String comment) { + // Skip if we can't even find the tag in the comment. + if (comment == null) { + return null; + } + + // sanitize & collapse whitespace + comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + int pos = comment.indexOf("{@deprecated"); + if (pos >= 0) { + comment = comment.substring(pos + 12 /* len of {@deprecated */); + comment = comment.replaceFirst("^([^}]*).*", "$1"); + } else if ((pos = comment.indexOf("@deprecated")) >= 0) { + comment = comment.substring(pos + 11 /* len of @deprecated */); + comment = comment.replaceFirst("^(.*?)(?:@.*|$)", "$1"); + } else { + return null; + } + + return comment.trim(); + } +} |