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