UiElementNode.java revision 6681db8624e514030730e3c52192330aec5279a9
1/*
2 * Copyright (C) 2007 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
17package com.android.ide.eclipse.adt.internal.editors.uimodel;
18
19import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME;
20import static com.android.ide.common.layout.LayoutConstants.ANDROID_PKG_PREFIX;
21import static com.android.ide.common.layout.LayoutConstants.ANDROID_SUPPORT_PKG_PREFIX;
22import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS;
23import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
24import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
25import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
26import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_URI;
27import static com.android.sdklib.SdkConstants.NS_RESOURCES;
28import static com.android.tools.lint.detector.api.LintConstants.XMLNS_PREFIX;
29
30import com.android.annotations.Nullable;
31import com.android.annotations.VisibleForTesting;
32import com.android.ide.common.api.IAttributeInfo.Format;
33import com.android.ide.common.resources.platform.AttributeInfo;
34import com.android.ide.eclipse.adt.AdtPlugin;
35import com.android.ide.eclipse.adt.AdtUtils;
36import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
37import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
38import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
39import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
40import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
41import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
42import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
43import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
44import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
45import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
46import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
47import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors;
48import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
49import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors;
50import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
51import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
52import com.android.sdklib.SdkConstants;
53
54import org.eclipse.jface.text.TextUtilities;
55import org.eclipse.jface.viewers.StyledString;
56import org.eclipse.ui.views.properties.IPropertyDescriptor;
57import org.eclipse.ui.views.properties.IPropertySource;
58import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
59import org.eclipse.wst.xml.core.internal.document.ElementImpl;
60import org.w3c.dom.Attr;
61import org.w3c.dom.Document;
62import org.w3c.dom.Element;
63import org.w3c.dom.NamedNodeMap;
64import org.w3c.dom.Node;
65import org.w3c.dom.Text;
66
67import java.util.ArrayList;
68import java.util.Collection;
69import java.util.Collections;
70import java.util.HashMap;
71import java.util.HashSet;
72import java.util.List;
73import java.util.Locale;
74import java.util.Map;
75import java.util.Map.Entry;
76import java.util.Set;
77
78/**
79 * Represents an XML node that can be modified by the user interface in the XML editor.
80 * <p/>
81 * Each tree viewer used in the application page's parts needs to keep a model representing
82 * each underlying node in the tree. This interface represents the base type for such a node.
83 * <p/>
84 * Each node acts as an intermediary model between the actual XML model (the real data support)
85 * and the tree viewers or the corresponding page parts.
86 * <p/>
87 * Element nodes don't contain data per se. Their data is contained in their attributes
88 * as well as their children's attributes, see {@link UiAttributeNode}.
89 * <p/>
90 * The structure of a given {@link UiElementNode} is declared by a corresponding
91 * {@link ElementDescriptor}.
92 * <p/>
93 * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
94 * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
95 */
96@SuppressWarnings("restriction") // XML model
97public class UiElementNode implements IPropertySource {
98
99    /** List of prefixes removed from android:id strings when creating short descriptions. */
100    private static String[] ID_PREFIXES = {
101        "@android:id/", //$NON-NLS-1$
102        NEW_ID_PREFIX, ID_PREFIX, "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$
103
104    /** The element descriptor for the node. Always present, never null. */
105    private ElementDescriptor mDescriptor;
106    /** The parent element node in the UI model. It is null for a root element or until
107     *  the node is attached to its parent. */
108    private UiElementNode mUiParent;
109    /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the
110     *  root node. All children have the value set to null and query their parent. */
111    private AndroidXmlEditor mEditor;
112    /** The XML {@link Document} model that is being mirror by the UI model. This is defined
113     *  only for the root node. All children have the value set to null and query their parent. */
114    private Document mXmlDocument;
115    /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
116     *  have no corresponding XML node or for new UI nodes before their XML node is set. */
117    private Node mXmlNode;
118    /** The list of all UI children nodes. Can be empty but never null. There's one UI children
119     *  node per existing XML children node. */
120    private ArrayList<UiElementNode> mUiChildren;
121    /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
122     *  The list is always defined and never null. Unlike the UiElementNode children list, this
123     *  is always defined, even for attributes that do not exist in the XML model - that's because
124     *  "missing" attributes in the XML model simply mean a default value is used. Also note that
125     *  the underlying collection is a map, so order is not respected. To get the desired attribute
126     *  order, iterate through the {@link ElementDescriptor}'s attribute list. */
127    private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
128    private HashSet<UiAttributeNode> mUnknownUiAttributes;
129    /** A read-only view of the UI children node collection. */
130    private List<UiElementNode> mReadOnlyUiChildren;
131    /** A read-only view of the UI attributes collection. */
132    private Collection<UiAttributeNode> mCachedAllUiAttributes;
133    /** A map of hidden attribute descriptors. Key is the XML name. */
134    private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
135    /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
136     *  listeners attached, so the list is only created on demand and can be null. */
137    private List<IUiUpdateListener> mUiUpdateListeners;
138    /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names.
139     *  The default is to have one that creates new {@link ElementDescriptor}. */
140    private IUnknownDescriptorProvider mUnknownDescProvider;
141    /** Error Flag */
142    private boolean mHasError;
143
144    /**
145     * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
146     *
147     * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
148     */
149    public UiElementNode(ElementDescriptor elementDescriptor) {
150        mDescriptor = elementDescriptor;
151        clearContent();
152    }
153
154    @Override
155    public String toString() {
156      return String.format("%s [desc: %s, parent: %s, children: %d]",         //$NON-NLS-1$
157              this.getClass().getSimpleName(),
158              mDescriptor,
159              mUiParent != null ? mUiParent.toString() : "none",              //$NON-NLS-1$
160                      mUiChildren != null ? mUiChildren.size() : 0
161      );
162    }
163
164    /**
165     * Clears the {@link UiElementNode} by resetting the children list and
166     * the {@link UiAttributeNode}s list.
167     * Also resets the attached XML node, document, editor if any.
168     * <p/>
169     * The parent {@link UiElementNode} node is not reset so that it's position
170     * in the hierarchy be left intact, if any.
171     */
172    /* package */ void clearContent() {
173        mXmlNode = null;
174        mXmlDocument = null;
175        mEditor = null;
176        clearAttributes();
177        mReadOnlyUiChildren = null;
178        if (mUiChildren == null) {
179            mUiChildren = new ArrayList<UiElementNode>();
180        } else {
181            // We can't remove mandatory nodes, we just clear them.
182            for (int i = mUiChildren.size() - 1; i >= 0; --i) {
183                removeUiChildAtIndex(i);
184            }
185        }
186    }
187
188    /**
189     * Clears the internal list of attributes, the read-only cached version of it
190     * and the read-only cached hidden attribute list.
191     */
192    private void clearAttributes() {
193        mUiAttributes = null;
194        mCachedAllUiAttributes = null;
195        mCachedHiddenAttributes = null;
196        mUnknownUiAttributes = new HashSet<UiAttributeNode>();
197    }
198
199    /**
200     * Gets or creates the internal UiAttributes list.
201     * <p/>
202     * When the descriptor derives from ViewElementDescriptor, this list depends on the
203     * current UiParent node.
204     *
205     * @return A new set of {@link UiAttributeNode} that matches the expected
206     *         attributes for this node.
207     */
208    private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
209        if (mUiAttributes == null) {
210            AttributeDescriptor[] attrList = getAttributeDescriptors();
211            mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attrList.length);
212            for (AttributeDescriptor desc : attrList) {
213                UiAttributeNode uiNode = desc.createUiNode(this);
214                if (uiNode != null) {  // Some AttributeDescriptors do not have UI associated
215                    mUiAttributes.put(desc, uiNode);
216                }
217            }
218        }
219        return mUiAttributes;
220    }
221
222    /**
223     * Computes a short string describing the UI node suitable for tree views.
224     * Uses the element's attribute "android:name" if present, or the "android:label" one
225     * followed by the element's name if not repeated.
226     *
227     * @return A short string describing the UI node suitable for tree views.
228     */
229    public String getShortDescription() {
230        String name = mDescriptor.getUiName();
231        String attr = getDescAttribute();
232        if (attr != null) {
233            // If the ui name is repeated in the attribute value, don't use it.
234            // Typical case is to avoid ".pkg.MyActivity (Activity)".
235            if (attr.contains(name)) {
236                return attr;
237            } else {
238                return String.format("%1$s (%2$s)", attr, name);
239            }
240        }
241
242        return name;
243    }
244
245    /** Returns the key attribute that can be used to describe this node, or null */
246    private String getDescAttribute() {
247        if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
248            // Application and Manifest nodes have a special treatment: they are unique nodes
249            // so we don't bother trying to differentiate their strings and we fall back to
250            // just using the UI name below.
251            Element elem = (Element) mXmlNode;
252
253            String attr = _Element_getAttributeNS(elem,
254                                SdkConstants.NS_RESOURCES,
255                                AndroidManifestDescriptors.ANDROID_NAME_ATTR);
256            if (attr == null || attr.length() == 0) {
257                attr = _Element_getAttributeNS(elem,
258                                SdkConstants.NS_RESOURCES,
259                                AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
260            } else if (mXmlNode.getNodeName().equals(LayoutDescriptors.VIEW_FRAGMENT)) {
261                attr = attr.substring(attr.lastIndexOf('.') + 1);
262            }
263            if (attr == null || attr.length() == 0) {
264                attr = _Element_getAttributeNS(elem,
265                                SdkConstants.NS_RESOURCES,
266                                OtherXmlDescriptors.PREF_KEY_ATTR);
267            }
268            if (attr == null || attr.length() == 0) {
269                attr = _Element_getAttributeNS(elem,
270                                null, // no namespace
271                                ValuesDescriptors.NAME_ATTR);
272            }
273            if (attr == null || attr.length() == 0) {
274                attr = _Element_getAttributeNS(elem,
275                                SdkConstants.NS_RESOURCES,
276                                LayoutDescriptors.ID_ATTR);
277
278                if (attr != null && attr.length() > 0) {
279                    for (String prefix : ID_PREFIXES) {
280                        if (attr.startsWith(prefix)) {
281                            attr = attr.substring(prefix.length());
282                            break;
283                        }
284                    }
285                }
286            }
287            if (attr != null && attr.length() > 0) {
288                return attr;
289            }
290        }
291
292        return null;
293    }
294
295    /**
296     * Computes a styled string describing the UI node suitable for tree views.
297     * Similar to {@link #getShortDescription()} but styles the Strings.
298     *
299     * @return A styled string describing the UI node suitable for tree views.
300     */
301    public StyledString getStyledDescription() {
302        String uiName = mDescriptor.getUiName();
303
304        // Special case: for <view>, show the class attribute value instead.
305        // This is done here rather than in the descriptor since this depends on
306        // node instance data.
307        if (LayoutDescriptors.VIEW_VIEWTAG.equals(uiName) && mXmlNode instanceof Element) {
308            Element element = (Element) mXmlNode;
309            String cls = element.getAttribute(ATTR_CLASS);
310            if (cls != null) {
311                uiName = cls.substring(cls.lastIndexOf('.') + 1);
312            }
313        }
314
315        StyledString styledString = new StyledString();
316        String attr = getDescAttribute();
317        if (attr != null) {
318            // Don't append the two when it's a repeat, e.g. Button01 (Button),
319            // only when the ui name is not part of the attribute
320            if (attr.toLowerCase(Locale.US).indexOf(uiName.toLowerCase(Locale.US)) == -1) {
321                styledString.append(attr);
322                styledString.append(String.format(" (%1$s)", uiName),
323                        StyledString.DECORATIONS_STYLER);
324            } else {
325                styledString.append(attr);
326            }
327        }
328
329        if (styledString.length() == 0) {
330            styledString.append(uiName);
331        }
332
333        return styledString;
334    }
335
336    /**
337     * Retrieves an attribute value by local name and namespace URI.
338     * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
339     * , applications must use the value <code>null</code> as the
340     * <code>namespaceURI</code> parameter for methods if they wish to have
341     * no namespace.
342     * <p/>
343     * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
344     * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
345     * This wrapper will return an empty string instead.
346     *
347     * @see Element#getAttributeNS(String, String)
348     * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
349     * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string.
350     */
351    private String _Element_getAttributeNS(Element element,
352            String namespaceURI,
353            String localName) {
354        try {
355            return element.getAttributeNS(namespaceURI, localName);
356        } catch (Exception ignore) {
357            return "";
358        }
359    }
360
361    /**
362     * Computes a "breadcrumb trail" description for this node.
363     * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
364     *
365     * @param includeRoot Whether to include the root (e.g. "Manifest") or not. Has no effect
366     *                     when called on the root node itself.
367     * @return The "breadcrumb trail" description for this node.
368     */
369    public String getBreadcrumbTrailDescription(boolean includeRoot) {
370        StringBuilder sb = new StringBuilder(getShortDescription());
371
372        for (UiElementNode uiNode = getUiParent();
373                uiNode != null;
374                uiNode = uiNode.getUiParent()) {
375            if (!includeRoot && uiNode.getUiParent() == null) {
376                break;
377            }
378            sb.insert(0, String.format("%1$s > ", uiNode.getShortDescription())); //$NON-NLS-1$
379        }
380
381        return sb.toString();
382    }
383
384    /**
385     * Sets the XML {@link Document}.
386     * <p/>
387     * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
388     * UI root element node (this method takes care of that.)
389     * @param xmlDoc The new XML document to associate this node with.
390     */
391    public void setXmlDocument(Document xmlDoc) {
392        if (mUiParent == null) {
393            mXmlDocument = xmlDoc;
394        } else {
395            mUiParent.setXmlDocument(xmlDoc);
396        }
397    }
398
399    /**
400     * Returns the XML {@link Document}.
401     * <p/>
402     * The value is initially null until the UI node is attached to its UI parent -- the value
403     * of the document is then propagated.
404     *
405     * @return the XML {@link Document} or the parent's XML {@link Document} or null.
406     */
407    public Document getXmlDocument() {
408        if (mXmlDocument != null) {
409            return mXmlDocument;
410        } else if (mUiParent != null) {
411            return mUiParent.getXmlDocument();
412        }
413        return null;
414    }
415
416    /**
417     * Returns the XML node associated with this UI node.
418     * <p/>
419     * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
420     * corresponding UI node will exist even if there is no corresponding XML node. Such structure
421     * is created and enforced by the parent of the tree, not the element themselves. However
422     * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
423     *
424     * @return The associated XML node. Can be null for mandatory nodes.
425     */
426    public Node getXmlNode() {
427        return mXmlNode;
428    }
429
430    /**
431     * Returns the {@link ElementDescriptor} for this node. This is never null.
432     * <p/>
433     * Do not use this to call getDescriptor().getAttributes(), instead call
434     * getAttributeDescriptors() which can be overridden by derived classes.
435     * @return The {@link ElementDescriptor} for this node. This is never null.
436     */
437    public ElementDescriptor getDescriptor() {
438        return mDescriptor;
439    }
440
441    /**
442     * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
443     * <p/>
444     * Use this instead of getDescriptor().getAttributes() -- derived classes can override
445     * this to manipulate the attribute descriptor list depending on the current UI node.
446     * @return The {@link AttributeDescriptor} array for the descriptor of this node.
447     */
448    public AttributeDescriptor[] getAttributeDescriptors() {
449        return mDescriptor.getAttributes();
450    }
451
452    /**
453     * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
454     * This is a subset of the getAttributeDescriptors() list.
455     * <p/>
456     * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
457     * could override this to manipulate the attribute descriptor list depending on the current
458     * UI node. There's no need for it right now so keep it private.
459     */
460    private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
461        if (mCachedHiddenAttributes == null) {
462            mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
463            for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
464                if (attrDesc instanceof XmlnsAttributeDescriptor) {
465                    mCachedHiddenAttributes.put(
466                            ((XmlnsAttributeDescriptor) attrDesc).getXmlNsName(),
467                            attrDesc);
468                }
469            }
470        }
471        return mCachedHiddenAttributes;
472    }
473
474    /**
475     * Sets the parent of this UiElementNode.
476     * <p/>
477     * The root node has no parent.
478     */
479    protected void setUiParent(UiElementNode parent) {
480        mUiParent = parent;
481        // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
482        clearAttributes();
483    }
484
485    /**
486     * @return The parent {@link UiElementNode} or null if this is the root node.
487     */
488    public UiElementNode getUiParent() {
489        return mUiParent;
490    }
491
492    /**
493     * Returns the root {@link UiElementNode}.
494     *
495     * @return The root {@link UiElementNode}.
496     */
497    public UiElementNode getUiRoot() {
498        UiElementNode root = this;
499        while (root.mUiParent != null) {
500            root = root.mUiParent;
501        }
502
503        return root;
504    }
505
506    /**
507     * Returns the index of this sibling (where the first child has index 0, the second child
508     * has index 1, and so on.)
509     *
510     * @return The sibling index of this node
511     */
512    public int getUiSiblingIndex() {
513        if (mUiParent != null) {
514            int index = 0;
515            for (UiElementNode node : mUiParent.getUiChildren()) {
516                if (node == this) {
517                    break;
518                }
519                index++;
520            }
521            return index;
522        }
523
524        return 0;
525    }
526
527    /**
528     * Returns the previous UI sibling of this UI node. If the node does not have a previous
529     * sibling, returns null.
530     *
531     * @return The previous UI sibling of this UI node, or null if not applicable.
532     */
533    public UiElementNode getUiPreviousSibling() {
534        if (mUiParent != null) {
535            List<UiElementNode> childlist = mUiParent.getUiChildren();
536            if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
537                int index = childlist.indexOf(this);
538                return index > 0 ? childlist.get(index - 1) : null;
539            }
540        }
541        return null;
542    }
543
544    /**
545     * Returns the next UI sibling of this UI node.
546     * If the node does not have a next sibling, returns null.
547     *
548     * @return The next UI sibling of this UI node, or null.
549     */
550    public UiElementNode getUiNextSibling() {
551        if (mUiParent != null) {
552            List<UiElementNode> childlist = mUiParent.getUiChildren();
553            if (childlist != null) {
554                int size = childlist.size();
555                if (size > 1 && childlist.get(size - 1) != this) {
556                    int index = childlist.indexOf(this);
557                    return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
558                }
559            }
560        }
561        return null;
562    }
563
564    /**
565     * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
566     * <p/>
567     * The editor must always be set on the root node. This method takes care of that.
568     *
569     * @param editor The editor to associate this node with.
570     */
571    public void setEditor(AndroidXmlEditor editor) {
572        if (mUiParent == null) {
573            mEditor = editor;
574        } else {
575            mUiParent.setEditor(editor);
576        }
577    }
578
579    /**
580     * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}.
581     * <p/>
582     * The value is initially null until the node is attached to its parent -- the value
583     * of the root node is then propagated.
584     *
585     * @return The embedding {@link AndroidXmlEditor} or null.
586     */
587    public AndroidXmlEditor getEditor() {
588        return mUiParent == null ? mEditor : mUiParent.getEditor();
589    }
590
591    /**
592     * Returns the Android target data for the file being edited.
593     *
594     * @return The Android target data for the file being edited.
595     */
596    public AndroidTargetData getAndroidTarget() {
597        return getEditor().getTargetData();
598    }
599
600    /**
601     * @return A read-only version of the children collection.
602     */
603    public List<UiElementNode> getUiChildren() {
604        if (mReadOnlyUiChildren == null) {
605            mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
606        }
607        return mReadOnlyUiChildren;
608    }
609
610    /**
611     * Returns a collection containing all the known attributes as well as
612     * all the unknown ui attributes.
613     *
614     * @return A read-only version of the attributes collection.
615     */
616    public Collection<UiAttributeNode> getAllUiAttributes() {
617        if (mCachedAllUiAttributes == null) {
618
619            List<UiAttributeNode> allValues =
620                new ArrayList<UiAttributeNode>(getInternalUiAttributes().values());
621            allValues.addAll(mUnknownUiAttributes);
622
623            mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues);
624        }
625        return mCachedAllUiAttributes;
626    }
627
628    /**
629     * Returns all the unknown ui attributes, that is those we found defined in the
630     * actual XML but that we don't have descriptors for.
631     *
632     * @return A read-only version of the unknown attributes collection.
633     */
634    public Collection<UiAttributeNode> getUnknownUiAttributes() {
635        return Collections.unmodifiableCollection(mUnknownUiAttributes);
636    }
637
638    /**
639     * Sets the error flag value.
640     *
641     * @param errorFlag the error flag
642     */
643    public final void setHasError(boolean errorFlag) {
644        mHasError = errorFlag;
645    }
646
647    /**
648     * Returns whether this node, its attributes, or one of the children nodes (and attributes)
649     * has errors.
650     *
651     * @return True if this node, its attributes, or one of the children nodes (and attributes)
652     * has errors.
653     */
654    public final boolean hasError() {
655        if (mHasError) {
656            return true;
657        }
658
659        // get the error value from the attributes.
660        for (UiAttributeNode attribute : getAllUiAttributes()) {
661            if (attribute.hasError()) {
662                return true;
663            }
664        }
665
666        // and now from the children.
667        for (UiElementNode child : mUiChildren) {
668            if (child.hasError()) {
669                return true;
670            }
671        }
672
673        return false;
674    }
675
676    /**
677     * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped
678     * XML names.
679     * <p/>
680     * The default is to have one that creates new {@link ElementDescriptor}.
681     * <p/>
682     * There is only one such provider in any UI model tree, attached to the root node.
683     *
684     * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null.
685     */
686    public IUnknownDescriptorProvider getUnknownDescriptorProvider() {
687        if (mUiParent != null) {
688            return mUiParent.getUnknownDescriptorProvider();
689        }
690        if (mUnknownDescProvider == null) {
691            // Create the default one on demand.
692            mUnknownDescProvider = new IUnknownDescriptorProvider() {
693
694                private final HashMap<String, ElementDescriptor> mMap =
695                    new HashMap<String, ElementDescriptor>();
696
697                /**
698                 * The default is to create a new ElementDescriptor wrapping
699                 * the unknown XML local name and reuse previously created descriptors.
700                 */
701                @Override
702                public ElementDescriptor getDescriptor(String xmlLocalName) {
703
704                    ElementDescriptor desc = mMap.get(xmlLocalName);
705
706                    if (desc == null) {
707                        desc = new ElementDescriptor(xmlLocalName);
708                        mMap.put(xmlLocalName, desc);
709                    }
710
711                    return desc;
712                }
713            };
714        }
715        return mUnknownDescProvider;
716    }
717
718    /**
719     * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped
720     * XML names.
721     * <p/>
722     * The default is to have one that creates new {@link ElementDescriptor}.
723     * <p/>
724     * There is only one such provider in any UI model tree, attached to the root node.
725     *
726     * @param unknownDescProvider The new provider to use. Must not be null.
727     */
728    public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) {
729        if (mUiParent == null) {
730            mUnknownDescProvider = unknownDescProvider;
731        } else {
732            mUiParent.setUnknownDescriptorProvider(unknownDescProvider);
733        }
734    }
735
736    /**
737     * Adds a new {@link IUiUpdateListener} to the internal update listener list.
738     *
739     * @param listener The listener to add.
740     */
741    public void addUpdateListener(IUiUpdateListener listener) {
742       if (mUiUpdateListeners == null) {
743           mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
744       }
745       if (!mUiUpdateListeners.contains(listener)) {
746           mUiUpdateListeners.add(listener);
747       }
748    }
749
750    /**
751     * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
752     * Does nothing if the list is empty or the listener is not registered.
753     *
754     * @param listener The listener to remove.
755     */
756    public void removeUpdateListener(IUiUpdateListener listener) {
757       if (mUiUpdateListeners != null) {
758           mUiUpdateListeners.remove(listener);
759       }
760    }
761
762    /**
763     * Finds a child node relative to this node using a path-like expression.
764     * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
765     * returns the latter. If there are multiple nodes with the same name at the same
766     * level, always uses the first one found.
767     *
768     * @param path The path like expression to select a child node.
769     * @return The ui node found or null.
770     */
771    public UiElementNode findUiChildNode(String path) {
772        String[] items = path.split("/");  //$NON-NLS-1$
773        UiElementNode uiNode = this;
774        for (String item : items) {
775            boolean nextSegment = false;
776            for (UiElementNode c : uiNode.mUiChildren) {
777                if (c.getDescriptor().getXmlName().equals(item)) {
778                    uiNode = c;
779                    nextSegment = true;
780                    break;
781                }
782            }
783            if (!nextSegment) {
784                return null;
785            }
786        }
787        return uiNode;
788    }
789
790    /**
791     * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
792     * Looks recursively in all children UI nodes.
793     *
794     * @param xmlNode The XML node to look for.
795     * @return The {@link UiElementNode} that contains xmlNode or null if not found,
796     */
797    public UiElementNode findXmlNode(Node xmlNode) {
798        if (xmlNode == null) {
799            return null;
800        }
801        if (getXmlNode() == xmlNode) {
802            return this;
803        }
804
805        for (UiElementNode uiChild : mUiChildren) {
806            UiElementNode found = uiChild.findXmlNode(xmlNode);
807            if (found != null) {
808                return found;
809            }
810        }
811
812        return null;
813    }
814
815    /**
816     * Returns the {@link UiAttributeNode} matching this attribute descriptor or
817     * null if not found.
818     *
819     * @param attrDesc The {@link AttributeDescriptor} to match.
820     * @return the {@link UiAttributeNode} matching this attribute descriptor or null
821     *         if not found.
822     */
823    public UiAttributeNode findUiAttribute(AttributeDescriptor attrDesc) {
824        return getInternalUiAttributes().get(attrDesc);
825    }
826
827    /**
828     * Populate this element node with all values from the given XML node.
829     *
830     * This fails if the given XML node has a different element name -- it won't change the
831     * type of this ui node.
832     *
833     * This method can be both used for populating values the first time and updating values
834     * after the XML model changed.
835     *
836     * @param xmlNode The XML node to mirror
837     * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
838     */
839    public boolean loadFromXmlNode(Node xmlNode) {
840        boolean structureChanged = (mXmlNode != xmlNode);
841        mXmlNode = xmlNode;
842        if (xmlNode != null) {
843            updateAttributeList(xmlNode);
844            structureChanged |= updateElementList(xmlNode);
845            invokeUiUpdateListeners(structureChanged ? UiUpdateState.CHILDREN_CHANGED
846                                                      : UiUpdateState.ATTR_UPDATED);
847        }
848        return structureChanged;
849    }
850
851    /**
852     * Clears the UI node and reload it from the given XML node.
853     * <p/>
854     * This works by clearing all references to any previous XML or UI nodes and
855     * then reloads the XML document from scratch. The editor reference is kept.
856     * <p/>
857     * This is used in the special case where the ElementDescriptor structure has changed.
858     * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
859     * and reload everything. This is not subtle and should be used very rarely.
860     *
861     * @param xmlNode The XML node or document to reload. Can be null.
862     */
863    public void reloadFromXmlNode(Node xmlNode) {
864        // The editor needs to be preserved, it is not affected by an XML change.
865        AndroidXmlEditor editor = getEditor();
866        clearContent();
867        setEditor(editor);
868        if (xmlNode != null) {
869            setXmlDocument(xmlNode.getOwnerDocument());
870        }
871        // This will reload all the XML and recreate the UI structure from scratch.
872        loadFromXmlNode(xmlNode);
873    }
874
875    /**
876     * Called by attributes when they want to commit their value
877     * to an XML node.
878     * <p/>
879     * For mandatory nodes, this makes sure the underlying XML element node
880     * exists in the model. If not, it is created and assigned as the underlying
881     * XML node.
882     * </br>
883     * For non-mandatory nodes, simply return the underlying XML node, which
884     * must always exists.
885     *
886     * @return The XML node matching this {@link UiElementNode} or null.
887     */
888    public Node prepareCommit() {
889        if (getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
890            createXmlNode();
891            // The new XML node has been created.
892            // We don't need to refresh using loadFromXmlNode() since there are
893            // no attributes or elements that need to be loading into this node.
894        }
895        return getXmlNode();
896    }
897
898    /**
899     * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
900     * This is called by the UI when the embedding part needs to be committed.
901     */
902    public void commit() {
903        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
904            uiAttr.commit();
905        }
906    }
907
908    /**
909     * Returns true if the part has been modified with respect to the data
910     * loaded from the model.
911     * @return True if the part has been modified with respect to the data
912     * loaded from the model.
913     */
914    public boolean isDirty() {
915        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
916            if (uiAttr.isDirty()) {
917                return true;
918            }
919        }
920
921        return false;
922    }
923
924    /**
925     * Creates the underlying XML element node for this UI node if it doesn't already
926     * exists.
927     *
928     * @return The new value of getXmlNode() (can be null if creation failed)
929     */
930    public Node createXmlNode() {
931        if (mXmlNode != null) {
932            return null;
933        }
934        Node parentXmlNode = null;
935        if (mUiParent != null) {
936            parentXmlNode = mUiParent.prepareCommit();
937            if (parentXmlNode == null) {
938                // The parent failed to create its own backing XML node. Abort.
939                // No need to throw an exception, the parent will most likely
940                // have done so itself.
941                return null;
942            }
943        }
944
945        String elementName = getDescriptor().getXmlName();
946        Document doc = getXmlDocument();
947
948        // We *must* have a root node. If not, we need to abort.
949        if (doc == null) {
950            throw new RuntimeException(
951                    String.format("Missing XML document for %1$s XML node.", elementName));
952        }
953
954        // If we get here and parentXmlNode is null, the node is to be created
955        // as the root node of the document (which can't be null, cf. check above).
956        if (parentXmlNode == null) {
957            parentXmlNode = doc;
958        }
959
960        mXmlNode = doc.createElement(elementName);
961
962        // If this element does not have children, mark it as an empty tag
963        // such that the XML looks like <tag/> instead of <tag></tag>
964        if (!mDescriptor.hasChildren()) {
965            if (mXmlNode instanceof ElementImpl) {
966                ElementImpl element = (ElementImpl) mXmlNode;
967                element.setEmptyTag(true);
968            }
969        }
970
971        Node xmlNextSibling = null;
972
973        UiElementNode uiNextSibling = getUiNextSibling();
974        if (uiNextSibling != null) {
975            xmlNextSibling = uiNextSibling.getXmlNode();
976        }
977
978        Node previousTextNode = null;
979        if (xmlNextSibling != null) {
980            Node previousNode = xmlNextSibling.getPreviousSibling();
981            if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) {
982                previousTextNode = previousNode;
983            }
984        } else {
985            Node lastChild = parentXmlNode.getLastChild();
986            if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
987                previousTextNode = lastChild;
988            }
989        }
990
991        String insertAfter = null;
992
993        // Try to figure out the indentation node to insert. Even in auto-formatting
994        // we need to do this, because it turns out the XML editor's formatter does
995        // not do a very good job with completely botched up XML; it does a much better
996        // job if the new XML is already mostly well formatted. Thus, the main purpose
997        // of applying the real XML formatter after our own indentation attempts here is
998        // to make it apply its own tab-versus-spaces indentation properties, have it
999        // insert line breaks before attributes (if the user has configured that), etc.
1000
1001        // First figure out the indentation level of the newly inserted element;
1002        // this is either the same as the previous sibling, or if there is no sibling,
1003        // it's the indentation of the parent plus one indentation level.
1004        boolean isFirstChild = getUiPreviousSibling() == null
1005                || parentXmlNode.getFirstChild() == null;
1006        AndroidXmlEditor editor = getEditor();
1007        String indent;
1008        String parentIndent = ""; //$NON-NLS-1$
1009        if (isFirstChild) {
1010            indent = parentIndent = editor.getIndent(parentXmlNode);
1011            // We need to add one level of indentation. Are we using tabs?
1012            // Can't get to formatting settings so let's just look at the
1013            // parent indentation and see if we can guess
1014            if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') {
1015                indent = indent + '\t';
1016            } else {
1017                // Not using tabs, or we can't figure it out (because parent had no
1018                // indentation). In that case, indent with 4 spaces, as seems to
1019                // be the Android default.
1020                indent = indent + "    "; //$NON-NLS-1$
1021            }
1022        } else {
1023            // Find out the indent of the previous sibling
1024            indent = editor.getIndent(getUiPreviousSibling().getXmlNode());
1025        }
1026
1027        // We want to insert the new element BEFORE the text node which precedes
1028        // the next element, since that text node is the next element's indentation!
1029        if (previousTextNode != null) {
1030            xmlNextSibling = previousTextNode;
1031        } else {
1032            // If there's no previous text node, we are probably inside an
1033            // empty element (<LinearLayout>|</LinearLayout>) and in that case we need
1034            // to not only insert a newline and indentation before the new element, but
1035            // after it as well.
1036            insertAfter = parentIndent;
1037        }
1038
1039        // Insert indent text node before the new element
1040        IStructuredDocument document = editor.getStructuredDocument();
1041        String newLine;
1042        if (document != null) {
1043            newLine = TextUtilities.getDefaultLineDelimiter(document);
1044        } else {
1045            newLine = AdtUtils.getLineSeparator();
1046        }
1047        Text indentNode = doc.createTextNode(newLine + indent);
1048        parentXmlNode.insertBefore(indentNode, xmlNextSibling);
1049
1050        // Insert the element itself
1051        parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
1052
1053        // Insert a separator after the tag. We only do this when we've inserted
1054        // a tag into an area where there was no whitespace before
1055        // (e.g. a new child of <LinearLayout></LinearLayout>).
1056        if (insertAfter != null) {
1057            Text sep = doc.createTextNode(newLine + insertAfter);
1058            parentXmlNode.insertBefore(sep, xmlNextSibling);
1059        }
1060
1061        // Set all initial attributes in the XML node if they are not empty.
1062        // Iterate on the descriptor list to get the desired order and then use the
1063        // internal values, if any.
1064        List<UiAttributeNode> addAttributes = new ArrayList<UiAttributeNode>();
1065
1066        for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
1067            if (attrDesc instanceof XmlnsAttributeDescriptor) {
1068                XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attrDesc;
1069                Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI,
1070                        desc.getXmlNsName());
1071                attr.setValue(desc.getValue());
1072                attr.setPrefix(desc.getXmlNsPrefix());
1073                mXmlNode.getAttributes().setNamedItemNS(attr);
1074            } else {
1075                UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc);
1076
1077                // Don't apply the attribute immediately, instead record this attribute
1078                // such that we can gather all attributes and sort them first.
1079                // This is necessary because the XML model will *append* all attributes
1080                // so we want to add them in a particular order.
1081                // (Note that we only have to worry about UiAttributeNodes with non null
1082                // values, since this is a new node and we therefore don't need to attempt
1083                // to remove existing attributes)
1084                String value = uiAttr.getCurrentValue();
1085                if (value != null && value.length() > 0) {
1086                    addAttributes.add(uiAttr);
1087                }
1088            }
1089        }
1090
1091        // Sort and apply the attributes in order, because the Eclipse XML model will always
1092        // append the XML attributes, so by inserting them in our desired order they will
1093        // appear that way in the XML
1094        Collections.sort(addAttributes);
1095
1096        for (UiAttributeNode node : addAttributes) {
1097            commitAttributeToXml(node, node.getCurrentValue());
1098            node.setDirty(false);
1099        }
1100
1101        getEditor().scheduleNodeReformat(this, false);
1102
1103        // Notify per-node listeners
1104        invokeUiUpdateListeners(UiUpdateState.CREATED);
1105        // Notify global listeners
1106        fireNodeCreated(this, getUiSiblingIndex());
1107
1108        return mXmlNode;
1109    }
1110
1111    /**
1112     * Removes the XML node corresponding to this UI node if it exists
1113     * and also removes all mirrored information in this UI node (i.e. children, attributes)
1114     *
1115     * @return The removed node or null if it didn't exist in the first place.
1116     */
1117    public Node deleteXmlNode() {
1118        if (mXmlNode == null) {
1119            return null;
1120        }
1121
1122        int previousIndex = getUiSiblingIndex();
1123
1124        // First clear the internals of the node and *then* actually deletes the XML
1125        // node (because doing so will generate an update even and this node may be
1126        // revisited via loadFromXmlNode).
1127        Node oldXmlNode = mXmlNode;
1128        clearContent();
1129
1130        Node xmlParent = oldXmlNode.getParentNode();
1131        if (xmlParent == null) {
1132            xmlParent = getXmlDocument();
1133        }
1134        Node previousSibling = oldXmlNode.getPreviousSibling();
1135        oldXmlNode = xmlParent.removeChild(oldXmlNode);
1136
1137        // We need to remove the text node BEFORE the removed element, since THAT's the
1138        // indentation node for the removed element.
1139        if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
1140                && previousSibling.getNodeValue().trim().length() == 0) {
1141            xmlParent.removeChild(previousSibling);
1142        }
1143
1144        invokeUiUpdateListeners(UiUpdateState.DELETED);
1145        fireNodeDeleted(this, previousIndex);
1146
1147        return oldXmlNode;
1148    }
1149
1150    /**
1151     * Updates the element list for this UiElementNode.
1152     * At the end, the list of children UiElementNode here will match the one from the
1153     * provided XML {@link Node}:
1154     * <ul>
1155     * <li> Walk both the current ui children list and the xml children list at the same time.
1156     * <li> If we have a new xml child but already reached the end of the ui child list, add the
1157     *      new xml node.
1158     * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
1159     *      move it here. It means the XML child list has been reordered.
1160     * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
1161     * <li> At the end, we may have finished walking the xml child list but still have remaining
1162     *      ui children, simply delete them as they matching trailing xml nodes that have been
1163     *      removed unless they are mandatory ui nodes.
1164     * </ul>
1165     * Note that only the first case is used when populating the ui list the first time.
1166     *
1167     * @param xmlNode The XML node to mirror
1168     * @return True when the XML structure has changed.
1169     */
1170    protected boolean updateElementList(Node xmlNode) {
1171        boolean structureChanged = false;
1172        boolean hasMandatoryLast = false;
1173        int uiIndex = 0;
1174        Node xmlChild = xmlNode.getFirstChild();
1175        while (xmlChild != null) {
1176            if (xmlChild.getNodeType() == Node.ELEMENT_NODE) {
1177                String elementName = xmlChild.getNodeName();
1178                UiElementNode uiNode = null;
1179                CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
1180                if (mUiChildren.size() <= uiIndex) {
1181                    // A new node is being added at the end of the list
1182                    ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
1183                            false /* recursive */);
1184                    if (desc == null && elementName.indexOf('.') != -1 &&
1185                            (!elementName.startsWith(ANDROID_PKG_PREFIX)
1186                                    || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
1187                        AndroidXmlEditor editor = getEditor();
1188                        if (editor != null && editor.getProject() != null) {
1189                            desc = service.getDescriptor(editor.getProject(), elementName);
1190                        }
1191                    }
1192                    if (desc == null) {
1193                        // Unknown node. Create a temporary descriptor for it.
1194                        // We'll add unknown attributes to it later.
1195                        IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
1196                        desc = p.getDescriptor(elementName);
1197                    }
1198                    structureChanged = true;
1199                    uiNode = appendNewUiChild(desc);
1200                    uiIndex++;
1201                } else {
1202                    // A new node is being inserted or moved.
1203                    // Note: mandatory nodes can be created without an XML node in which case
1204                    // getXmlNode() is null.
1205                    UiElementNode uiChild;
1206                    int n = mUiChildren.size();
1207                    for (int j = uiIndex; j < n; j++) {
1208                        uiChild = mUiChildren.get(j);
1209                        if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) {
1210                            if (j > uiIndex) {
1211                                // Found the same XML node at some later index, now move it here.
1212                                mUiChildren.remove(j);
1213                                mUiChildren.add(uiIndex, uiChild);
1214                                structureChanged = true;
1215                            }
1216                            uiNode = uiChild;
1217                            uiIndex++;
1218                            break;
1219                        }
1220                    }
1221
1222                    if (uiNode == null) {
1223                        // Look for an unused mandatory node with no XML node attached
1224                        // referencing the same XML element name
1225                        for (int j = uiIndex; j < n; j++) {
1226                            uiChild = mUiChildren.get(j);
1227                            if (uiChild.getXmlNode() == null &&
1228                                    uiChild.getDescriptor().getMandatory() !=
1229                                                                Mandatory.NOT_MANDATORY &&
1230                                    uiChild.getDescriptor().getXmlName().equals(elementName)) {
1231
1232                                if (j > uiIndex) {
1233                                    // Found it, now move it here
1234                                    mUiChildren.remove(j);
1235                                    mUiChildren.add(uiIndex, uiChild);
1236                                }
1237                                // Assign the XML node to this empty mandatory element.
1238                                uiChild.mXmlNode = xmlChild;
1239                                structureChanged = true;
1240                                uiNode = uiChild;
1241                                uiIndex++;
1242                            }
1243                        }
1244                    }
1245
1246                    if (uiNode == null) {
1247                        // Inserting new node
1248                        ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
1249                                false /* recursive */);
1250                        if (desc == null && elementName.indexOf('.') != -1 &&
1251                                (!elementName.startsWith(ANDROID_PKG_PREFIX)
1252                                        || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
1253                            AndroidXmlEditor editor = getEditor();
1254                            if (editor != null && editor.getProject() != null) {
1255                                desc = service.getDescriptor(editor.getProject(), elementName);
1256                            }
1257                        }
1258                        if (desc == null) {
1259                            // Unknown node. Create a temporary descriptor for it.
1260                            // We'll add unknown attributes to it later.
1261                            IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
1262                            desc = p.getDescriptor(elementName);
1263                        } else {
1264                            structureChanged = true;
1265                            uiNode = insertNewUiChild(uiIndex, desc);
1266                            uiIndex++;
1267                        }
1268                    }
1269                }
1270                if (uiNode != null) {
1271                    // If we touched an UI Node, even an existing one, refresh its content.
1272                    // For new nodes, this will populate them recursively.
1273                    structureChanged |= uiNode.loadFromXmlNode(xmlChild);
1274
1275                    // Remember if there are any mandatory-last nodes to reorder.
1276                    hasMandatoryLast |=
1277                        uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST;
1278                }
1279            }
1280            xmlChild = xmlChild.getNextSibling();
1281        }
1282
1283        // There might be extra UI nodes at the end if the XML node list got shorter.
1284        for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) {
1285             structureChanged |= removeUiChildAtIndex(index);
1286        }
1287
1288        if (hasMandatoryLast) {
1289            // At least one mandatory-last uiNode was moved. Let's see if we can
1290            // move them back to the last position. That's possible if the only
1291            // thing between these and the end are other mandatory empty uiNodes
1292            // (mandatory uiNodes with no XML attached are pure "virtual" reserved
1293            // slots and it's ok to reorganize them but other can't.)
1294            int n = mUiChildren.size() - 1;
1295            for (int index = n; index >= 0; index--) {
1296                UiElementNode uiChild = mUiChildren.get(index);
1297                Mandatory mand = uiChild.getDescriptor().getMandatory();
1298                if (mand == Mandatory.MANDATORY_LAST && index < n) {
1299                    // Remove it from index and move it back at the end of the list.
1300                    mUiChildren.remove(index);
1301                    mUiChildren.add(uiChild);
1302                } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) {
1303                    // We found at least one non-mandatory or a mandatory node with an actual
1304                    // XML attached, so there's nothing we can reorganize past this point.
1305                    break;
1306                }
1307            }
1308        }
1309
1310        return structureChanged;
1311    }
1312
1313    /**
1314     * Internal helper to remove an UI child node given by its index in the
1315     * internal child list.
1316     *
1317     * Also invokes the update listener on the node to be deleted *after* the node has
1318     * been removed.
1319     *
1320     * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1
1321     * @return True if the structure has changed
1322     * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
1323     *         know that could never happen unless the computer is on fire or something.
1324     */
1325    private boolean removeUiChildAtIndex(int uiIndex) {
1326        UiElementNode uiNode = mUiChildren.get(uiIndex);
1327        ElementDescriptor desc = uiNode.getDescriptor();
1328
1329        try {
1330            if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
1331                // This is a mandatory node. Such a node must exist in the UiNode hierarchy
1332                // even if there's no XML counterpart. However we only need to keep one.
1333
1334                // Check if the parent (e.g. this node) has another similar ui child node.
1335                boolean keepNode = true;
1336                for (UiElementNode child : mUiChildren) {
1337                    if (child != uiNode && child.getDescriptor() == desc) {
1338                        // We found another child with the same descriptor that is not
1339                        // the node we want to remove. This means we have one mandatory
1340                        // node so we can safely remove uiNode.
1341                        keepNode = false;
1342                        break;
1343                    }
1344                }
1345
1346                if (keepNode) {
1347                    // We can't remove a mandatory node as we need to keep at least one
1348                    // mandatory node in the parent. Instead we just clear its content
1349                    // (including its XML Node reference).
1350
1351                    // A mandatory node with no XML means it doesn't really exist, so it can't be
1352                    // deleted. So the structure will change only if the ui node is actually
1353                    // associated to an XML node.
1354                    boolean xmlExists = (uiNode.getXmlNode() != null);
1355
1356                    uiNode.clearContent();
1357                    return xmlExists;
1358                }
1359            }
1360
1361            mUiChildren.remove(uiIndex);
1362
1363            return true;
1364        } finally {
1365            // Tell listeners that a node has been removed.
1366            // The model has already been modified.
1367            invokeUiUpdateListeners(UiUpdateState.DELETED);
1368        }
1369    }
1370
1371    /**
1372     * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
1373     * and appends it to the end of the element children list.
1374     *
1375     * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
1376     * @return The new UI node that has been appended
1377     */
1378    public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
1379        UiElementNode uiNode;
1380        uiNode = descriptor.createUiNode();
1381        mUiChildren.add(uiNode);
1382        uiNode.setUiParent(this);
1383        uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
1384        return uiNode;
1385    }
1386
1387    /**
1388     * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
1389     * and inserts it in the element children list at the specified position.
1390     *
1391     * @param index The position where to insert in the element children list.
1392     *              Shifts the element currently at that position (if any) and any
1393     *              subsequent elements to the right (adds one to their indices).
1394     *              Index must >= 0 and <= getUiChildren.size().
1395     *              Using size() means to append to the end of the list.
1396     * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
1397     * @return The new UI node.
1398     */
1399    public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
1400        UiElementNode uiNode;
1401        uiNode = descriptor.createUiNode();
1402        mUiChildren.add(index, uiNode);
1403        uiNode.setUiParent(this);
1404        uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
1405        return uiNode;
1406    }
1407
1408    /**
1409     * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}
1410     * using the values from the XML element.
1411     * <p/>
1412     * For a given {@link UiElementNode}, the attribute list always exists in
1413     * full and is totally independent of whether the XML model actually
1414     * has the corresponding attributes.
1415     * <p/>
1416     * For each attribute declared in this {@link UiElementNode}, get
1417     * the corresponding XML attribute. It may not exist, in which case the
1418     * value will be null. We don't really know if a value has changed, so
1419     * the updateValue() is called on the UI attribute in all cases.
1420     *
1421     * @param xmlNode The XML node to mirror
1422     */
1423    protected void updateAttributeList(Node xmlNode) {
1424        NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
1425        HashSet<Node> visited = new HashSet<Node>();
1426
1427        // For all known (i.e. expected) UI attributes, find an existing XML attribute of
1428        // same (uri, local name) and update the internal Ui attribute value.
1429        for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
1430            AttributeDescriptor desc = uiAttr.getDescriptor();
1431            if (!(desc instanceof SeparatorAttributeDescriptor)) {
1432                Node xmlAttr = xmlAttrMap == null ? null :
1433                    xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
1434                uiAttr.updateValue(xmlAttr);
1435                visited.add(xmlAttr);
1436            }
1437        }
1438
1439        // Clone the current list of unknown attributes. We'll then remove from this list when
1440        // we find attributes which are still unknown. What will be left are the old unknown
1441        // attributes that have been deleted in the current XML attribute list.
1442        @SuppressWarnings("unchecked")
1443        HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();
1444
1445        // We need to ignore hidden attributes.
1446        Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();
1447
1448        // Traverse the actual XML attribute list to find unknown attributes
1449        if (xmlAttrMap != null) {
1450            for (int i = 0; i < xmlAttrMap.getLength(); i++) {
1451                Node xmlAttr = xmlAttrMap.item(i);
1452                // Ignore attributes which have actual descriptors
1453                if (visited.contains(xmlAttr)) {
1454                    continue;
1455                }
1456
1457                String xmlFullName = xmlAttr.getNodeName();
1458
1459                // Ignore attributes which are hidden (based on the prefix:localName key)
1460                if (hiddenAttrDesc.containsKey(xmlFullName)) {
1461                    continue;
1462                }
1463
1464                String xmlAttrLocalName = xmlAttr.getLocalName();
1465                String xmlNsUri = xmlAttr.getNamespaceURI();
1466
1467                UiAttributeNode uiAttr = null;
1468                for (UiAttributeNode a : mUnknownUiAttributes) {
1469                    String aLocalName = a.getDescriptor().getXmlLocalName();
1470                    String aNsUri = a.getDescriptor().getNamespaceUri();
1471                    if (aLocalName.equals(xmlAttrLocalName) &&
1472                            (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
1473                        // This attribute is still present in the unknown list
1474                        uiAttr = a;
1475                        // It has not been deleted
1476                        deleted.remove(a);
1477                        break;
1478                    }
1479                }
1480                if (uiAttr == null) {
1481                    uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri);
1482                }
1483
1484                uiAttr.updateValue(xmlAttr);
1485            }
1486
1487            // Remove from the internal list unknown attributes that have been deleted from the xml
1488            for (UiAttributeNode a : deleted) {
1489                mUnknownUiAttributes.remove(a);
1490                mCachedAllUiAttributes = null;
1491            }
1492        }
1493    }
1494
1495    /**
1496     * Create a new temporary text attribute descriptor for the unknown attribute
1497     * and returns a new {@link UiAttributeNode} associated to this descriptor.
1498     * <p/>
1499     * The attribute is not marked as dirty, doing so is up to the caller.
1500     */
1501    private UiAttributeNode addUnknownAttribute(String xmlFullName,
1502            String xmlAttrLocalName, String xmlNsUri) {
1503        // Create a new unknown attribute of format string
1504        TextAttributeDescriptor desc = new TextAttributeDescriptor(
1505                xmlAttrLocalName,           // xml name
1506                xmlNsUri,                // ui name
1507                new AttributeInfo(xmlAttrLocalName, Format.STRING_SET)
1508                );
1509        UiAttributeNode uiAttr = desc.createUiNode(this);
1510        mUnknownUiAttributes.add(uiAttr);
1511        mCachedAllUiAttributes = null;
1512        return uiAttr;
1513    }
1514
1515    /**
1516     * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node.
1517     */
1518    protected void invokeUiUpdateListeners(UiUpdateState state) {
1519        if (mUiUpdateListeners != null) {
1520            for (IUiUpdateListener listener : mUiUpdateListeners) {
1521                try {
1522                    listener.uiElementNodeUpdated(this, state);
1523                } catch (Exception e) {
1524                    // prevent a crashing listener from crashing the whole invocation chain
1525                    AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s",  //$NON-NLS-1$
1526                            getBreadcrumbTrailDescription(true),
1527                            state.toString());
1528                }
1529            }
1530        }
1531    }
1532
1533    // --- for derived implementations only ---
1534
1535    @VisibleForTesting
1536    public void setXmlNode(Node xmlNode) {
1537        mXmlNode = xmlNode;
1538    }
1539
1540    public void refreshUi() {
1541        invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
1542    }
1543
1544
1545    // ------------- Helpers
1546
1547    /**
1548     * Helper method to commit a single attribute value to XML.
1549     * <p/>
1550     * This method updates the XML regardless of the current XML value.
1551     * Callers should check first if an update is needed.
1552     * If the new value is empty, the XML attribute will be actually removed.
1553     * <p/>
1554     * Note that the caller MUST ensure that modifying the underlying XML model is
1555     * safe and must take care of marking the model as dirty if necessary.
1556     *
1557     * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
1558     *
1559     * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
1560     * @param newValue The new value to set.
1561     * @return True if the XML attribute was modified or removed, false if nothing changed.
1562     */
1563    public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
1564        // Get (or create) the underlying XML element node that contains the attributes.
1565        Node element = prepareCommit();
1566        if (element != null && uiAttr != null) {
1567            String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
1568            String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();
1569
1570            NamedNodeMap attrMap = element.getAttributes();
1571            if (newValue == null || newValue.length() == 0) {
1572                // Remove attribute if it's empty
1573                if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
1574                    attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
1575                    return true;
1576                }
1577            } else {
1578                // Add or replace an attribute
1579                Document doc = element.getOwnerDocument();
1580                if (doc != null) {
1581                    Attr attr;
1582                    if (attrNsUri != null && attrNsUri.length() > 0) {
1583                        attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName);
1584                        if (attr == null) {
1585                            attr = doc.createAttributeNS(attrNsUri, attrLocalName);
1586                            attr.setPrefix(lookupNamespacePrefix(element, attrNsUri));
1587                            attrMap.setNamedItemNS(attr);
1588                        }
1589                    } else {
1590                        attr = (Attr) attrMap.getNamedItem(attrLocalName);
1591                        if (attr == null) {
1592                            attr = doc.createAttribute(attrLocalName);
1593                            attrMap.setNamedItem(attr);
1594                        }
1595                    }
1596                    attr.setValue(newValue);
1597                    return true;
1598                }
1599            }
1600        }
1601        return false;
1602    }
1603
1604    /**
1605     * Helper method to commit all dirty attributes values to XML.
1606     * <p/>
1607     * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has
1608     * been called more than once and all the attributes marked as dirty must be committed to
1609     * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
1610     * attribute.
1611     * <p/>
1612     * Note that the caller MUST ensure that modifying the underlying XML model is
1613     * safe and must take care of marking the model as dirty if necessary.
1614     *
1615     * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
1616     *
1617     * @return True if one or more values were actually modified or removed,
1618     *         false if nothing changed.
1619     */
1620    @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong
1621    public boolean commitDirtyAttributesToXml() {
1622        boolean result = false;
1623        List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>();
1624        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
1625            if (uiAttr.isDirty()) {
1626                String value = uiAttr.getCurrentValue();
1627                if (value != null && value.length() > 0) {
1628                    // Defer the new attributes: set these last and in order
1629                    dirtyAttributes.add(uiAttr);
1630                } else {
1631                    result |= commitAttributeToXml(uiAttr, value);
1632                    uiAttr.setDirty(false);
1633                }
1634            }
1635        }
1636        if (dirtyAttributes.size() > 0) {
1637            result = true;
1638
1639            Collections.sort(dirtyAttributes);
1640
1641            // The Eclipse XML model will *always* append new attributes.
1642            // Therefore, if any of the dirty attributes are new, they will appear
1643            // after any existing, clean attributes on the element. To fix this,
1644            // we need to first remove any of these attributes, then insert them
1645            // back in the right order.
1646            Node element = prepareCommit();
1647            if (element == null) {
1648                return result;
1649            }
1650
1651            if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) {
1652                // If auto formatting, don't bother with attribute sorting here since the
1653                // order will be corrected as soon as the edit is committed anyway
1654                for (UiAttributeNode uiAttribute : dirtyAttributes) {
1655                    commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
1656                    uiAttribute.setDirty(false);
1657                }
1658
1659                return result;
1660            }
1661
1662            AttributeDescriptor descriptor = dirtyAttributes.get(0).getDescriptor();
1663            String firstName = descriptor.getXmlLocalName();
1664            String firstNamePrefix = null;
1665            if (descriptor.getNamespaceUri() != null) {
1666                firstNamePrefix = lookupNamespacePrefix(element, descriptor.getNamespaceUri());
1667            }
1668            NamedNodeMap attributes = ((Element) element).getAttributes();
1669            List<Attr> move = new ArrayList<Attr>();
1670            for (int i = 0, n = attributes.getLength(); i < n; i++) {
1671                Attr attribute = (Attr) attributes.item(i);
1672                if (UiAttributeNode.compareAttributes(
1673                        attribute.getPrefix(), attribute.getLocalName(),
1674                        firstNamePrefix, firstName) > 0) {
1675                    move.add(attribute);
1676                }
1677            }
1678
1679            for (Attr attribute : move) {
1680                if (attribute.getNamespaceURI() != null) {
1681                    attributes.removeNamedItemNS(attribute.getNamespaceURI(),
1682                            attribute.getLocalName());
1683                } else {
1684                    attributes.removeNamedItem(attribute.getName());
1685                }
1686            }
1687
1688            // Merge back the removed DOM attribute nodes and the new UI attribute nodes.
1689            // In cases where the attribute DOM name and the UI attribute names equal,
1690            // skip the DOM nodes and just apply the UI attributes.
1691            int domAttributeIndex = 0;
1692            int domAttributeIndexMax = move.size();
1693            int uiAttributeIndex = 0;
1694            int uiAttributeIndexMax = dirtyAttributes.size();
1695
1696            while (true) {
1697                Attr domAttribute;
1698                UiAttributeNode uiAttribute;
1699
1700                int compare;
1701                if (uiAttributeIndex < uiAttributeIndexMax) {
1702                    if (domAttributeIndex < domAttributeIndexMax) {
1703                        domAttribute = move.get(domAttributeIndex);
1704                        uiAttribute = dirtyAttributes.get(uiAttributeIndex);
1705
1706                        String domAttributeName = domAttribute.getLocalName();
1707                        String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName();
1708                        compare = UiAttributeNode.compareAttributes(domAttributeName,
1709                                uiAttributeName);
1710                    } else {
1711                        compare = 1;
1712                        uiAttribute = dirtyAttributes.get(uiAttributeIndex);
1713                        domAttribute = null;
1714                    }
1715                } else if (domAttributeIndex < domAttributeIndexMax) {
1716                    compare = -1;
1717                    domAttribute = move.get(domAttributeIndex);
1718                    uiAttribute = null;
1719                } else {
1720                    break;
1721                }
1722
1723                if (compare < 0) {
1724                    if (domAttribute.getNamespaceURI() != null) {
1725                        attributes.setNamedItemNS(domAttribute);
1726                    } else {
1727                        attributes.setNamedItem(domAttribute);
1728                    }
1729                    domAttributeIndex++;
1730                } else {
1731                    assert compare >= 0;
1732                    if (compare == 0) {
1733                        domAttributeIndex++;
1734                    }
1735                    commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
1736                    uiAttribute.setDirty(false);
1737                    uiAttributeIndex++;
1738                }
1739            }
1740        }
1741
1742        return result;
1743    }
1744
1745    /**
1746     * Returns the namespace prefix matching the requested namespace URI.
1747     * If no such declaration is found, returns the default "android" prefix for
1748     * the Android URI, and "app" for other URI's.
1749     *
1750     * @param node The current node. Must not be null.
1751     * @param nsUri The namespace URI of which the prefix is to be found,
1752     *              e.g. SdkConstants.NS_RESOURCES
1753     * @return The first prefix declared or the default "android" prefix
1754     *              (or "app" for non-Android URIs)
1755     */
1756    public static String lookupNamespacePrefix(Node node, String nsUri) {
1757        String defaultPrefix = NS_RESOURCES.equals(nsUri) ? ANDROID_NS_NAME : "app"; //$NON-NLS-1$
1758        return lookupNamespacePrefix(node, nsUri, defaultPrefix);
1759    }
1760
1761    /**
1762     * Returns the namespace prefix matching the requested namespace URI.
1763     * If no such declaration is found, returns the default "android" prefix.
1764     *
1765     * @param node The current node. Must not be null.
1766     * @param nsUri The namespace URI of which the prefix is to be found,
1767     *              e.g. SdkConstants.NS_RESOURCES
1768     * @param defaultPrefix The default prefix (root) to use if the namespace
1769     *              is not found. If null, do not create a new namespace
1770     *              if this URI is not defined for the document.
1771     * @return The first prefix declared or the provided prefix (possibly with
1772     *              a number appended to avoid conflicts with existing prefixes.
1773     */
1774    public static String lookupNamespacePrefix(
1775            @Nullable Node node, @Nullable String nsUri, @Nullable String defaultPrefix) {
1776        // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
1777        // The following code emulates this simple call:
1778        //   String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES);
1779
1780        // if the requested URI is null, it denotes an attribute with no namespace.
1781        if (nsUri == null) {
1782            return null;
1783        }
1784
1785        // per XML specification, the "xmlns" URI is reserved
1786        if (XMLNS_URI.equals(nsUri)) {
1787            return XMLNS;
1788        }
1789
1790        HashSet<String> visited = new HashSet<String>();
1791        Document doc = node == null ? null : node.getOwnerDocument();
1792
1793        // Ask the document about it. This method may not be implemented by the Document.
1794        String nsPrefix = null;
1795        try {
1796            nsPrefix = doc != null ? doc.lookupPrefix(nsUri) : null;
1797            if (nsPrefix != null) {
1798                return nsPrefix;
1799            }
1800        } catch (Throwable t) {
1801            // ignore
1802        }
1803
1804        // If that failed, try to look it up manually.
1805        // This also gathers prefixed in use in the case we want to generate a new one below.
1806        for (; node != null && node.getNodeType() == Node.ELEMENT_NODE;
1807               node = node.getParentNode()) {
1808            NamedNodeMap attrs = node.getAttributes();
1809            for (int n = attrs.getLength() - 1; n >= 0; --n) {
1810                Node attr = attrs.item(n);
1811                if (XMLNS.equals(attr.getPrefix())) {
1812                    String uri = attr.getNodeValue();
1813                    nsPrefix = attr.getLocalName();
1814                    // Is this the URI we are looking for? If yes, we found its prefix.
1815                    if (nsUri.equals(uri)) {
1816                        return nsPrefix;
1817                    }
1818                    visited.add(nsPrefix);
1819                }
1820            }
1821        }
1822
1823        // Failed the find a prefix. Generate a new sensible default prefix, unless
1824        // defaultPrefix was null in which case the caller does not want the document
1825        // modified.
1826        if (defaultPrefix == null) {
1827            return null;
1828        }
1829
1830        //
1831        // We need to make sure the prefix is not one that was declared in the scope
1832        // visited above. Pick a unique prefix from the provided default prefix.
1833        String prefix = defaultPrefix;
1834        String base = prefix;
1835        for (int i = 1; visited.contains(prefix); i++) {
1836            prefix = base + Integer.toString(i);
1837        }
1838        // Also create & define this prefix/URI in the XML document as an attribute in the
1839        // first element of the document.
1840        if (doc != null) {
1841            node = doc.getFirstChild();
1842            while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
1843                node = node.getNextSibling();
1844            }
1845            if (node != null) {
1846                // This doesn't work:
1847                //Attr attr = doc.createAttributeNS(XMLNS_URI, prefix);
1848                //attr.setPrefix(XMLNS);
1849                //
1850                // Xerces throws
1851                //org.w3c.dom.DOMException: NAMESPACE_ERR: An attempt is made to create or
1852                // change an object in a way which is incorrect with regard to namespaces.
1853                //
1854                // Instead pass in the concatenated prefix. (This is covered by
1855                // the UiElementNodeTest#testCreateNameSpace() test.)
1856                Attr attr = doc.createAttributeNS(XMLNS_URI, XMLNS_PREFIX + prefix);
1857                attr.setValue(nsUri);
1858                node.getAttributes().setNamedItemNS(attr);
1859            }
1860        }
1861
1862        return prefix;
1863    }
1864
1865    /**
1866     * Utility method to internally set the value of a text attribute for the current
1867     * UiElementNode.
1868     * <p/>
1869     * This method is a helper. It silently ignores the errors such as the requested
1870     * attribute not being present in the element or attribute not being settable.
1871     * It accepts inherited attributes (such as layout).
1872     * <p/>
1873     * This does not commit to the XML model. It does mark the attribute node as dirty.
1874     * This is up to the caller.
1875     *
1876     * @see #commitAttributeToXml(UiAttributeNode, String)
1877     * @see #commitDirtyAttributesToXml()
1878     *
1879     * @param attrXmlName The XML <em>local</em> name of the attribute to modify
1880     * @param attrNsUri The namespace URI of the attribute.
1881     *                  Can be null if the attribute uses the global namespace.
1882     * @param value The new value for the attribute. If set to null, the attribute is removed.
1883     * @param override True if the value must be set even if one already exists.
1884     * @return The {@link UiAttributeNode} that has been modified or null.
1885     */
1886    public UiAttributeNode setAttributeValue(
1887            String attrXmlName,
1888            String attrNsUri,
1889            String value,
1890            boolean override) {
1891        if (value == null) {
1892            value = ""; //$NON-NLS-1$ -- this removes an attribute
1893        }
1894
1895        getEditor().scheduleNodeReformat(this, true);
1896
1897        // Try with all internal attributes
1898        UiAttributeNode uiAttr = setInternalAttrValue(
1899                getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
1900        if (uiAttr != null) {
1901            return uiAttr;
1902        }
1903
1904        if (uiAttr == null) {
1905            // Failed to find the attribute. For non-android attributes that is mostly expected,
1906            // in which case we just create a new custom one. As a side effect, we'll find the
1907            // attribute descriptor via getAllUiAttributes().
1908            addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri);
1909
1910            // We've created the attribute, but not actually set the value on it, so let's do it.
1911            // Try with the updated internal attributes.
1912            // Implementation detail: we could just do a setCurrentValue + setDirty on the
1913            // uiAttr returned by addUnknownAttribute(); however going through setInternalAttrValue
1914            // means we won't duplicate the logic, at the expense of doing one more lookup.
1915            uiAttr = setInternalAttrValue(
1916                    getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
1917        }
1918
1919        return uiAttr;
1920    }
1921
1922    private UiAttributeNode setInternalAttrValue(
1923            Collection<UiAttributeNode> attributes,
1924            String attrXmlName,
1925            String attrNsUri,
1926            String value,
1927            boolean override) {
1928
1929        // For namespace less attributes (like the "layout" attribute of an <include> tag
1930        // we may be passed "" as the namespace (during an attribute copy), and it
1931        // should really be null instead.
1932        if (attrNsUri != null && attrNsUri.length() == 0) {
1933            attrNsUri = null;
1934        }
1935
1936        for (UiAttributeNode uiAttr : attributes) {
1937            AttributeDescriptor uiDesc = uiAttr.getDescriptor();
1938
1939            if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
1940                // Both NS URI must be either null or equal.
1941                if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) ||
1942                        (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) {
1943
1944                    // Not all attributes are editable, ignore those which are not.
1945                    if (uiAttr instanceof IUiSettableAttributeNode) {
1946                        String current = uiAttr.getCurrentValue();
1947                        // Only update (and mark as dirty) if the attribute did not have any
1948                        // value or if the value was different.
1949                        if (override || current == null || !current.equals(value)) {
1950                            ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value);
1951                            // mark the attribute as dirty since their internal content
1952                            // as been modified, but not the underlying XML model
1953                            uiAttr.setDirty(true);
1954                            return uiAttr;
1955                        }
1956                    }
1957
1958                    // We found the attribute but it's not settable. Since attributes are
1959                    // not duplicated, just abandon here.
1960                    break;
1961                }
1962            }
1963        }
1964
1965        return null;
1966    }
1967
1968    /**
1969     * Utility method to retrieve the internal value of an attribute.
1970     * <p/>
1971     * Note that this retrieves the *field* value if the attribute has some UI, and
1972     * not the actual XML value. They may differ if the attribute is dirty.
1973     *
1974     * @param attrXmlName The XML name of the attribute to modify
1975     * @return The current internal value for the attribute or null in case of error.
1976     */
1977    public String getAttributeValue(String attrXmlName) {
1978        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1979
1980        for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
1981            AttributeDescriptor uiDesc = entry.getKey();
1982            if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
1983                UiAttributeNode uiAttr = entry.getValue();
1984                return uiAttr.getCurrentValue();
1985            }
1986        }
1987        return null;
1988    }
1989
1990    // ------ IPropertySource methods
1991
1992    @Override
1993    public Object getEditableValue() {
1994        return null;
1995    }
1996
1997    /*
1998     * (non-Javadoc)
1999     * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
2000     *
2001     * Returns the property descriptor for this node. Since the descriptors are not linked to the
2002     * data, the AttributeDescriptor are used directly.
2003     */
2004    @Override
2005    public IPropertyDescriptor[] getPropertyDescriptors() {
2006        List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();
2007
2008        // get the standard descriptors
2009        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
2010        Set<AttributeDescriptor> keys = attributeMap.keySet();
2011
2012
2013        // we only want the descriptor that do implement the IPropertyDescriptor interface.
2014        for (AttributeDescriptor key : keys) {
2015            if (key instanceof IPropertyDescriptor) {
2016                propDescs.add((IPropertyDescriptor)key);
2017            }
2018        }
2019
2020        // now get the descriptor from the unknown attributes
2021        for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
2022            if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
2023                propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
2024            }
2025        }
2026
2027        // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
2028        return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
2029    }
2030
2031    /*
2032     * (non-Javadoc)
2033     * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
2034     *
2035     * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
2036     * which return the AttributeDescriptor itself.
2037     */
2038    @Override
2039    public Object getPropertyValue(Object id) {
2040        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
2041
2042        UiAttributeNode attribute = attributeMap.get(id);
2043
2044        if (attribute == null) {
2045            // look for the id in the unknown attributes.
2046            for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
2047                if (id == unknownAttr.getDescriptor()) {
2048                    return unknownAttr;
2049                }
2050            }
2051        }
2052
2053        return attribute;
2054    }
2055
2056    /*
2057     * (non-Javadoc)
2058     * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
2059     *
2060     * Returns whether the property is set. In our case this is if the string is non empty.
2061     */
2062    @Override
2063    public boolean isPropertySet(Object id) {
2064        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
2065
2066        UiAttributeNode attribute = attributeMap.get(id);
2067
2068        if (attribute != null) {
2069            return attribute.getCurrentValue().length() > 0;
2070        }
2071
2072        // look for the id in the unknown attributes.
2073        for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
2074            if (id == unknownAttr.getDescriptor()) {
2075                return unknownAttr.getCurrentValue().length() > 0;
2076            }
2077        }
2078
2079        return false;
2080    }
2081
2082    /*
2083     * (non-Javadoc)
2084     * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
2085     *
2086     * Reset the property to its default value. For now we simply empty it.
2087     */
2088    @Override
2089    public void resetPropertyValue(Object id) {
2090        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
2091
2092        UiAttributeNode attribute = attributeMap.get(id);
2093        if (attribute != null) {
2094            // TODO: reset the value of the attribute
2095
2096            return;
2097        }
2098
2099        // look for the id in the unknown attributes.
2100        for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
2101            if (id == unknownAttr.getDescriptor()) {
2102                // TODO: reset the value of the attribute
2103
2104                return;
2105            }
2106        }
2107    }
2108
2109    /*
2110     * (non-Javadoc)
2111     * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
2112     *
2113     * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
2114     * AttributeDescriptor itself. Value should be a String.
2115     */
2116    @Override
2117    public void setPropertyValue(Object id, Object value) {
2118        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
2119
2120        UiAttributeNode attribute = attributeMap.get(id);
2121
2122        if (attribute == null) {
2123            // look for the id in the unknown attributes.
2124            for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
2125                if (id == unknownAttr.getDescriptor()) {
2126                    attribute = unknownAttr;
2127                    break;
2128                }
2129            }
2130        }
2131
2132        if (attribute != null) {
2133
2134            // get the current value and compare it to the new value
2135            String oldValue = attribute.getCurrentValue();
2136            final String newValue = (String)value;
2137
2138            if (oldValue.equals(newValue)) {
2139                return;
2140            }
2141
2142            final UiAttributeNode fAttribute = attribute;
2143            AndroidXmlEditor editor = getEditor();
2144            editor.wrapEditXmlModel(new Runnable() {
2145                @Override
2146                public void run() {
2147                    commitAttributeToXml(fAttribute, newValue);
2148                }
2149            });
2150        }
2151    }
2152
2153    /**
2154     * Returns true if this node is an ancestor (parent, grandparent, and so on)
2155     * of the given node. Note that a node is not considered an ancestor of
2156     * itself.
2157     *
2158     * @param node the node to test
2159     * @return true if this node is an ancestor of the given node
2160     */
2161    public boolean isAncestorOf(UiElementNode node) {
2162        node = node.getUiParent();
2163        while (node != null) {
2164            if (node == this) {
2165                return true;
2166            }
2167            node = node.getUiParent();
2168        }
2169        return false;
2170    }
2171
2172    /**
2173     * Finds the nearest common parent of the two given nodes (which could be one of the
2174     * two nodes as well)
2175     *
2176     * @param node1 the first node to test
2177     * @param node2 the second node to test
2178     * @return the nearest common parent of the two given nodes
2179     */
2180    public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) {
2181        while (node2 != null) {
2182            UiElementNode current = node1;
2183            while (current != null && current != node2) {
2184                current = current.getUiParent();
2185            }
2186            if (current == node2) {
2187                return current;
2188            }
2189            node2 = node2.getUiParent();
2190        }
2191
2192        return null;
2193    }
2194
2195    // ---- Global node create/delete Listeners ----
2196
2197    /** List of listeners to be notified of newly created nodes, or null */
2198    private static List<NodeCreationListener> sListeners;
2199
2200    /** Notify listeners that a new node has been created */
2201    private void fireNodeCreated(UiElementNode newChild, int index) {
2202        // Nothing to do if there aren't any listeners. We don't need to worry about
2203        // the case where one thread is firing node changes while another is adding a listener
2204        // (in that case it's still okay for this node firing not to be heard) so perform
2205        // the check outside of synchronization.
2206        if (sListeners == null) {
2207            return;
2208        }
2209        synchronized (UiElementNode.class) {
2210            if (sListeners != null) {
2211                UiElementNode parent = newChild.getUiParent();
2212                for (NodeCreationListener listener : sListeners) {
2213                    listener.nodeCreated(parent, newChild, index);
2214                }
2215            }
2216        }
2217    }
2218
2219    /** Notify listeners that a new node has been deleted */
2220    private void fireNodeDeleted(UiElementNode oldChild, int index) {
2221        if (sListeners == null) {
2222            return;
2223        }
2224        synchronized (UiElementNode.class) {
2225            if (sListeners != null) {
2226                UiElementNode parent = oldChild.getUiParent();
2227                for (NodeCreationListener listener : sListeners) {
2228                    listener.nodeDeleted(parent, oldChild, index);
2229                }
2230            }
2231        }
2232    }
2233
2234    /**
2235     * Adds a {@link NodeCreationListener} to be notified when new nodes are created
2236     *
2237     * @param listener the listener to be notified
2238     */
2239    public static void addNodeCreationListener(NodeCreationListener listener) {
2240        synchronized (UiElementNode.class) {
2241            if (sListeners == null) {
2242                sListeners = new ArrayList<NodeCreationListener>(1);
2243            }
2244            sListeners.add(listener);
2245        }
2246    }
2247
2248    /**
2249     * Removes a {@link NodeCreationListener} from the set of listeners such that it is
2250     * no longer notified when nodes are created.
2251     *
2252     * @param listener the listener to be removed from the notification list
2253     */
2254    public static void removeNodeCreationListener(NodeCreationListener listener) {
2255        synchronized (UiElementNode.class) {
2256            sListeners.remove(listener);
2257            if (sListeners.size() == 0) {
2258                sListeners = null;
2259            }
2260        }
2261    }
2262
2263    /** Interface implemented by listeners to be notified of newly created nodes */
2264    public interface NodeCreationListener {
2265        /**
2266         * Called when a new child node is created and added to the given parent
2267         *
2268         * @param parent the parent of the created node
2269         * @param child the newly node
2270         * @param index the index among the siblings of the child <b>after</b>
2271         *            insertion
2272         */
2273        void nodeCreated(UiElementNode parent, UiElementNode child, int index);
2274
2275        /**
2276         * Called when a child node is removed from the given parent
2277         *
2278         * @param parent the parent of the removed node
2279         * @param child the removed node
2280         * @param previousIndex the index among the siblings of the child
2281         *            <b>before</b> removal
2282         */
2283        void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex);
2284    }
2285}
2286