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