| /* |
| * Copyright (C) 2013 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.tools.idea.rendering; |
| |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.ide.common.rendering.api.ILayoutPullParser; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.intellij.openapi.diagnostic.Logger; |
| import org.jetbrains.annotations.NotNull; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.AUTO_URI; |
| |
| /** |
| * Simple wrapper around an XML document which provides its contents as a pull parser. |
| * Most of this is based on the {@link com.android.tools.idea.rendering.LayoutPsiPullParser} but |
| * with DOM nodes instead of PSI elements as the data model |
| */ |
| public class DomPullParser extends LayoutPullParser { |
| @NotNull |
| private final List<Element> myNodeStack = new ArrayList<Element>(); |
| |
| @Nullable |
| private final Element myRoot; |
| |
| @Nullable |
| private Map<Element, ?> myViewCookies; |
| |
| /** |
| * Constructs a new {@link DomPullParser}, a parser which wraps an XML DOM and provides a pull parser interface |
| * |
| * @param root the root element |
| */ |
| public DomPullParser(@Nullable Element root) { |
| myRoot = root; |
| } |
| |
| /** Sets view cookies to be returned to the layout parser */ |
| public DomPullParser setViewCookies(@Nullable Map<Element, ?> viewCookies) { |
| myViewCookies = viewCookies; |
| return this; |
| } |
| |
| @VisibleForTesting |
| public Element getRoot() { |
| return myRoot; |
| } |
| |
| @Nullable |
| protected Element getCurrentElement() { |
| if (myNodeStack.size() > 0) { |
| return myNodeStack.get(myNodeStack.size() - 1); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| private Attr getAttribute(int i) { |
| if (myParsingState != START_TAG) { |
| throw new IndexOutOfBoundsException(); |
| } |
| |
| Element element = getCurrentElement(); |
| if (element != null) { |
| return (Attr)element.getAttributes().item(i); |
| } |
| |
| return null; |
| } |
| |
| private void push(@NotNull Element node) { |
| myNodeStack.add(node); |
| } |
| |
| @NotNull |
| private Element pop() { |
| return myNodeStack.remove(myNodeStack.size() - 1); |
| } |
| |
| // ------------- IXmlPullParser -------- |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * This implementation returns the underlying DOM node of type {@link Element}. |
| * Note that the link between the layout editor and the parsing code depends on this being the actual |
| * type returned, so you can't just randomly change it here. |
| */ |
| @Nullable |
| @Override |
| public Object getViewCookie() { |
| Element element = getCurrentElement(); |
| if (myViewCookies != null) { |
| return myViewCookies.get(element); |
| } |
| return element; |
| } |
| |
| /** |
| * Legacy method required by {@link com.android.layoutlib.api.IXmlPullParser} |
| */ |
| @SuppressWarnings("deprecation") |
| @Nullable |
| @Override |
| public Object getViewKey() { |
| return getViewCookie(); |
| } |
| |
| /** |
| * This implementation does nothing for now as all the embedded XML will use a normal KXML |
| * parser. |
| */ |
| @Nullable |
| @Override |
| public ILayoutPullParser getParser(String layoutName) { |
| return null; |
| } |
| |
| // ------------- XmlPullParser -------- |
| |
| @Override |
| public String getPositionDescription() { |
| return "XML DOM element depth:" + myNodeStack.size(); |
| } |
| |
| /* |
| * This does not seem to be called by the layoutlib, but we keep this (and maintain |
| * it) just in case. |
| */ |
| @Override |
| public int getAttributeCount() { |
| Element node = getCurrentElement(); |
| |
| if (node != null) { |
| return node.getAttributes().getLength(); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * This does not seem to be called by the layoutlib, but we keep this (and maintain |
| * it) just in case. |
| */ |
| @Nullable |
| @Override |
| public String getAttributeName(int i) { |
| Attr attribute = getAttribute(i); |
| if (attribute != null) { |
| String localName = attribute.getLocalName(); |
| if (localName == null) { |
| return attribute.getName(); |
| } |
| return localName; |
| } |
| |
| return null; |
| } |
| |
| /* |
| * This does not seem to be called by the layoutlib, but we keep this (and maintain |
| * it) just in case. |
| */ |
| @Override |
| public String getAttributeNamespace(int i) { |
| Attr attribute = getAttribute(i); |
| if (attribute != null) { |
| return attribute.getNamespaceURI(); |
| } |
| return ""; //$NON-NLS-1$ |
| } |
| |
| /* |
| * This does not seem to be called by the layoutlib, but we keep this (and maintain |
| * it) just in case. |
| */ |
| @Nullable |
| @Override |
| public String getAttributePrefix(int i) { |
| Attr attribute = getAttribute(i); |
| if (attribute != null) { |
| return attribute.getPrefix(); |
| } |
| return null; |
| } |
| |
| /* |
| * This does not seem to be called by the layoutlib, but we keep this (and maintain |
| * it) just in case. |
| */ |
| @Nullable |
| @Override |
| public String getAttributeValue(int i) { |
| Attr attribute = getAttribute(i); |
| if (attribute != null) { |
| return attribute.getValue(); |
| } |
| |
| return null; |
| } |
| |
| /* |
| * This is the main method used by the LayoutInflater to query for attributes. |
| */ |
| @Nullable |
| @Override |
| public String getAttributeValue(String namespace, String localName) { |
| Element element = getCurrentElement(); |
| if (element != null) { |
| Attr attribute = element.getAttributeNodeNS(namespace, localName); |
| |
| // Auto-convert http://schemas.android.com/apk/res-auto resources. The lookup |
| // will be for the current application's resource package, e.g. |
| // http://schemas.android.com/apk/res/foo.bar, but the XML document will |
| // be using http://schemas.android.com/apk/res-auto in library projects: |
| if (attribute == null && namespace != null && !namespace.equals(ANDROID_URI)) { |
| attribute = element.getAttributeNodeNS(AUTO_URI, localName); |
| } |
| |
| if (attribute != null) { |
| return attribute.getValue(); |
| } |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public int getDepth() { |
| return myNodeStack.size(); |
| } |
| |
| @Nullable |
| @Override |
| public String getName() { |
| if (myParsingState == START_TAG || myParsingState == END_TAG) { |
| Element currentNode = getCurrentElement(); |
| assert currentNode != null; // Should only be called when START_TAG |
| return currentNode.getTagName(); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String getNamespace() { |
| if (myParsingState == START_TAG || myParsingState == END_TAG) { |
| Element currentNode = getCurrentElement(); |
| assert currentNode != null; // Should only be called when START_TAG |
| return currentNode.getNamespaceURI(); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String getPrefix() { |
| if (myParsingState == START_TAG || myParsingState == END_TAG) { |
| Element currentNode = getCurrentElement(); |
| assert currentNode != null; // Should only be called when START_TAG |
| return currentNode.getPrefix(); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public boolean isEmptyElementTag() throws XmlPullParserException { |
| if (myParsingState == START_TAG) { |
| Element currentNode = getCurrentElement(); |
| assert currentNode != null; // Should only be called when START_TAG |
| return currentNode.getChildNodes().getLength() == 0; |
| } |
| |
| throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG", this, null); |
| } |
| |
| @Override |
| protected void onNextFromStartDocument() { |
| if (myRoot != null) { |
| push(myRoot); |
| myParsingState = START_TAG; |
| } else { |
| myParsingState = END_DOCUMENT; |
| } |
| } |
| |
| @Override |
| protected void onNextFromStartTag() { |
| // get the current node, and look for text or children (children first) |
| Element node = getCurrentElement(); |
| assert node != null; // Should only be called when START_TAG |
| List<Element> children = LintUtils.getChildren(node); |
| if (children.size() > 0) { |
| // move to the new child, and don't change the state. |
| push(children.get(0)); |
| |
| // in case the current state is CURRENT_DOC, we set the proper state. |
| myParsingState = START_TAG; |
| } |
| else { |
| if (myParsingState == START_DOCUMENT) { |
| // this handles the case where there's no node. |
| myParsingState = END_DOCUMENT; |
| } |
| else { |
| myParsingState = END_TAG; |
| } |
| } |
| } |
| |
| @Override |
| protected void onNextFromEndTag() { |
| // look for a sibling. if no sibling, go back to the parent |
| Element node = getCurrentElement(); |
| assert node != null; // Should only be called when END_TAG |
| |
| Node sibling = node.getNextSibling(); |
| while (sibling != null && !(sibling instanceof Element)) { |
| sibling = sibling.getNextSibling(); |
| } |
| if (sibling != null) { |
| node = (Element)sibling; |
| // to go to the sibling, we need to remove the current node, |
| pop(); |
| // and add its sibling. |
| push(node); |
| myParsingState = START_TAG; |
| } |
| else { |
| // move back to the parent |
| pop(); |
| |
| // we have only one element left (myRoot), then we're done with the document. |
| if (myNodeStack.isEmpty()) { |
| myParsingState = END_DOCUMENT; |
| } |
| else { |
| myParsingState = END_TAG; |
| } |
| } |
| } |
| |
| /** |
| * Creates an empty new document builder. |
| * <p> |
| * The new documents will not validate, will ignore comments, and will |
| * support namespaces. |
| * |
| * @return the new document builder |
| */ |
| @Nullable |
| public static DocumentBuilder createNewDocumentBuilder() { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| factory.setValidating(false); |
| factory.setIgnoringComments(true); |
| try { |
| return factory.newDocumentBuilder(); |
| } catch (ParserConfigurationException e) { |
| Logger.getInstance(DomPullParser.class).error(e); |
| } |
| return null; |
| } |
| |
| /** |
| * Creates an empty plain XML document. |
| * <p> |
| * The new document will not validate, will ignore comments, and will |
| * support namespaces. |
| * |
| * @return the new document |
| */ |
| @Nullable |
| public static Document createEmptyPlainDocument() { |
| return createNewDocumentBuilder().newDocument(); |
| } |
| } |