| /* |
| * Copyright (C) 2010 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.layout; |
| |
| import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static junit.framework.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.IAttributeInfo; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.common.api.INodeHandler; |
| import com.android.ide.common.api.Margins; |
| import com.android.ide.common.api.Rect; |
| import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; |
| import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; |
| import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; |
| import com.google.common.base.Splitter; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| |
| import java.io.IOException; |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import junit.framework.Assert; |
| |
| /** Test/mock implementation of {@link INode} */ |
| @SuppressWarnings("javadoc") |
| public class TestNode implements INode { |
| private TestNode mParent; |
| |
| private final List<TestNode> mChildren = new ArrayList<TestNode>(); |
| |
| private final String mFqcn; |
| |
| private Rect mBounds = new Rect(); // Invalid bounds initially |
| |
| private Map<String, IAttribute> mAttributes = new HashMap<String, IAttribute>(); |
| |
| private Map<String, IAttributeInfo> mAttributeInfos = new HashMap<String, IAttributeInfo>(); |
| |
| private List<String> mAttributeSources; |
| |
| public TestNode(String fqcn) { |
| this.mFqcn = fqcn; |
| } |
| |
| public TestNode bounds(Rect bounds) { |
| this.mBounds = bounds; |
| |
| return this; |
| } |
| |
| public TestNode id(String id) { |
| return set(ANDROID_URI, ATTR_ID, id); |
| } |
| |
| public TestNode set(String uri, String name, String value) { |
| setAttribute(uri, name, value); |
| |
| return this; |
| } |
| |
| public TestNode add(TestNode child) { |
| mChildren.add(child); |
| child.mParent = this; |
| |
| return this; |
| } |
| |
| public TestNode add(TestNode... children) { |
| for (TestNode child : children) { |
| mChildren.add(child); |
| child.mParent = this; |
| } |
| |
| return this; |
| } |
| |
| public static TestNode create(String fcqn) { |
| return new TestNode(fcqn); |
| } |
| |
| public void removeChild(int index) { |
| TestNode removed = mChildren.remove(index); |
| removed.mParent = null; |
| } |
| |
| // ==== INODE ==== |
| |
| @Override |
| public @NonNull INode appendChild(@NonNull String viewFqcn) { |
| return insertChildAt(viewFqcn, mChildren.size()); |
| } |
| |
| @Override |
| public void editXml(@NonNull String undoName, @NonNull INodeHandler callback) { |
| callback.handle(this); |
| } |
| |
| public void putAttributeInfo(String uri, String attrName, IAttributeInfo info) { |
| mAttributeInfos.put(uri + attrName, info); |
| } |
| |
| @Override |
| public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) { |
| return mAttributeInfos.get(uri + attrName); |
| } |
| |
| @Override |
| public @NonNull Rect getBounds() { |
| return mBounds; |
| } |
| |
| @Override |
| public @NonNull INode[] getChildren() { |
| return mChildren.toArray(new INode[mChildren.size()]); |
| } |
| |
| @Override |
| public @NonNull IAttributeInfo[] getDeclaredAttributes() { |
| return mAttributeInfos.values().toArray(new IAttributeInfo[mAttributeInfos.size()]); |
| } |
| |
| @Override |
| public @NonNull String getFqcn() { |
| return mFqcn; |
| } |
| |
| @Override |
| public @NonNull IAttribute[] getLiveAttributes() { |
| return mAttributes.values().toArray(new IAttribute[mAttributes.size()]); |
| } |
| |
| @Override |
| public INode getParent() { |
| return mParent; |
| } |
| |
| @Override |
| public INode getRoot() { |
| TestNode curr = this; |
| while (curr.mParent != null) { |
| curr = curr.mParent; |
| } |
| |
| return curr; |
| } |
| |
| @Override |
| public String getStringAttr(@Nullable String uri, @NonNull String attrName) { |
| IAttribute attr = mAttributes.get(uri + attrName); |
| if (attr == null) { |
| return null; |
| } |
| |
| return attr.getValue(); |
| } |
| |
| @Override |
| public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) { |
| TestNode child = new TestNode(viewFqcn); |
| if (index == -1) { |
| mChildren.add(child); |
| } else { |
| mChildren.add(index, child); |
| } |
| child.mParent = this; |
| return child; |
| } |
| |
| @Override |
| public void removeChild(@NonNull INode node) { |
| int index = mChildren.indexOf(node); |
| if (index != -1) { |
| removeChild(index); |
| } |
| } |
| |
| @Override |
| public boolean setAttribute(@Nullable String uri, @NonNull String localName, |
| @Nullable String value) { |
| mAttributes.put(uri + localName, new TestAttribute(uri, localName, value)); |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| String id = getStringAttr(ANDROID_URI, ATTR_ID); |
| return "TestNode [id=" + (id != null ? id : "?") + ", fqn=" + mFqcn + ", infos=" |
| + mAttributeInfos + ", attributes=" + mAttributes + ", bounds=" + mBounds + "]"; |
| } |
| |
| @Override |
| public int getBaseline() { |
| return -1; |
| } |
| |
| @Override |
| public @NonNull Margins getMargins() { |
| return null; |
| } |
| |
| @Override |
| public @NonNull List<String> getAttributeSources() { |
| return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList(); |
| } |
| |
| public void setAttributeSources(List<String> attributeSources) { |
| mAttributeSources = attributeSources; |
| } |
| |
| /** Create a test node from the given XML */ |
| public static TestNode createFromXml(String xml) { |
| Document document = DomUtilities.parseDocument(xml, false); |
| assertNotNull(document); |
| assertNotNull(document.getDocumentElement()); |
| |
| return createFromNode(document.getDocumentElement()); |
| } |
| |
| public static String toXml(TestNode node) { |
| assertTrue("This method only works with nodes constructed from XML", |
| node instanceof TestXmlNode); |
| Document document = ((TestXmlNode) node).mElement.getOwnerDocument(); |
| // Insert new whitespace nodes etc |
| String xml = dumpDocument(document); |
| document = DomUtilities.parseDocument(xml, false); |
| |
| XmlPrettyPrinter printer = new XmlPrettyPrinter(XmlFormatPreferences.create(), |
| XmlFormatStyle.LAYOUT, "\n"); |
| StringBuilder sb = new StringBuilder(1000); |
| sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); |
| printer.prettyPrint(-1, document, null, null, sb, false); |
| return sb.toString(); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private static String dumpDocument(Document document) { |
| // Diagnostics: print out the XML that we're about to render |
| org.apache.xml.serialize.OutputFormat outputFormat = |
| new org.apache.xml.serialize.OutputFormat( |
| "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$ |
| outputFormat.setIndent(2); |
| outputFormat.setLineWidth(100); |
| outputFormat.setIndenting(true); |
| outputFormat.setOmitXMLDeclaration(true); |
| outputFormat.setOmitDocumentType(true); |
| StringWriter stringWriter = new StringWriter(); |
| // Using FQN here to avoid having an import above, which will result |
| // in a deprecation warning, and there isn't a way to annotate a single |
| // import element with a SuppressWarnings. |
| org.apache.xml.serialize.XMLSerializer serializer = |
| new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat); |
| serializer.setNamespaces(true); |
| try { |
| serializer.serialize(document.getDocumentElement()); |
| return stringWriter.toString(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| return null; |
| } |
| |
| private static TestNode createFromNode(Element element) { |
| String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName(); |
| TestNode node = new TestXmlNode(fqcn, element); |
| |
| for (Element child : DomUtilities.getChildren(element)) { |
| node.add(createFromNode(child)); |
| } |
| |
| return node; |
| } |
| |
| @Nullable |
| public static TestNode findById(TestNode node, String id) { |
| id = BaseLayoutRule.stripIdPrefix(id); |
| return node.findById(id); |
| } |
| |
| private TestNode findById(String targetId) { |
| String id = getStringAttr(ANDROID_URI, ATTR_ID); |
| if (id != null && targetId.equals(BaseLayoutRule.stripIdPrefix(id))) { |
| return this; |
| } |
| |
| for (TestNode child : mChildren) { |
| TestNode result = child.findById(targetId); |
| if (result != null) { |
| return result; |
| } |
| } |
| |
| return null; |
| } |
| |
| private static String getTagName(String fqcn) { |
| return fqcn.substring(fqcn.lastIndexOf('.') + 1); |
| } |
| |
| private static class TestXmlNode extends TestNode { |
| private final Element mElement; |
| |
| public TestXmlNode(String fqcn, Element element) { |
| super(fqcn); |
| mElement = element; |
| } |
| |
| @Override |
| public @NonNull IAttribute[] getLiveAttributes() { |
| List<IAttribute> result = new ArrayList<IAttribute>(); |
| |
| NamedNodeMap attributes = mElement.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Attr attribute = (Attr) attributes.item(i); |
| result.add(new TestXmlAttribute(attribute)); |
| } |
| return result.toArray(new IAttribute[result.size()]); |
| } |
| |
| @Override |
| public boolean setAttribute(String uri, String localName, String value) { |
| if (value == null) { |
| mElement.removeAttributeNS(uri, localName); |
| } else { |
| mElement.setAttributeNS(uri, localName, value); |
| } |
| return super.setAttribute(uri, localName, value); |
| } |
| |
| @Override |
| public INode appendChild(String viewFqcn) { |
| Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn)); |
| mElement.appendChild(child); |
| return new TestXmlNode(viewFqcn, child); |
| } |
| |
| @Override |
| public INode insertChildAt(String viewFqcn, int index) { |
| if (index == -1) { |
| return appendChild(viewFqcn); |
| } |
| Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn)); |
| List<Element> children = DomUtilities.getChildren(mElement); |
| if (children.size() >= index) { |
| Element before = children.get(index); |
| mElement.insertBefore(child, before); |
| } else { |
| fail("Unexpected index"); |
| mElement.appendChild(child); |
| } |
| return new TestXmlNode(viewFqcn, child); |
| } |
| |
| @Override |
| public String getStringAttr(String uri, String name) { |
| String value; |
| if (uri == null) { |
| value = mElement.getAttribute(name); |
| } else { |
| value = mElement.getAttributeNS(uri, name); |
| } |
| if (value.isEmpty()) { |
| value = null; |
| } |
| |
| return value; |
| } |
| |
| @Override |
| public void removeChild(INode node) { |
| assert node instanceof TestXmlNode; |
| mElement.removeChild(((TestXmlNode) node).mElement); |
| } |
| |
| @Override |
| public void removeChild(int index) { |
| List<Element> children = DomUtilities.getChildren(mElement); |
| assertTrue(index < children.size()); |
| Element oldChild = children.get(index); |
| mElement.removeChild(oldChild); |
| } |
| } |
| |
| public static class TestXmlAttribute implements IAttribute { |
| private Attr mAttribute; |
| |
| public TestXmlAttribute(Attr attribute) { |
| this.mAttribute = attribute; |
| } |
| |
| @Override |
| public String getUri() { |
| return mAttribute.getNamespaceURI(); |
| } |
| |
| @Override |
| public String getName() { |
| String name = mAttribute.getLocalName(); |
| if (name == null) { |
| name = mAttribute.getName(); |
| } |
| return name; |
| } |
| |
| @Override |
| public String getValue() { |
| return mAttribute.getValue(); |
| } |
| } |
| |
| // Recursively initialize this node with the bounds specified in the given hierarchy |
| // dump (from ViewHierarchy's DUMP_INFO flag |
| public void assignBounds(String bounds) { |
| Iterable<String> split = Splitter.on('\n').trimResults().split(bounds); |
| assignBounds(split.iterator()); |
| } |
| |
| private void assignBounds(Iterator<String> iterator) { |
| assertTrue(iterator.hasNext()); |
| String desc = iterator.next(); |
| |
| Pattern pattern = Pattern.compile("^\\s*(.+)\\s+\\[(.+)\\]\\s*(<.+>)?\\s*(\\S+)?\\s*$"); |
| Matcher matcher = pattern.matcher(desc); |
| assertTrue(matcher.matches()); |
| String fqn = matcher.group(1); |
| assertEquals(getFqcn(), fqn); |
| String boundsString = matcher.group(2); |
| String[] bounds = boundsString.split(","); |
| assertEquals(boundsString, 4, bounds.length); |
| try { |
| int left = Integer.parseInt(bounds[0]); |
| int top = Integer.parseInt(bounds[1]); |
| int right = Integer.parseInt(bounds[2]); |
| int bottom = Integer.parseInt(bounds[3]); |
| mBounds = new Rect(left, top, right - left, bottom - top); |
| } catch (NumberFormatException nufe) { |
| Assert.fail(nufe.getLocalizedMessage()); |
| } |
| String tag = matcher.group(3); |
| |
| for (INode child : getChildren()) { |
| assertTrue(iterator.hasNext()); |
| ((TestNode) child).assignBounds(iterator); |
| } |
| } |
| } |