| /* |
| * 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.ide.common.rendering.api.ILayoutPullParser; |
| import com.android.ide.common.res2.ValueXmlHelper; |
| import com.android.resources.Density; |
| import com.android.resources.ResourceFolderType; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.psi.xml.XmlAttribute; |
| import com.intellij.psi.xml.XmlFile; |
| import com.intellij.psi.xml.XmlTag; |
| import org.jetbrains.annotations.NotNull; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.tools.idea.rendering.RenderService.AttributeFilter; |
| |
| /** |
| * {@link com.android.ide.common.rendering.api.ILayoutPullParser} implementation on top of |
| * the PSI {@link XmlTag}. |
| * <p/> |
| * It's designed to work on layout files, and will not work on other resource files (no text event |
| * support for example). |
| * <p/> |
| * This pull parser generates {@link com.android.ide.common.rendering.api.ViewInfo}s whose keys |
| * are of type {@link XmlTag}. |
| */ |
| public class LayoutPsiPullParser extends LayoutPullParser { |
| @NotNull |
| private final RenderLogger myLogger; |
| |
| @NotNull |
| private final List<Element> myNodeStack = new ArrayList<Element>(); |
| |
| @Nullable |
| protected final Element myRoot; |
| |
| @Nullable |
| private String myToolsPrefix; |
| |
| @Nullable |
| protected String myAndroidPrefix; |
| |
| protected boolean myProvideViewCookies = true; |
| |
| /** |
| * Constructs a new {@link LayoutPsiPullParser}, a parser dedicated to the special case of |
| * parsing a layout resource files. |
| * |
| * @param file The {@link XmlTag} for the root node. |
| * @param logger The logger to emit warnings too, such as missing fragment associations |
| */ |
| @NotNull |
| public static LayoutPsiPullParser create(@NotNull XmlFile file, @NotNull RenderLogger logger) { |
| if (ResourceHelper.getFolderType(file) == ResourceFolderType.MENU) { |
| return new LayoutPsiPullParser(file, logger) { |
| @Nullable |
| @Override |
| public Object getViewCookie() { |
| if (myProvideViewCookies) { |
| Element element = getCurrentNode(); |
| if (element != null) { |
| // <menu> tags means that we are adding a sub-menu. Since we don't show the submenu, we |
| // return the enclosing tag. |
| if (element.tag.equals(FD_RES_MENU)) { |
| Element previousElement = getPreviousNode(); |
| if (previousElement != null) { |
| return previousElement.cookie; |
| } |
| } |
| return element.cookie; |
| } |
| } |
| |
| return null; |
| } |
| }; |
| } |
| return new LayoutPsiPullParser(file, logger); |
| } |
| |
| /** |
| * Constructs a new {@link LayoutPsiPullParser}, a parser dedicated to the special case of |
| * parsing a layout resource files, and handling "exploded rendering" - adding padding on views |
| * to make them easier to see and operate on. |
| * |
| * @param file The {@link com.intellij.psi.xml.XmlTag} for the root node. |
| * @param logger The logger to emit warnings too, such as missing fragment associations |
| * @param explodeNodes A set of individual nodes that should be assigned a fixed amount of |
| * padding ({@link com.android.tools.idea.rendering.PaddingLayoutPsiPullParser#FIXED_PADDING_VALUE}). |
| * This is intended for use with nodes that (without padding) would be |
| * invisible. |
| * @param density the density factor for the screen. |
| */ |
| @NotNull |
| public static LayoutPsiPullParser create(@NotNull XmlFile file, |
| @NotNull RenderLogger logger, |
| @Nullable Set<XmlTag> explodeNodes, |
| @NotNull Density density) { |
| if (explodeNodes != null && !explodeNodes.isEmpty()) { |
| return new PaddingLayoutPsiPullParser(file, logger, explodeNodes, density); |
| } else { |
| return new LayoutPsiPullParser(file, logger); |
| } |
| } |
| |
| @NotNull |
| public static LayoutPsiPullParser create(@Nullable final AttributeFilter filter, |
| @NotNull XmlTag root, |
| @NotNull RenderLogger logger) { |
| return new LayoutPsiPullParser(root, logger) { |
| @Override |
| public String getAttributeValue(final String namespace, final String localName) { |
| if (filter != null) { |
| Element element = getCurrentNode(); |
| if (element != null) { |
| final XmlTag tag = element.cookie; |
| if (tag != null) { |
| String value; |
| if (ApplicationManager.getApplication().isReadAccessAllowed()) { |
| value = filter.getAttribute(tag, namespace, localName); |
| } |
| else { |
| value = ApplicationManager.getApplication().runReadAction(new Computable<String>() { |
| @Override |
| @Nullable |
| public String compute() { |
| return filter.getAttribute(tag, namespace, localName); |
| } |
| }); |
| } |
| if (value != null) { |
| if (value.isEmpty()) { // empty means unset |
| return null; |
| } |
| return value; |
| } |
| // null means no preference, not "unset". |
| } |
| } |
| } |
| |
| return super.getAttributeValue(namespace, localName); |
| } |
| }; |
| } |
| |
| /** Use one of the {@link #create} factory methods instead */ |
| protected LayoutPsiPullParser(@NotNull XmlFile file, @NotNull RenderLogger logger) { |
| this(file.getRootTag(), logger); |
| } |
| |
| protected LayoutPsiPullParser(@Nullable final XmlTag root, @NotNull RenderLogger logger) { |
| myLogger = logger; |
| |
| if (root != null && root.isValid()) { |
| if (ApplicationManager.getApplication().isReadAccessAllowed()) { |
| myAndroidPrefix = root.getPrefixByNamespace(ANDROID_URI); |
| myToolsPrefix = root.getPrefixByNamespace(TOOLS_URI); |
| myRoot = createSnapshot(root); |
| } else { |
| myRoot = ApplicationManager.getApplication().runReadAction(new Computable<Element>() { |
| |
| @Override |
| public Element compute() { |
| myAndroidPrefix = root.getPrefixByNamespace(ANDROID_URI); |
| myToolsPrefix = root.getPrefixByNamespace(TOOLS_URI); |
| return createSnapshot(root); |
| } |
| }); |
| } |
| } else { |
| myRoot = null; |
| } |
| } |
| |
| @Nullable |
| protected final Element getCurrentNode() { |
| if (myNodeStack.size() > 0) { |
| return myNodeStack.get(myNodeStack.size() - 1); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| protected final Element getPreviousNode() { |
| if (myNodeStack.size() > 1) { |
| return myNodeStack.get(myNodeStack.size() - 2); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| protected final Attribute getAttribute(int i) { |
| if (myParsingState != START_TAG) { |
| throw new IndexOutOfBoundsException(); |
| } |
| |
| // get the current uiNode |
| Element uiNode = getCurrentNode(); |
| if (uiNode != null) { |
| return uiNode.attributes.get(i); |
| } |
| |
| return null; |
| } |
| |
| protected void push(@NotNull Element node) { |
| myNodeStack.add(node); |
| } |
| |
| @NotNull |
| protected Element pop() { |
| return myNodeStack.remove(myNodeStack.size() - 1); |
| } |
| |
| // ------------- IXmlPullParser -------- |
| |
| /** |
| * {@inheritDoc} |
| * <p/> |
| * This implementation returns the underlying DOM node of type {@link XmlTag}. |
| * Note that the link between the GLE 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() { |
| if (myProvideViewCookies) { |
| Element element = getCurrentNode(); |
| if (element != null) { |
| return element.cookie; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * 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 = getCurrentNode(); |
| |
| if (node != null) { |
| return node.attributes.size(); |
| } |
| |
| 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) { |
| Attribute attribute = getAttribute(i); |
| if (attribute != null) { |
| return attribute.name; |
| } |
| |
| 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) { |
| Attribute attribute = getAttribute(i); |
| if (attribute != null) { |
| return attribute.namespace; |
| } |
| 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) { |
| Attribute attribute = getAttribute(i); |
| if (attribute != null) { |
| String prefix = attribute.prefix; |
| if (prefix.isEmpty()) { |
| prefix = null; |
| } |
| return prefix; |
| } |
| 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) { |
| Attribute attribute = getAttribute(i); |
| if (attribute != null) { |
| return attribute.value; |
| } |
| |
| return null; |
| } |
| |
| /* |
| * This is the main method used by the LayoutInflater to query for attributes. |
| */ |
| @Nullable |
| @Override |
| public String getAttributeValue(String namespace, String localName) { |
| // get the current uiNode |
| Element tag = getCurrentNode(); |
| if (tag != null) { |
| if (ATTR_LAYOUT.equals(localName) && VIEW_FRAGMENT.equals(tag.tag)) { |
| String layout = tag.getAttribute(LayoutMetadata.KEY_FRAGMENT_LAYOUT, TOOLS_URI); |
| if (layout != null) { |
| return layout; |
| } |
| } |
| |
| String value = null; |
| if (namespace == null) { |
| value = tag.getAttribute(localName); |
| } else if (namespace.equals(ANDROID_URI)) { |
| if (myAndroidPrefix != null) { |
| if (myToolsPrefix != null) { |
| for (Attribute attribute : tag.attributes) { |
| if (localName.equals(attribute.name)) { |
| if (myToolsPrefix.equals(attribute.prefix)) { |
| value = attribute.value; |
| if (value.isEmpty()) { |
| // Empty when there is a runtime attribute set means unset the runtime attribute |
| value = tag.getAttribute(localName, ANDROID_URI) != null ? null : value; |
| } |
| break; |
| } else if (myAndroidPrefix.equals(attribute.prefix)) { |
| value = attribute.value; |
| // Don't break: continue searching in case we find a tools design time attribute |
| } |
| } |
| } |
| } else { |
| value = tag.getAttribute(localName, ANDROID_URI); |
| } |
| } else { |
| value = tag.getAttribute(localName, namespace); |
| } |
| } else { |
| // 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: |
| for (Attribute attribute : tag.attributes) { |
| if (localName.equals(attribute.name) && (namespace.equals(attribute.namespace) || |
| AUTO_URI.equals(attribute.namespace))) { |
| value = attribute.value; |
| break; |
| } |
| } |
| } |
| |
| if (value != null) { |
| // on the fly convert match_parent to fill_parent for compatibility with older |
| // platforms. |
| if (VALUE_MATCH_PARENT.equals(value) && |
| (ATTR_LAYOUT_WIDTH.equals(localName) || ATTR_LAYOUT_HEIGHT.equals(localName)) && |
| ANDROID_URI.equals(namespace)) { |
| return VALUE_FILL_PARENT; |
| } |
| |
| // Handle unicode and XML escapes |
| for (int i = 0, n = value.length(); i < n; i++) { |
| char c = value.charAt(i); |
| if (c == '&' || c == '\\') { |
| value = ValueXmlHelper.unescapeResourceString(value, true, false); |
| break; |
| } |
| } |
| } |
| |
| return value; |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public int getDepth() { |
| return myNodeStack.size(); |
| } |
| |
| @Nullable |
| @Override |
| public String getName() { |
| if (myParsingState == START_TAG || myParsingState == END_TAG) { |
| Element currentNode = getCurrentNode(); |
| assert currentNode != null; // Should only be called when START_TAG |
| String name = currentNode.tag; |
| if (name.equals(VIEW_FRAGMENT)) { |
| // Temporarily translate <fragment> to <include> (and in getAttribute |
| // we will also provide a layout-attribute for the corresponding |
| // fragment name attribute) |
| String layout = currentNode.getAttribute(LayoutMetadata.KEY_FRAGMENT_LAYOUT, TOOLS_URI); |
| if (layout != null) { |
| return VIEW_INCLUDE; |
| } else { |
| String fragmentId = currentNode.getAttribute(ATTR_CLASS); |
| if (fragmentId == null || fragmentId.isEmpty()) { |
| fragmentId = currentNode.getAttribute(ATTR_NAME, ANDROID_URI); |
| if (fragmentId == null || fragmentId.isEmpty()) { |
| fragmentId = currentNode.getAttribute(ATTR_ID, ANDROID_URI); |
| } |
| } |
| myLogger.warning(RenderLogger.TAG_MISSING_FRAGMENT, "Missing fragment association", fragmentId); |
| } |
| } |
| |
| return name; |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String getNamespace() { |
| if (myParsingState == START_TAG || myParsingState == END_TAG) { |
| Element currentNode = getCurrentNode(); |
| assert currentNode != null; // Should only be called when START_TAG |
| return currentNode.namespace; |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String getPrefix() { |
| if (myParsingState == START_TAG || myParsingState == END_TAG) { |
| Element currentNode = getCurrentNode(); |
| assert currentNode != null; // Should only be called when START_TAG |
| String prefix = currentNode.prefix; |
| if (prefix.isEmpty()) { |
| prefix = null; |
| } |
| return prefix; |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public boolean isEmptyElementTag() throws XmlPullParserException { |
| if (myParsingState == START_TAG) { |
| Element currentNode = getCurrentNode(); |
| assert currentNode != null; // Should only be called when START_TAG |
| // This isn't quite right; if layoutlib starts needing this, stash XmlTag#isEmpty() in snapshot |
| return currentNode.children.isEmpty(); |
| } |
| |
| 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 = getCurrentNode(); |
| assert node != null; // Should only be called when START_TAG |
| List<Element> children = node.children; |
| if (!children.isEmpty()) { |
| // 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 = getCurrentNode(); |
| assert node != null; // Should only be called when END_TAG |
| |
| Element sibling = node.next; |
| if (sibling != null) { |
| node = 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; |
| } |
| } |
| } |
| |
| /** Sets whether this parser will provide view cookies */ |
| public void setProvideViewCookies(boolean provideViewCookies) { |
| myProvideViewCookies = provideViewCookies; |
| } |
| |
| private static Element createSnapshot(XmlTag tag) { |
| // <include> tags can't be at the root level; handle <fragment> rewriting here such that we don't |
| // need to handle it as a tag name rewrite (where it's harder to change the structure) |
| // https://code.google.com/p/android/issues/detail?id=67910 |
| String rootTag = tag.getName(); |
| if (rootTag.equals(VIEW_FRAGMENT)) { |
| Element element = new Element(tag, FRAME_LAYOUT, "", ""); |
| XmlAttribute[] psiAttributes = tag.getAttributes(); |
| List<Attribute> attributes = Lists.newArrayListWithExpectedSize(psiAttributes.length); |
| element.attributes = attributes; |
| for (XmlAttribute psiAttribute : psiAttributes) { |
| Attribute attribute = createAttributeSnapshot(psiAttribute); |
| attributes.add(attribute); |
| } |
| Element include = new Element(null, VIEW_FRAGMENT, "", ""); |
| element.children = Collections.singletonList(include); |
| include.children = Collections.emptyList(); |
| List<Attribute> includeAttributes = Lists.newArrayListWithExpectedSize(psiAttributes.length); |
| include.attributes = includeAttributes; |
| for (XmlAttribute psiAttribute : psiAttributes) { |
| String name = psiAttribute.getName(); |
| if (name.startsWith(XMLNS_PREFIX)) { |
| continue; |
| } |
| String localName = psiAttribute.getLocalName(); |
| if (localName.startsWith(ATTR_LAYOUT_MARGIN) || localName.startsWith(ATTR_PADDING) || |
| localName.equals(ATTR_ID)) { |
| continue; |
| } |
| Attribute attribute = createAttributeSnapshot(psiAttribute); |
| includeAttributes.add(attribute); |
| } |
| return element; |
| } else if (rootTag.equals(FRAME_LAYOUT)) { |
| Element root = createTagSnapshot(tag); |
| |
| // tools:layout on a <FrameLayout> acts like an <include> child. This |
| // lets you preview runtime additions on FrameLayouts. |
| String layout = tag.getAttributeValue(ATTR_LAYOUT, TOOLS_URI); |
| if (layout != null && root.children.isEmpty()) { |
| String prefix = tag.getPrefixByNamespace(ANDROID_URI); |
| if (prefix != null) { |
| List<Element> children = Lists.newArrayList(); |
| children.addAll(root.children); |
| root.children = children; |
| |
| Element element = new Element(null, VIEW_INCLUDE, "", ""); |
| children.add(element); |
| element.children = Collections.emptyList(); |
| List<Attribute> attributes = Lists.newArrayListWithExpectedSize(3); |
| element.attributes = attributes; |
| attributes.add(new Attribute("", "", ATTR_LAYOUT, layout)); |
| attributes.add(new Attribute(ANDROID_URI, prefix, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT)); |
| attributes.add(new Attribute(ANDROID_URI, prefix, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT)); |
| } |
| } |
| |
| // Allow <FrameLayout tools:visibleChildren="1,3,5"> to make all but the given children visible |
| String visibleChild = tag.getAttributeValue("visibleChildren", TOOLS_URI); |
| if (visibleChild != null) { |
| Set<Integer> indices = Sets.newHashSet(); |
| for (String s : Splitter.on(',').trimResults().omitEmptyStrings().split(visibleChild)) { |
| try { |
| indices.add(Integer.parseInt(s)); |
| } catch (NumberFormatException e) { |
| // ignore metadata if it's incorrect |
| } |
| } |
| String prefix = tag.getPrefixByNamespace(ANDROID_URI); |
| if (prefix != null) { |
| for (int i = 0, n = root.children.size(); i < n; i++) { |
| Element child = root.children.get(i); |
| boolean visible = indices.contains(i); |
| child.setAttribute(ATTR_VISIBILITY, ANDROID_URI, prefix, visible ? "visible" : "gone"); |
| } |
| } |
| } |
| |
| return root; |
| } else { |
| Element root = createTagSnapshot(tag); |
| |
| // Ensure that root tags that qualify for adapter binding specify an id attribute, since that is required for |
| // attribute binding to work. (Without this, a <ListView> at the root level will not show Item 1, Item 2, etc. |
| if (rootTag.equals(LIST_VIEW) || rootTag.equals(EXPANDABLE_LIST_VIEW) || rootTag.equals(GRID_VIEW) || rootTag.equals(SPINNER)) { |
| XmlAttribute id = tag.getAttribute(ATTR_ID, ANDROID_URI); |
| if (id == null) { |
| String prefix = tag.getPrefixByNamespace(ANDROID_URI); |
| if (prefix != null) { |
| root.attributes.add(new Attribute(ANDROID_URI, prefix, ATTR_ID, "@+id/_dynamic")); |
| } |
| } |
| } |
| |
| return root; |
| } |
| } |
| |
| private static Element createTagSnapshot(XmlTag tag) { |
| Element element = new Element(tag); |
| |
| // Attributes |
| XmlAttribute[] psiAttributes = tag.getAttributes(); |
| List<Attribute> attributes = Lists.newArrayListWithExpectedSize(psiAttributes.length); |
| element.attributes = attributes; |
| for (XmlAttribute psiAttribute : psiAttributes) { |
| Attribute attribute = createAttributeSnapshot(psiAttribute); |
| attributes.add(attribute); |
| } |
| |
| // Children |
| XmlTag[] subTags = tag.getSubTags(); |
| if (subTags.length > 0) { |
| Element last = null; |
| ArrayList<Element> children = Lists.newArrayListWithExpectedSize(subTags.length); |
| element.children = children; |
| for (XmlTag subTag : subTags) { |
| Element child = createTagSnapshot(subTag); |
| children.add(child); |
| if (last != null) { |
| last.next = child; |
| } |
| last = child; |
| } |
| } else { |
| element.children = Collections.emptyList(); |
| } |
| |
| return element; |
| } |
| |
| private static Attribute createAttributeSnapshot(XmlAttribute psiAttribute) { |
| String localName = psiAttribute.getLocalName(); |
| String namespace = psiAttribute.getNamespace(); |
| String prefix = psiAttribute.getNamespacePrefix(); |
| String value = psiAttribute.getValue(); |
| return new Attribute(namespace, prefix, localName, value); |
| } |
| |
| protected static class Element { |
| public final String namespace; |
| public final String tag; |
| public final XmlTag cookie; |
| private final String prefix; |
| public Element next; |
| public List<Element> children; |
| public List<Attribute> attributes; |
| |
| public Element(@Nullable XmlTag tag, String tagName, String prefix, String namespace) { |
| this.tag = tagName; |
| this.prefix = prefix; |
| this.namespace = namespace; |
| this.cookie = tag; |
| } |
| |
| public Element(@NotNull XmlTag tag) { |
| this(tag, tag.getName(), tag.getNamespacePrefix(), tag.getNamespace()); |
| } |
| |
| @Nullable |
| public String getAttribute(String name) { |
| return getAttribute(name, null); |
| } |
| |
| @Nullable |
| public String getAttribute(String name, @Nullable String namespace) { |
| // We just use a list rather than a map since in layouts the number of attributes is |
| // typically very small so map overhead isn't worthwhile |
| for (Attribute attribute : attributes) { |
| if (name.equals(attribute.name) && (namespace == null || namespace.equals(attribute.namespace))) { |
| return attribute.value; |
| } |
| } |
| |
| return null; |
| } |
| |
| private void setAttribute(String name, String namespace, String prefix, @Nullable String value) { |
| for (Attribute attribute : attributes) { |
| if (name.equals(attribute.name) && (namespace == null || namespace.equals(attribute.namespace))) { |
| attributes.remove(attribute); |
| break; |
| } |
| } |
| if (value != null) { |
| attributes.add(new Attribute(namespace, prefix, name, value)); |
| } |
| } |
| } |
| |
| protected static class Attribute { |
| public String namespace; |
| public String prefix; |
| public String name; |
| public String value; |
| |
| private Attribute(@Nullable String namespace, String prefix, String name, @Nullable String value) { |
| this.namespace = namespace; |
| this.prefix = prefix; |
| this.name = name; |
| this.value = value; |
| } |
| } |
| } |