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