blob: 30886e4fa650d6b0d4b2299242073cdc7a962023 [file] [log] [blame]
/*
* 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_URI;
import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
import static com.android.SdkConstants.ATTR_ID;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.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.common.xml.XmlFormatStyle;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
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;
/** 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 EclipseXmlPrettyPrinter(EclipseXmlFormatPreferences.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) {
fail(nufe.getLocalizedMessage());
}
String tag = matcher.group(3);
for (INode child : getChildren()) {
assertTrue(iterator.hasNext());
((TestNode) child).assignBounds(iterator);
}
}
}