OSDN Git Service

original
[gb-231r1-is01/GB_2.3_IS01.git] / sdk / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / common / resources / platform / AttrsXmlParser.java
diff --git a/sdk/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java b/sdk/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttrsXmlParser.java
new file mode 100644 (file)
index 0000000..bd79e29
--- /dev/null
@@ -0,0 +1,627 @@
+/*
+ * 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 com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.log.ILogger;
+import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
+
+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.HashMap;
+import java.util.Map;
+import java.util.TreeSet;
+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 HashMap<String, AttributeInfo> mAttributeMap;
+
+    /** Map of all attribute names for a given element */
+    private final Map<String, DeclareStyleableInfo> mStyleMap =
+        new HashMap<String, DeclareStyleableInfo>();
+
+    /**
+     * 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.
+     */
+    public AttrsXmlParser(String osAttrsXmlPath, ILogger log) {
+        this(osAttrsXmlPath, null /* inheritableAttributes */, log);
+    }
+
+    /**
+     * 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.
+     */
+    public AttrsXmlParser(
+            String osAttrsXmlPath,
+            AttrsXmlParser inheritableAttributes,
+            ILogger log) {
+        mOsAttrsXmlPath = osAttrsXmlPath;
+        mLog = log;
+
+        assert osAttrsXmlPath != null;
+        assert log != null;
+
+        if (inheritableAttributes == null) {
+            mAttributeMap = new HashMap<String, AttributeInfo>();
+            mEnumFlagValues = new HashMap<String, Map<String,Integer>>();
+        } else {
+            mAttributeMap = new HashMap<String, AttributeInfo>(inheritableAttributes.mAttributeMap);
+            mEnumFlagValues = new HashMap<String, Map<String,Integer>>(
+                                                             inheritableAttributes.mEnumFlagValues);
+        }
+    }
+
+    /**
+     * 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) {
+                info.setAttributes(style.getAttributes());
+                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".
+            String xmlName = String.format("%1$s_%2$s", //$NON-NLS-1$
+                    info.getViewLayoutClass().getShortClassName(),
+                    info.getShortClassName());
+            xmlName = xmlName.replaceFirst("Params$", ""); //$NON-NLS-1$ //$NON-NLS-2$
+
+            DeclareStyleableInfo style = mStyleMap.get(xmlName);
+            if (style != null) {
+                info.setAttributes(style.getAttributes());
+            }
+        }
+    }
+
+    /**
+     * 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 &lt;declare-styleable&gt; and &lt;attr&gt; nodes
+     * in the top &lt;resources&gt; 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);  //$NON-NLS-1$
+                            }
+                            mStyleMap.put(name, style);
+                            unknownParents.remove(name);
+                            if (lastComment != null) {
+                                style.setJavaDoc(parseJavadoc(lastComment.getNodeValue()));
+                            }
+                        }
+                    }
+                } 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 &lt;declare-styleable&gt;.
+     * <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 &lt;attr&gt; 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().length == 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) {
+                        info.setJavaDoc(parseJavadoc(lastComment.getNodeValue()));
+                        info.setDeprecatedDoc(parseDeprecatedDoc(lastComment.getNodeValue()));
+                    }
+                }
+            }
+        }
+        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 implicitely 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) {
+        TreeSet<AttributeInfo.Format> formats = new TreeSet<AttributeInfo.Format>();
+        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$
+                try {
+                    Format format = AttributeInfo.Format.valueOf(f.toUpperCase());
+                    // enum and flags are handled differently right below
+                    if (format != null &&
+                            format != AttributeInfo.Format.ENUM &&
+                            format != AttributeInfo.Format.FLAG) {
+                        formats.add(format);
+                    }
+                } catch (IllegalArgumentException e) {
+                    mLog.error(e,
+                        "Unknown format name '%s' in <attr name=\"%s\">, file '%s'.", //$NON-NLS-1$
+                        f, name, getOsAttrsXmlPath());
+                }
+            }
+        }
+
+        // does this <attr> have <enum> children?
+        enumValues = parseEnumFlagValues(attrNode, "enum", name); //$NON-NLS-1$
+        if (enumValues != null) {
+            formats.add(AttributeInfo.Format.ENUM);
+        }
+
+        // does this <attr> have <flag> children?
+        flagValues = parseEnumFlagValues(attrNode, "flag", name); //$NON-NLS-1$
+        if (flagValues != null) {
+            formats.add(AttributeInfo.Format.FLAG);
+        }
+
+        AttributeInfo info = new AttributeInfo(name,
+                formats.toArray(new AttributeInfo.Format[formats.size()]));
+        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 {
+                            int i = value.startsWith("0x") ?
+                                    Integer.parseInt(value.substring(2), 16 /* radix */) :
+                                    Integer.parseInt(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();
+    }
+}