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