1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.ide.common.layout; 17 18import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; 19import static com.android.SdkConstants.ATTR_ID; 20import static com.android.SdkConstants.ANDROID_URI; 21import static junit.framework.Assert.assertEquals; 22import static org.junit.Assert.assertNotNull; 23import static org.junit.Assert.assertTrue; 24import static org.junit.Assert.fail; 25 26import com.android.annotations.NonNull; 27import com.android.annotations.Nullable; 28import com.android.ide.common.api.IAttributeInfo; 29import com.android.ide.common.api.INode; 30import com.android.ide.common.api.INodeHandler; 31import com.android.ide.common.api.Margins; 32import com.android.ide.common.api.Rect; 33import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; 34import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; 35import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; 36import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 37import com.google.common.base.Splitter; 38 39import org.w3c.dom.Attr; 40import org.w3c.dom.Document; 41import org.w3c.dom.Element; 42import org.w3c.dom.NamedNodeMap; 43 44import java.io.IOException; 45import java.io.StringWriter; 46import java.util.ArrayList; 47import java.util.Collections; 48import java.util.HashMap; 49import java.util.Iterator; 50import java.util.List; 51import java.util.Map; 52import java.util.regex.Matcher; 53import java.util.regex.Pattern; 54 55import junit.framework.Assert; 56 57/** Test/mock implementation of {@link INode} */ 58@SuppressWarnings("javadoc") 59public class TestNode implements INode { 60 private TestNode mParent; 61 62 private final List<TestNode> mChildren = new ArrayList<TestNode>(); 63 64 private final String mFqcn; 65 66 private Rect mBounds = new Rect(); // Invalid bounds initially 67 68 private Map<String, IAttribute> mAttributes = new HashMap<String, IAttribute>(); 69 70 private Map<String, IAttributeInfo> mAttributeInfos = new HashMap<String, IAttributeInfo>(); 71 72 private List<String> mAttributeSources; 73 74 public TestNode(String fqcn) { 75 this.mFqcn = fqcn; 76 } 77 78 public TestNode bounds(Rect bounds) { 79 this.mBounds = bounds; 80 81 return this; 82 } 83 84 public TestNode id(String id) { 85 return set(ANDROID_URI, ATTR_ID, id); 86 } 87 88 public TestNode set(String uri, String name, String value) { 89 setAttribute(uri, name, value); 90 91 return this; 92 } 93 94 public TestNode add(TestNode child) { 95 mChildren.add(child); 96 child.mParent = this; 97 98 return this; 99 } 100 101 public TestNode add(TestNode... children) { 102 for (TestNode child : children) { 103 mChildren.add(child); 104 child.mParent = this; 105 } 106 107 return this; 108 } 109 110 public static TestNode create(String fcqn) { 111 return new TestNode(fcqn); 112 } 113 114 public void removeChild(int index) { 115 TestNode removed = mChildren.remove(index); 116 removed.mParent = null; 117 } 118 119 // ==== INODE ==== 120 121 @Override 122 public @NonNull INode appendChild(@NonNull String viewFqcn) { 123 return insertChildAt(viewFqcn, mChildren.size()); 124 } 125 126 @Override 127 public void editXml(@NonNull String undoName, @NonNull INodeHandler callback) { 128 callback.handle(this); 129 } 130 131 public void putAttributeInfo(String uri, String attrName, IAttributeInfo info) { 132 mAttributeInfos.put(uri + attrName, info); 133 } 134 135 @Override 136 public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) { 137 return mAttributeInfos.get(uri + attrName); 138 } 139 140 @Override 141 public @NonNull Rect getBounds() { 142 return mBounds; 143 } 144 145 @Override 146 public @NonNull INode[] getChildren() { 147 return mChildren.toArray(new INode[mChildren.size()]); 148 } 149 150 @Override 151 public @NonNull IAttributeInfo[] getDeclaredAttributes() { 152 return mAttributeInfos.values().toArray(new IAttributeInfo[mAttributeInfos.size()]); 153 } 154 155 @Override 156 public @NonNull String getFqcn() { 157 return mFqcn; 158 } 159 160 @Override 161 public @NonNull IAttribute[] getLiveAttributes() { 162 return mAttributes.values().toArray(new IAttribute[mAttributes.size()]); 163 } 164 165 @Override 166 public INode getParent() { 167 return mParent; 168 } 169 170 @Override 171 public INode getRoot() { 172 TestNode curr = this; 173 while (curr.mParent != null) { 174 curr = curr.mParent; 175 } 176 177 return curr; 178 } 179 180 @Override 181 public String getStringAttr(@Nullable String uri, @NonNull String attrName) { 182 IAttribute attr = mAttributes.get(uri + attrName); 183 if (attr == null) { 184 return null; 185 } 186 187 return attr.getValue(); 188 } 189 190 @Override 191 public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) { 192 TestNode child = new TestNode(viewFqcn); 193 if (index == -1) { 194 mChildren.add(child); 195 } else { 196 mChildren.add(index, child); 197 } 198 child.mParent = this; 199 return child; 200 } 201 202 @Override 203 public void removeChild(@NonNull INode node) { 204 int index = mChildren.indexOf(node); 205 if (index != -1) { 206 removeChild(index); 207 } 208 } 209 210 @Override 211 public boolean setAttribute(@Nullable String uri, @NonNull String localName, 212 @Nullable String value) { 213 mAttributes.put(uri + localName, new TestAttribute(uri, localName, value)); 214 return true; 215 } 216 217 @Override 218 public String toString() { 219 String id = getStringAttr(ANDROID_URI, ATTR_ID); 220 return "TestNode [id=" + (id != null ? id : "?") + ", fqn=" + mFqcn + ", infos=" 221 + mAttributeInfos + ", attributes=" + mAttributes + ", bounds=" + mBounds + "]"; 222 } 223 224 @Override 225 public int getBaseline() { 226 return -1; 227 } 228 229 @Override 230 public @NonNull Margins getMargins() { 231 return null; 232 } 233 234 @Override 235 public @NonNull List<String> getAttributeSources() { 236 return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList(); 237 } 238 239 public void setAttributeSources(List<String> attributeSources) { 240 mAttributeSources = attributeSources; 241 } 242 243 /** Create a test node from the given XML */ 244 public static TestNode createFromXml(String xml) { 245 Document document = DomUtilities.parseDocument(xml, false); 246 assertNotNull(document); 247 assertNotNull(document.getDocumentElement()); 248 249 return createFromNode(document.getDocumentElement()); 250 } 251 252 public static String toXml(TestNode node) { 253 assertTrue("This method only works with nodes constructed from XML", 254 node instanceof TestXmlNode); 255 Document document = ((TestXmlNode) node).mElement.getOwnerDocument(); 256 // Insert new whitespace nodes etc 257 String xml = dumpDocument(document); 258 document = DomUtilities.parseDocument(xml, false); 259 260 XmlPrettyPrinter printer = new XmlPrettyPrinter(XmlFormatPreferences.create(), 261 XmlFormatStyle.LAYOUT, "\n"); 262 StringBuilder sb = new StringBuilder(1000); 263 sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); 264 printer.prettyPrint(-1, document, null, null, sb, false); 265 return sb.toString(); 266 } 267 268 @SuppressWarnings("deprecation") 269 private static String dumpDocument(Document document) { 270 // Diagnostics: print out the XML that we're about to render 271 org.apache.xml.serialize.OutputFormat outputFormat = 272 new org.apache.xml.serialize.OutputFormat( 273 "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$ 274 outputFormat.setIndent(2); 275 outputFormat.setLineWidth(100); 276 outputFormat.setIndenting(true); 277 outputFormat.setOmitXMLDeclaration(true); 278 outputFormat.setOmitDocumentType(true); 279 StringWriter stringWriter = new StringWriter(); 280 // Using FQN here to avoid having an import above, which will result 281 // in a deprecation warning, and there isn't a way to annotate a single 282 // import element with a SuppressWarnings. 283 org.apache.xml.serialize.XMLSerializer serializer = 284 new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat); 285 serializer.setNamespaces(true); 286 try { 287 serializer.serialize(document.getDocumentElement()); 288 return stringWriter.toString(); 289 } catch (IOException e) { 290 e.printStackTrace(); 291 } 292 return null; 293 } 294 295 private static TestNode createFromNode(Element element) { 296 String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName(); 297 TestNode node = new TestXmlNode(fqcn, element); 298 299 for (Element child : DomUtilities.getChildren(element)) { 300 node.add(createFromNode(child)); 301 } 302 303 return node; 304 } 305 306 @Nullable 307 public static TestNode findById(TestNode node, String id) { 308 id = BaseLayoutRule.stripIdPrefix(id); 309 return node.findById(id); 310 } 311 312 private TestNode findById(String targetId) { 313 String id = getStringAttr(ANDROID_URI, ATTR_ID); 314 if (id != null && targetId.equals(BaseLayoutRule.stripIdPrefix(id))) { 315 return this; 316 } 317 318 for (TestNode child : mChildren) { 319 TestNode result = child.findById(targetId); 320 if (result != null) { 321 return result; 322 } 323 } 324 325 return null; 326 } 327 328 private static String getTagName(String fqcn) { 329 return fqcn.substring(fqcn.lastIndexOf('.') + 1); 330 } 331 332 private static class TestXmlNode extends TestNode { 333 private final Element mElement; 334 335 public TestXmlNode(String fqcn, Element element) { 336 super(fqcn); 337 mElement = element; 338 } 339 340 @Override 341 public @NonNull IAttribute[] getLiveAttributes() { 342 List<IAttribute> result = new ArrayList<IAttribute>(); 343 344 NamedNodeMap attributes = mElement.getAttributes(); 345 for (int i = 0, n = attributes.getLength(); i < n; i++) { 346 Attr attribute = (Attr) attributes.item(i); 347 result.add(new TestXmlAttribute(attribute)); 348 } 349 return result.toArray(new IAttribute[result.size()]); 350 } 351 352 @Override 353 public boolean setAttribute(String uri, String localName, String value) { 354 if (value == null) { 355 mElement.removeAttributeNS(uri, localName); 356 } else { 357 mElement.setAttributeNS(uri, localName, value); 358 } 359 return super.setAttribute(uri, localName, value); 360 } 361 362 @Override 363 public INode appendChild(String viewFqcn) { 364 Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn)); 365 mElement.appendChild(child); 366 return new TestXmlNode(viewFqcn, child); 367 } 368 369 @Override 370 public INode insertChildAt(String viewFqcn, int index) { 371 if (index == -1) { 372 return appendChild(viewFqcn); 373 } 374 Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn)); 375 List<Element> children = DomUtilities.getChildren(mElement); 376 if (children.size() >= index) { 377 Element before = children.get(index); 378 mElement.insertBefore(child, before); 379 } else { 380 fail("Unexpected index"); 381 mElement.appendChild(child); 382 } 383 return new TestXmlNode(viewFqcn, child); 384 } 385 386 @Override 387 public String getStringAttr(String uri, String name) { 388 String value; 389 if (uri == null) { 390 value = mElement.getAttribute(name); 391 } else { 392 value = mElement.getAttributeNS(uri, name); 393 } 394 if (value.isEmpty()) { 395 value = null; 396 } 397 398 return value; 399 } 400 401 @Override 402 public void removeChild(INode node) { 403 assert node instanceof TestXmlNode; 404 mElement.removeChild(((TestXmlNode) node).mElement); 405 } 406 407 @Override 408 public void removeChild(int index) { 409 List<Element> children = DomUtilities.getChildren(mElement); 410 assertTrue(index < children.size()); 411 Element oldChild = children.get(index); 412 mElement.removeChild(oldChild); 413 } 414 } 415 416 public static class TestXmlAttribute implements IAttribute { 417 private Attr mAttribute; 418 419 public TestXmlAttribute(Attr attribute) { 420 this.mAttribute = attribute; 421 } 422 423 @Override 424 public String getUri() { 425 return mAttribute.getNamespaceURI(); 426 } 427 428 @Override 429 public String getName() { 430 String name = mAttribute.getLocalName(); 431 if (name == null) { 432 name = mAttribute.getName(); 433 } 434 return name; 435 } 436 437 @Override 438 public String getValue() { 439 return mAttribute.getValue(); 440 } 441 } 442 443 // Recursively initialize this node with the bounds specified in the given hierarchy 444 // dump (from ViewHierarchy's DUMP_INFO flag 445 public void assignBounds(String bounds) { 446 Iterable<String> split = Splitter.on('\n').trimResults().split(bounds); 447 assignBounds(split.iterator()); 448 } 449 450 private void assignBounds(Iterator<String> iterator) { 451 assertTrue(iterator.hasNext()); 452 String desc = iterator.next(); 453 454 Pattern pattern = Pattern.compile("^\\s*(.+)\\s+\\[(.+)\\]\\s*(<.+>)?\\s*(\\S+)?\\s*$"); 455 Matcher matcher = pattern.matcher(desc); 456 assertTrue(matcher.matches()); 457 String fqn = matcher.group(1); 458 assertEquals(getFqcn(), fqn); 459 String boundsString = matcher.group(2); 460 String[] bounds = boundsString.split(","); 461 assertEquals(boundsString, 4, bounds.length); 462 try { 463 int left = Integer.parseInt(bounds[0]); 464 int top = Integer.parseInt(bounds[1]); 465 int right = Integer.parseInt(bounds[2]); 466 int bottom = Integer.parseInt(bounds[3]); 467 mBounds = new Rect(left, top, right - left, bottom - top); 468 } catch (NumberFormatException nufe) { 469 Assert.fail(nufe.getLocalizedMessage()); 470 } 471 String tag = matcher.group(3); 472 473 for (INode child : getChildren()) { 474 assertTrue(iterator.hasNext()); 475 ((TestNode) child).assignBounds(iterator); 476 } 477 } 478} 479