blob: 3b4643780397f56cf05ff1b5b548b4ab2ad9adea [file] [log] [blame]
/*
* 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();
}
}