Home | History | Annotate | Download | only in layout
      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 
     73     public TestNode(String fqcn) {
     74         this.mFqcn = fqcn;
     75     }
     76 
     77     public TestNode bounds(Rect bounds) {
     78         this.mBounds = bounds;
     79 
     80         return this;
     81     }
     82 
     83     public TestNode id(String id) {
     84         return set(ANDROID_URI, ATTR_ID, id);
     85     }
     86 
     87     public TestNode set(String uri, String name, String value) {
     88         setAttribute(uri, name, value);
     89 
     90         return this;
     91     }
     92 
     93     public TestNode add(TestNode child) {
     94         mChildren.add(child);
     95         child.mParent = this;
     96 
     97         return this;
     98     }
     99 
    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 
    109     public static TestNode create(String fcqn) {
    110         return new TestNode(fcqn);
    111     }
    112 
    113     public void removeChild(int index) {
    114         TestNode removed = mChildren.remove(index);
    115         removed.mParent = null;
    116     }
    117 
    118     // ==== INODE ====
    119 
    120     @Override
    121     public @NonNull INode appendChild(@NonNull String viewFqcn) {
    122         return insertChildAt(viewFqcn, mChildren.size());
    123     }
    124 
    125     @Override
    126     public void editXml(@NonNull String undoName, @NonNull INodeHandler callback) {
    127         callback.handle(this);
    128     }
    129 
    130     public void putAttributeInfo(String uri, String attrName, IAttributeInfo info) {
    131         mAttributeInfos.put(uri + attrName, info);
    132     }
    133 
    134     @Override
    135     public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) {
    136         return mAttributeInfos.get(uri + attrName);
    137     }
    138 
    139     @Override
    140     public @NonNull Rect getBounds() {
    141         return mBounds;
    142     }
    143 
    144     @Override
    145     public @NonNull INode[] getChildren() {
    146         return mChildren.toArray(new INode[mChildren.size()]);
    147     }
    148 
    149     @Override
    150     public @NonNull IAttributeInfo[] getDeclaredAttributes() {
    151         return mAttributeInfos.values().toArray(new IAttributeInfo[mAttributeInfos.size()]);
    152     }
    153 
    154     @Override
    155     public @NonNull String getFqcn() {
    156         return mFqcn;
    157     }
    158 
    159     @Override
    160     public @NonNull IAttribute[] getLiveAttributes() {
    161         return mAttributes.values().toArray(new IAttribute[mAttributes.size()]);
    162     }
    163 
    164     @Override
    165     public INode getParent() {
    166         return mParent;
    167     }
    168 
    169     @Override
    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
    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
    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
    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
    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
    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
    224     public int getBaseline() {
    225         return -1;
    226     }
    227 
    228     @Override
    229     public @NonNull Margins getMargins() {
    230         return null;
    231     }
    232 
    233     @Override
    234     public @NonNull List<String> getAttributeSources() {
    235         return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList();
    236     }
    237 
    238     public void setAttributeSources(List<String> attributeSources) {
    239         mAttributeSources = attributeSources;
    240     }
    241 
    242     /** Create a test node from the given 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 
    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")
    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 
    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
    306     public static TestNode findById(TestNode node, String id) {
    307         id = BaseLayoutRule.stripIdPrefix(id);
    308         return node.findById(id);
    309     }
    310 
    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 
    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 
    334         public TestXmlNode(String fqcn, Element element) {
    335             super(fqcn);
    336             mElement = element;
    337         }
    338 
    339         @Override
    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
    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
    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
    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
    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
    401         public void removeChild(INode node) {
    402             assert node instanceof TestXmlNode;
    403             mElement.removeChild(((TestXmlNode) node).mElement);
    404         }
    405 
    406         @Override
    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