• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.ide.eclipse.adt.internal.editors.uimodel;
18 
19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME;
20 import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS;
21 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
22 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
23 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
24 import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_URI;
25 import static com.android.sdklib.SdkConstants.NS_RESOURCES;
26 import static com.android.tools.lint.detector.api.LintConstants.XMLNS_PREFIX;
27 
28 import com.android.annotations.Nullable;
29 import com.android.annotations.VisibleForTesting;
30 import com.android.ide.common.api.IAttributeInfo.Format;
31 import com.android.ide.common.resources.platform.AttributeInfo;
32 import com.android.ide.eclipse.adt.AdtPlugin;
33 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
34 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
35 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
36 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
37 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
38 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
39 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
40 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
41 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
42 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
43 import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
44 import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors;
45 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
46 import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors;
47 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
48 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
49 import com.android.sdklib.SdkConstants;
50 
51 import org.eclipse.core.resources.IProject;
52 import org.eclipse.jface.viewers.StyledString;
53 import org.eclipse.ui.views.properties.IPropertyDescriptor;
54 import org.eclipse.ui.views.properties.IPropertySource;
55 import org.eclipse.wst.xml.core.internal.document.ElementImpl;
56 import org.w3c.dom.Attr;
57 import org.w3c.dom.Document;
58 import org.w3c.dom.Element;
59 import org.w3c.dom.NamedNodeMap;
60 import org.w3c.dom.Node;
61 import org.w3c.dom.Text;
62 
63 import java.util.ArrayList;
64 import java.util.Collection;
65 import java.util.Collections;
66 import java.util.HashMap;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Locale;
70 import java.util.Map;
71 import java.util.Map.Entry;
72 import 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
93 public 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 List<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      */
UiElementNode(ElementDescriptor elementDescriptor)145     public UiElementNode(ElementDescriptor elementDescriptor) {
146         mDescriptor = elementDescriptor;
147         clearContent();
148     }
149 
150     @Override
toString()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      */
clearContent()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      */
clearAttributes()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      */
getInternalUiAttributes()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      */
getShortDescription()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 */
getDescAttribute()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      */
getStyledDescription()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      */
_Element_getAttributeNS(Element element, String namespaceURI, String localName)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      */
getBreadcrumbTrailDescription(boolean includeRoot)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      */
setXmlDocument(Document xmlDoc)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      */
getXmlDocument()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      */
getXmlNode()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      */
getDescriptor()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      */
getAttributeDescriptors()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      */
getHiddenAttributeDescriptors()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      */
setUiParent(UiElementNode parent)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      */
getUiParent()484     public UiElementNode getUiParent() {
485         return mUiParent;
486     }
487 
488     /**
489      * Returns the root {@link UiElementNode}.
490      *
491      * @return The root {@link UiElementNode}.
492      */
getUiRoot()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      */
getUiSiblingIndex()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      */
getUiPreviousSibling()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      */
getUiNextSibling()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      */
setEditor(AndroidXmlEditor editor)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      */
getEditor()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      */
getAndroidTarget()592     public AndroidTargetData getAndroidTarget() {
593         return getEditor().getTargetData();
594     }
595 
596     /**
597      * @return A read-only version of the children collection.
598      */
getUiChildren()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      */
getAllUiAttributes()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      */
getUnknownUiAttributes()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      */
setHasError(boolean errorFlag)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      */
hasError()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      */
getUnknownDescriptorProvider()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      */
setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider)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      */
addUpdateListener(IUiUpdateListener listener)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      */
removeUpdateListener(IUiUpdateListener listener)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      */
findUiChildNode(String path)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      */
findXmlNode(Node xmlNode)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      */
findUiAttribute(AttributeDescriptor attrDesc)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      */
loadFromXmlNode(Node xmlNode)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      */
reloadFromXmlNode(Node xmlNode)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      */
prepareCommit()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      */
commit()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      */
isDirty()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      */
createXmlNode()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      */
deleteXmlNode()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      */
updateElementList(Node xmlNode)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      */
removeUiChildAtIndex(int uiIndex)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      */
appendNewUiChild(ElementDescriptor descriptor)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      */
insertNewUiChild(int index, ElementDescriptor descriptor)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      */
updateAttributeList(Node xmlNode)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      */
addUnknownAttribute(String xmlFullName, String xmlAttrLocalName, String xmlNsUri)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      */
invokeUiUpdateListeners(UiUpdateState state)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
setXmlNode(Node xmlNode)1515     public void setXmlNode(Node xmlNode) {
1516         mXmlNode = xmlNode;
1517     }
1518 
refreshUi()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      */
commitAttributeToXml(UiAttributeNode uiAttr, String newValue)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
commitDirtyAttributesToXml()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      */
lookupNamespacePrefix(Node node, String nsUri)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      */
lookupNamespacePrefix( @ullable Node node, @Nullable String nsUri, @Nullable String defaultPrefix)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      */
setAttributeValue( String attrXmlName, String attrNsUri, String value, boolean override)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 
setInternalAttrValue( Collection<UiAttributeNode> attributes, String attrXmlName, String attrNsUri, String value, boolean override)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      */
getAttributeValue(String attrXmlName)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
getEditableValue()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
getPropertyDescriptors()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
getPropertyValue(Object id)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
isPropertySet(Object id)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
resetPropertyValue(Object id)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
setPropertyValue(Object id, Object value)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      */
isAncestorOf(UiElementNode node)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      */
getCommonAncestor(UiElementNode node1, UiElementNode node2)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 */
fireNodeCreated(UiElementNode newChild, int index)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 */
fireNodeDeleted(UiElementNode oldChild, int index)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      */
addNodeCreationListener(NodeCreationListener listener)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      */
removeNodeCreationListener(NodeCreationListener listener)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          */
nodeCreated(UiElementNode parent, UiElementNode child, int index)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          */
nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex)2262         void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex);
2263     }
2264 }
2265