• 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 com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.editors.layout.gscripts.IAttributeInfo.Format;
21 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
22 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
24 import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
25 import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
26 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
27 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
28 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
29 import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
30 import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
31 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
32 import com.android.ide.eclipse.adt.internal.editors.xml.descriptors.XmlDescriptors;
33 import com.android.ide.eclipse.adt.internal.resources.AttributeInfo;
34 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
35 import com.android.sdklib.SdkConstants;
36 
37 import org.eclipse.core.runtime.IStatus;
38 import org.eclipse.ui.views.properties.IPropertyDescriptor;
39 import org.eclipse.ui.views.properties.IPropertySource;
40 import org.w3c.dom.Attr;
41 import org.w3c.dom.Document;
42 import org.w3c.dom.Element;
43 import org.w3c.dom.NamedNodeMap;
44 import org.w3c.dom.Node;
45 import org.w3c.dom.Text;
46 
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Set;
55 import java.util.Map.Entry;
56 
57 /**
58  * Represents an XML node that can be modified by the user interface in the XML editor.
59  * <p/>
60  * Each tree viewer used in the application page's parts needs to keep a model representing
61  * each underlying node in the tree. This interface represents the base type for such a node.
62  * <p/>
63  * Each node acts as an intermediary model between the actual XML model (the real data support)
64  * and the tree viewers or the corresponding page parts.
65  * <p/>
66  * Element nodes don't contain data per se. Their data is contained in their attributes
67  * as well as their children's attributes, see {@link UiAttributeNode}.
68  * <p/>
69  * The structure of a given {@link UiElementNode} is declared by a corresponding
70  * {@link ElementDescriptor}.
71  * <p/>
72  * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
73  * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
74  */
75 public class UiElementNode implements IPropertySource {
76 
77     /** List of prefixes removed from android:id strings when creating short descriptions. */
78     private static String[] ID_PREFIXES = {
79         "@android:id/", //$NON-NLS-1$
80         "@+id/", "@id/", "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
81 
82     /** The element descriptor for the node. Always present, never null. */
83     private ElementDescriptor mDescriptor;
84     /** The parent element node in the UI model. It is null for a root element or until
85      *  the node is attached to its parent. */
86     private UiElementNode mUiParent;
87     /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the
88      *  root node. All children have the value set to null and query their parent. */
89     private AndroidXmlEditor mEditor;
90     /** The XML {@link Document} model that is being mirror by the UI model. This is defined
91      *  only for the root node. All children have the value set to null and query their parent. */
92     private Document mXmlDocument;
93     /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
94      *  have no corresponding XML node or for new UI nodes before their XML node is set. */
95     private Node mXmlNode;
96     /** The list of all UI children nodes. Can be empty but never null. There's one UI children
97      *  node per existing XML children node. */
98     private ArrayList<UiElementNode> mUiChildren;
99     /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
100      *  The list is always defined and never null. Unlike the UiElementNode children list, this
101      *  is always defined, even for attributes that do not exist in the XML model - that's because
102      *  "missing" attributes in the XML model simply mean a default value is used. Also note that
103      *  the underlying collection is a map, so order is not respected. To get the desired attribute
104      *  order, iterate through the {@link ElementDescriptor}'s attribute list. */
105     private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
106     private HashSet<UiAttributeNode> mUnknownUiAttributes;
107     /** A read-only view of the UI children node collection. */
108     private List<UiElementNode> mReadOnlyUiChildren;
109     /** A read-only view of the UI attributes collection. */
110     private Collection<UiAttributeNode> mReadOnlyUiAttributes;
111     /** A map of hidden attribute descriptors. Key is the XML name. */
112     private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
113     /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
114      *  listeners attached, so the list is only created on demand and can be null. */
115     private ArrayList<IUiUpdateListener> mUiUpdateListeners;
116     /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names.
117      *  The default is to have one that creates new {@link ElementDescriptor}. */
118     private IUnknownDescriptorProvider mUnknownDescProvider;
119     /** Error Flag */
120     private boolean mHasError;
121     /** Temporary data used by the editors. This data is not sync'ed with the XML */
122     private Object mEditData;
123 
124     /**
125      * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
126      *
127      * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
128      */
UiElementNode(ElementDescriptor elementDescriptor)129     public UiElementNode(ElementDescriptor elementDescriptor) {
130         mDescriptor = elementDescriptor;
131         clearContent();
132     }
133 
134     /**
135      * Clears the {@link UiElementNode} by resetting the children list and
136      * the {@link UiAttributeNode}s list.
137      * Also resets the attached XML node, document, editor if any.
138      * <p/>
139      * The parent {@link UiElementNode} node is not reset so that it's position
140      * in the hierarchy be left intact, if any.
141      */
clearContent()142     /* package */ void clearContent() {
143         mXmlNode = null;
144         mXmlDocument = null;
145         mEditor = null;
146         clearAttributes();
147         mReadOnlyUiChildren = null;
148         if (mUiChildren == null) {
149             mUiChildren = new ArrayList<UiElementNode>();
150         } else {
151             // We can't remove mandatory nodes, we just clear them.
152             for (int i = mUiChildren.size() - 1; i >= 0; --i) {
153                 removeUiChildAtIndex(i);
154             }
155         }
156     }
157 
158     /**
159      * Clears the internal list of attributes, the read-only cached version of it
160      * and the read-only cached hidden attribute list.
161      */
clearAttributes()162     private void clearAttributes() {
163         mUiAttributes = null;
164         mReadOnlyUiAttributes = null;
165         mCachedHiddenAttributes = null;
166         mUnknownUiAttributes = new HashSet<UiAttributeNode>();
167     }
168 
169     /**
170      * Gets or creates the internal UiAttributes list.
171      * <p/>
172      * When the descriptor derives from ViewElementDescriptor, this list depends on the
173      * current UiParent node.
174      *
175      * @return A new set of {@link UiAttributeNode} that matches the expected
176      *         attributes for this node.
177      */
getInternalUiAttributes()178     private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
179         if (mUiAttributes == null) {
180             AttributeDescriptor[] attr_list = getAttributeDescriptors();
181             mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attr_list.length);
182             for (AttributeDescriptor desc : attr_list) {
183                 UiAttributeNode ui_node = desc.createUiNode(this);
184                 if (ui_node != null) {  // Some AttributeDescriptors do not have UI associated
185                     mUiAttributes.put(desc, ui_node);
186                 }
187             }
188         }
189         return mUiAttributes;
190     }
191 
192     /**
193      * Computes a short string describing the UI node suitable for tree views.
194      * Uses the element's attribute "android:name" if present, or the "android:label" one
195      * followed by the element's name.
196      *
197      * @return A short string describing the UI node suitable for tree views.
198      */
getShortDescription()199     public String getShortDescription() {
200         if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
201 
202             // Application and Manifest nodes have a special treatment: they are unique nodes
203             // so we don't bother trying to differentiate their strings and we fall back to
204             // just using the UI name below.
205             Element elem = (Element) mXmlNode;
206 
207             String attr = _Element_getAttributeNS(elem,
208                                 SdkConstants.NS_RESOURCES,
209                                 AndroidManifestDescriptors.ANDROID_NAME_ATTR);
210             if (attr == null || attr.length() == 0) {
211                 attr = _Element_getAttributeNS(elem,
212                                 SdkConstants.NS_RESOURCES,
213                                 AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
214             }
215             if (attr == null || attr.length() == 0) {
216                 attr = _Element_getAttributeNS(elem,
217                                 SdkConstants.NS_RESOURCES,
218                                 XmlDescriptors.PREF_KEY_ATTR);
219             }
220             if (attr == null || attr.length() == 0) {
221                 attr = _Element_getAttributeNS(elem,
222                                 null, // no namespace
223                                 ResourcesDescriptors.NAME_ATTR);
224             }
225             if (attr == null || attr.length() == 0) {
226                 attr = _Element_getAttributeNS(elem,
227                                 SdkConstants.NS_RESOURCES,
228                                 LayoutDescriptors.ID_ATTR);
229 
230                 if (attr != null && attr.length() > 0) {
231                     for (String prefix : ID_PREFIXES) {
232                         if (attr.startsWith(prefix)) {
233                             attr = attr.substring(prefix.length());
234                             break;
235                         }
236                     }
237                 }
238             }
239             if (attr != null && attr.length() > 0) {
240                 return String.format("%1$s (%2$s)", attr, mDescriptor.getUiName());
241             }
242         }
243 
244         return String.format("%1$s", mDescriptor.getUiName());
245     }
246 
247     /**
248      * Retrieves an attribute value by local name and namespace URI.
249      * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
250      * , applications must use the value <code>null</code> as the
251      * <code>namespaceURI</code> parameter for methods if they wish to have
252      * no namespace.
253      * <p/>
254      * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
255      * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
256      * This wrapper will return null instead.
257      *
258      * @see Element#getAttributeNS(String, String)
259      * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
260      * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string.
261      */
_Element_getAttributeNS(Element element, String namespaceURI, String localName)262     private String _Element_getAttributeNS(Element element,
263             String namespaceURI,
264             String localName) {
265         try {
266             return element.getAttributeNS(namespaceURI, localName);
267         } catch (Exception ignore) {
268             return "";
269         }
270     }
271 
272     /**
273      * Computes a "breadcrumb trail" description for this node.
274      * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
275      *
276      * @param include_root Whether to include the root (e.g. "Manifest") or not. Has no effect
277      *                     when called on the root node itself.
278      * @return The "breadcrumb trail" description for this node.
279      */
getBreadcrumbTrailDescription(boolean include_root)280     public String getBreadcrumbTrailDescription(boolean include_root) {
281         StringBuilder sb = new StringBuilder(getShortDescription());
282 
283         for (UiElementNode ui_node = getUiParent();
284                 ui_node != null;
285                 ui_node = ui_node.getUiParent()) {
286             if (!include_root && ui_node.getUiParent() == null) {
287                 break;
288             }
289             sb.insert(0, String.format("%1$s > ", ui_node.getShortDescription())); //$NON-NLS-1$
290         }
291 
292         return sb.toString();
293     }
294 
295     /**
296      * Sets the XML {@link Document}.
297      * <p/>
298      * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
299      * UI root element node (this method takes care of that.)
300      */
setXmlDocument(Document xml_doc)301     public void setXmlDocument(Document xml_doc) {
302         if (mUiParent == null) {
303             mXmlDocument = xml_doc;
304         } else {
305             mUiParent.setXmlDocument(xml_doc);
306         }
307     }
308 
309     /**
310      * Returns the XML {@link Document}.
311      * <p/>
312      * The value is initially null until the UI node is attached to its UI parent -- the value
313      * of the document is then propagated.
314      *
315      * @return the XML {@link Document} or the parent's XML {@link Document} or null.
316      */
getXmlDocument()317     public Document getXmlDocument() {
318         if (mXmlDocument != null) {
319             return mXmlDocument;
320         } else if (mUiParent != null) {
321             return mUiParent.getXmlDocument();
322         }
323         return null;
324     }
325 
326     /**
327      * Returns the XML node associated with this UI node.
328      * <p/>
329      * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
330      * corresponding UI node will exist even if there is no corresponding XML node. Such structure
331      * is created and enforced by the parent of the tree, not the element themselves. However
332      * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
333      *
334      * @return The associated XML node. Can be null for mandatory nodes.
335      */
getXmlNode()336     public Node getXmlNode() {
337         return mXmlNode;
338     }
339 
340     /**
341      * Returns the {@link ElementDescriptor} for this node. This is never null.
342      * <p/>
343      * Do not use this to call getDescriptor().getAttributes(), instead call
344      * getAttributeDescriptors() which can be overriden by derived classes.
345      */
getDescriptor()346     public ElementDescriptor getDescriptor() {
347         return mDescriptor;
348     }
349 
350     /**
351      * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
352      * <p/>
353      * Use this instead of getDescriptor().getAttributes() -- derived classes can override
354      * this to manipulate the attribute descriptor list depending on the current UI node.
355      */
getAttributeDescriptors()356     public AttributeDescriptor[] getAttributeDescriptors() {
357         return mDescriptor.getAttributes();
358     }
359 
360     /**
361      * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
362      * This is a subset of the getAttributeDescriptors() list.
363      * <p/>
364      * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
365      * could override this to manipulate the attribute descriptor list depending on the current
366      * UI node. There's no need for it right now so keep it private.
367      */
getHiddenAttributeDescriptors()368     private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
369         if (mCachedHiddenAttributes == null) {
370             mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
371             for (AttributeDescriptor attr_desc : getAttributeDescriptors()) {
372                 if (attr_desc instanceof XmlnsAttributeDescriptor) {
373                     mCachedHiddenAttributes.put(
374                             ((XmlnsAttributeDescriptor) attr_desc).getXmlNsName(),
375                             attr_desc);
376                 }
377             }
378         }
379         return mCachedHiddenAttributes;
380     }
381 
382     /**
383      * Sets the parent of this UiElementNode.
384      * <p/>
385      * The root node has no parent.
386      */
setUiParent(UiElementNode parent)387     protected void setUiParent(UiElementNode parent) {
388         mUiParent = parent;
389         // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
390         clearAttributes();
391     }
392 
393     /**
394      * @return The parent {@link UiElementNode} or null if this is the root node.
395      */
getUiParent()396     public UiElementNode getUiParent() {
397         return mUiParent;
398     }
399 
400     /**
401      * Returns The root {@link UiElementNode}.
402      */
getUiRoot()403     public UiElementNode getUiRoot() {
404         UiElementNode root = this;
405         while (root.mUiParent != null) {
406             root = root.mUiParent;
407         }
408 
409         return root;
410     }
411 
412     /**
413      * Returns the previous UI sibling of this UI node.
414      * If the node does not have a previous sibling, returns null.
415      */
getUiPreviousSibling()416     public UiElementNode getUiPreviousSibling() {
417         if (mUiParent != null) {
418             List<UiElementNode> childlist = mUiParent.getUiChildren();
419             if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
420                 int index = childlist.indexOf(this);
421                 return index > 0 ? childlist.get(index - 1) : null;
422             }
423         }
424         return null;
425     }
426 
427     /**
428      * Returns the next UI sibling of this UI node.
429      * If the node does not have a next sibling, returns null.
430      */
getUiNextSibling()431     public UiElementNode getUiNextSibling() {
432         if (mUiParent != null) {
433             List<UiElementNode> childlist = mUiParent.getUiChildren();
434             if (childlist != null) {
435                 int size = childlist.size();
436                 if (size > 1 && childlist.get(size - 1) != this) {
437                     int index = childlist.indexOf(this);
438                     return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
439                 }
440             }
441         }
442         return null;
443     }
444 
445     /**
446      * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
447      * <p/>
448      * The editor must always be set on the root node. This method takes care of that.
449      */
setEditor(AndroidXmlEditor editor)450     public void setEditor(AndroidXmlEditor editor) {
451         if (mUiParent == null) {
452             mEditor = editor;
453         } else {
454             mUiParent.setEditor(editor);
455         }
456     }
457 
458     /**
459      * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}.
460      * <p/>
461      * The value is initially null until the node is attached to its parent -- the value
462      * of the root node is then propagated.
463      *
464      * @return The embedding {@link AndroidXmlEditor} or null.
465      */
getEditor()466     public AndroidXmlEditor getEditor() {
467         return mUiParent == null ? mEditor : mUiParent.getEditor();
468     }
469 
470     /**
471      * Returns the Android target data for the file being edited.
472      */
getAndroidTarget()473     public AndroidTargetData getAndroidTarget() {
474         return getEditor().getTargetData();
475     }
476 
477     /**
478      * @return A read-only version of the children collection.
479      */
getUiChildren()480     public List<UiElementNode> getUiChildren() {
481         if (mReadOnlyUiChildren == null) {
482             mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
483         }
484         return mReadOnlyUiChildren;
485     }
486 
487     /**
488      * @return A read-only version of the attributes collection.
489      */
getUiAttributes()490     public Collection<UiAttributeNode> getUiAttributes() {
491         if (mReadOnlyUiAttributes == null) {
492             mReadOnlyUiAttributes = Collections.unmodifiableCollection(
493                     getInternalUiAttributes().values());
494         }
495         return mReadOnlyUiAttributes;
496     }
497 
498     /**
499      * @return A read-only version of the unknown attributes collection.
500      */
getUnknownUiAttributes()501     public Collection<UiAttributeNode> getUnknownUiAttributes() {
502         return Collections.unmodifiableCollection(mUnknownUiAttributes);
503     }
504 
505     /**
506      * Sets the error flag value.
507      * @param errorFlag the error flag
508      */
setHasError(boolean errorFlag)509     public final void setHasError(boolean errorFlag) {
510         mHasError = errorFlag;
511     }
512 
513     /**
514      * Returns whether this node, its attributes, or one of the children nodes (and attributes)
515      * has errors.
516      */
hasError()517     public final boolean hasError() {
518         if (mHasError) {
519             return true;
520         }
521 
522         // get the error value from the attributes.
523         Collection<UiAttributeNode> attributes = getInternalUiAttributes().values();
524         for (UiAttributeNode attribute : attributes) {
525             if (attribute.hasError()) {
526                 return true;
527             }
528         }
529 
530         // and now from the children.
531         for (UiElementNode child : mUiChildren) {
532             if (child.hasError()) {
533                 return true;
534             }
535         }
536 
537         return false;
538     }
539 
540     /**
541      * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped
542      * XML names.
543      * <p/>
544      * The default is to have one that creates new {@link ElementDescriptor}.
545      * <p/>
546      * There is only one such provider in any UI model tree, attached to the root node.
547      *
548      * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null.
549      */
getUnknownDescriptorProvider()550     public IUnknownDescriptorProvider getUnknownDescriptorProvider() {
551         if (mUiParent != null) {
552             return mUiParent.getUnknownDescriptorProvider();
553         }
554         if (mUnknownDescProvider == null) {
555             // Create the default one on demand.
556             mUnknownDescProvider = new IUnknownDescriptorProvider() {
557 
558                 private final HashMap<String, ElementDescriptor> mMap =
559                     new HashMap<String, ElementDescriptor>();
560 
561                 /**
562                  * The default is to create a new ElementDescriptor wrapping
563                  * the unknown XML local name and reuse previously created descriptors.
564                  */
565                 public ElementDescriptor getDescriptor(String xmlLocalName) {
566 
567                     ElementDescriptor desc = mMap.get(xmlLocalName);
568 
569                     if (desc == null) {
570                         desc = new ElementDescriptor(xmlLocalName);
571                         mMap.put(xmlLocalName, desc);
572                     }
573 
574                     return desc;
575                 }
576             };
577         }
578         return mUnknownDescProvider;
579     }
580 
581     /**
582      * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped
583      * XML names.
584      * <p/>
585      * The default is to have one that creates new {@link ElementDescriptor}.
586      * <p/>
587      * There is only one such provider in any UI model tree, attached to the root node.
588      *
589      * @param unknownDescProvider The new provider to use. Must not be null.
590      */
setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider)591     public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) {
592         if (mUiParent == null) {
593             mUnknownDescProvider = unknownDescProvider;
594         } else {
595             mUiParent.setUnknownDescriptorProvider(unknownDescProvider);
596         }
597     }
598 
599     /**
600      * Adds a new {@link IUiUpdateListener} to the internal update listener list.
601      */
addUpdateListener(IUiUpdateListener listener)602     public void addUpdateListener(IUiUpdateListener listener) {
603        if (mUiUpdateListeners == null) {
604            mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
605        }
606        if (!mUiUpdateListeners.contains(listener)) {
607            mUiUpdateListeners.add(listener);
608        }
609     }
610 
611     /**
612      * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
613      * Does nothing if the list is empty or the listener is not registered.
614      */
removeUpdateListener(IUiUpdateListener listener)615     public void removeUpdateListener(IUiUpdateListener listener) {
616        if (mUiUpdateListeners != null) {
617            mUiUpdateListeners.remove(listener);
618        }
619     }
620 
621     /**
622      * Finds a child node relative to this node using a path-like expression.
623      * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
624      * returns the latter. If there are multiple nodes with the same name at the same
625      * level, always uses the first one found.
626      *
627      * @param path The path like expression to select a child node.
628      * @return The ui node found or null.
629      */
findUiChildNode(String path)630     public UiElementNode findUiChildNode(String path) {
631         String[] items = path.split("/");  //$NON-NLS-1$
632         UiElementNode ui_node = this;
633         for (String item : items) {
634             boolean next_segment = false;
635             for (UiElementNode c : ui_node.mUiChildren) {
636                 if (c.getDescriptor().getXmlName().equals(item)) {
637                     ui_node = c;
638                     next_segment = true;
639                     break;
640                 }
641             }
642             if (!next_segment) {
643                 return null;
644             }
645         }
646         return ui_node;
647     }
648 
649     /**
650      * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
651      * Looks recursively in all children UI nodes.
652      *
653      * @param xmlNode The XML node to look for.
654      * @return The {@link UiElementNode} that contains xmlNode or null if not found,
655      */
findXmlNode(Node xmlNode)656     public UiElementNode findXmlNode(Node xmlNode) {
657         if (xmlNode == null) {
658             return null;
659         }
660         if (getXmlNode() == xmlNode) {
661             return this;
662         }
663 
664         for (UiElementNode uiChild : mUiChildren) {
665             UiElementNode found = uiChild.findXmlNode(xmlNode);
666             if (found != null) {
667                 return found;
668             }
669         }
670 
671         return null;
672     }
673 
674     /**
675      * Returns the {@link UiAttributeNode} matching this attribute descriptor or
676      * null if not found.
677      *
678      * @param attr_desc The {@link AttributeDescriptor} to match.
679      * @return the {@link UiAttributeNode} matching this attribute descriptor or null
680      *         if not found.
681      */
findUiAttribute(AttributeDescriptor attr_desc)682     public UiAttributeNode findUiAttribute(AttributeDescriptor attr_desc) {
683         return getInternalUiAttributes().get(attr_desc);
684     }
685 
686     /**
687      * Populate this element node with all values from the given XML node.
688      *
689      * This fails if the given XML node has a different element name -- it won't change the
690      * type of this ui node.
691      *
692      * This method can be both used for populating values the first time and updating values
693      * after the XML model changed.
694      *
695      * @param xml_node The XML node to mirror
696      * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
697      */
loadFromXmlNode(Node xml_node)698     public boolean loadFromXmlNode(Node xml_node) {
699         boolean structure_changed = (mXmlNode != xml_node);
700         mXmlNode = xml_node;
701         if (xml_node != null) {
702             updateAttributeList(xml_node);
703             structure_changed |= updateElementList(xml_node);
704             invokeUiUpdateListeners(structure_changed ? UiUpdateState.CHILDREN_CHANGED
705                                                       : UiUpdateState.ATTR_UPDATED);
706         }
707         return structure_changed;
708     }
709 
710     /**
711      * Clears the UI node and reload it from the given XML node.
712      * <p/>
713      * This works by clearing all references to any previous XML or UI nodes and
714      * then reloads the XML document from scratch. The editor reference is kept.
715      * <p/>
716      * This is used in the special case where the ElementDescriptor structure has changed.
717      * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
718      * and reload everything. This is not subtle and should be used very rarely.
719      *
720      * @param xml_node The XML node or document to reload. Can be null.
721      */
reloadFromXmlNode(Node xml_node)722     public void reloadFromXmlNode(Node xml_node) {
723         // The editor needs to be preserved, it is not affected by an XML change.
724         AndroidXmlEditor editor = getEditor();
725         clearContent();
726         setEditor(editor);
727         if (xml_node != null) {
728             setXmlDocument(xml_node.getOwnerDocument());
729         }
730         // This will reload all the XML and recreate the UI structure from scratch.
731         loadFromXmlNode(xml_node);
732     }
733 
734     /**
735      * Called by attributes when they want to commit their value
736      * to an XML node.
737      * <p/>
738      * For mandatory nodes, this makes sure the underlying XML element node
739      * exists in the model. If not, it is created and assigned as the underlying
740      * XML node.
741      * </br>
742      * For non-mandatory nodes, simply return the underlying XML node, which
743      * must always exists.
744      *
745      * @return The XML node matching this {@link UiElementNode} or null.
746      */
prepareCommit()747     public Node prepareCommit() {
748         if (getDescriptor().isMandatory()) {
749             createXmlNode();
750             // The new XML node has been created.
751             // We don't need to refresh using loadFromXmlNode() since there are
752             // no attributes or elements that need to be loading into this node.
753         }
754         return getXmlNode();
755     }
756 
757     /**
758      * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
759      * This is called by the UI when the embedding part needs to be committed.
760      */
commit()761     public void commit() {
762         for (UiAttributeNode ui_attr : getInternalUiAttributes().values()) {
763             ui_attr.commit();
764         }
765 
766         for (UiAttributeNode ui_attr : mUnknownUiAttributes) {
767             ui_attr.commit();
768         }
769     }
770 
771     /**
772      * Returns true if the part has been modified with respect to the data
773      * loaded from the model.
774      */
isDirty()775     public boolean isDirty() {
776         for (UiAttributeNode ui_attr : getInternalUiAttributes().values()) {
777             if (ui_attr.isDirty()) {
778                 return true;
779             }
780         }
781 
782         for (UiAttributeNode ui_attr : mUnknownUiAttributes) {
783             if (ui_attr.isDirty()) {
784                 return true;
785             }
786         }
787 
788         return false;
789     }
790 
791     /**
792      * Creates the underlying XML element node for this UI node if it doesn't already
793      * exists.
794      *
795      * @return The new value of getXmlNode() (can be null if creation failed)
796      */
createXmlNode()797     public Node createXmlNode() {
798         if (mXmlNode != null) {
799             return null;
800         }
801         Node parentXmlNode = null;
802         if (mUiParent != null) {
803             parentXmlNode = mUiParent.prepareCommit();
804             if (parentXmlNode == null) {
805                 // The parent failed to create its own backing XML node. Abort.
806                 // No need to throw an exception, the parent will most likely
807                 // have done so itself.
808                 return null;
809             }
810         }
811 
812         String element_name = getDescriptor().getXmlName();
813         Document doc = getXmlDocument();
814 
815         // We *must* have a root node. If not, we need to abort.
816         if (doc == null) {
817             throw new RuntimeException(
818                     String.format("Missing XML document for %1$s XML node.", element_name));
819         }
820 
821         // If we get here and parent_xml_node is null, the node is to be created
822         // as the root node of the document (which can't be null, cf check above).
823         if (parentXmlNode == null) {
824             parentXmlNode = doc;
825         }
826 
827         mXmlNode = doc.createElement(element_name);
828 
829         Node xmlNextSibling = null;
830 
831         UiElementNode uiNextSibling = getUiNextSibling();
832         if (uiNextSibling != null) {
833             xmlNextSibling = uiNextSibling.getXmlNode();
834         }
835 
836         parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
837 
838         // Insert a separator after the tag, to make it easier to read
839         Text sep = doc.createTextNode("\n");
840         parentXmlNode.appendChild(sep);
841 
842         // Set all initial attributes in the XML node if they are not empty.
843         // Iterate on the descriptor list to get the desired order and then use the
844         // internal values, if any.
845         for (AttributeDescriptor attr_desc : getAttributeDescriptors()) {
846             if (attr_desc instanceof XmlnsAttributeDescriptor) {
847                 XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attr_desc;
848                 Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI,
849                         desc.getXmlNsName());
850                 attr.setValue(desc.getValue());
851                 attr.setPrefix(desc.getXmlNsPrefix());
852                 mXmlNode.getAttributes().setNamedItemNS(attr);
853             } else {
854                 UiAttributeNode ui_attr = getInternalUiAttributes().get(attr_desc);
855                 commitAttributeToXml(ui_attr, ui_attr.getCurrentValue());
856             }
857         }
858 
859         invokeUiUpdateListeners(UiUpdateState.CREATED);
860         return mXmlNode;
861     }
862 
863     /**
864      * Removes the XML node corresponding to this UI node if it exists
865      * and also removes all mirrored information in this UI node (i.e. children, attributes)
866      *
867      * @return The removed node or null if it didn't exist in the firtst place.
868      */
deleteXmlNode()869     public Node deleteXmlNode() {
870         if (mXmlNode == null) {
871             return null;
872         }
873 
874         // First clear the internals of the node and *then* actually deletes the XML
875         // node (because doing so will generate an update even and this node may be
876         // revisited via loadFromXmlNode).
877         Node old_xml_node = mXmlNode;
878         clearContent();
879 
880         Node xml_parent = old_xml_node.getParentNode();
881         if (xml_parent == null) {
882             xml_parent = getXmlDocument();
883         }
884         old_xml_node = xml_parent.removeChild(old_xml_node);
885 
886         invokeUiUpdateListeners(UiUpdateState.DELETED);
887         return old_xml_node;
888     }
889 
890     /**
891      * Updates the element list for this UiElementNode.
892      * At the end, the list of children UiElementNode here will match the one from the
893      * provided XML {@link Node}:
894      * <ul>
895      * <li> Walk both the current ui children list and the xml children list at the same time.
896      * <li> If we have a new xml child but already reached the end of the ui child list, add the
897      *      new xml node.
898      * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
899      *      move it here. It means the XML child list has been reordered.
900      * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
901      * <li> At the end, we may have finished walking the xml child list but still have remaining
902      *      ui children, simply delete them as they matching trailing xml nodes that have been
903      *      removed unless they are mandatory ui nodes.
904      * </ul>
905      * Note that only the first case is used when populating the ui list the first time.
906      *
907      * @param xml_node The XML node to mirror
908      * @return True when the XML structure has changed.
909      */
updateElementList(Node xml_node)910     protected boolean updateElementList(Node xml_node) {
911         boolean structure_changed = false;
912         int ui_index = 0;
913         Node xml_child = xml_node.getFirstChild();
914         while (xml_child != null) {
915             if (xml_child.getNodeType() == Node.ELEMENT_NODE) {
916                 String element_name = xml_child.getNodeName();
917                 UiElementNode ui_node = null;
918                 if (mUiChildren.size() <= ui_index) {
919                     // A new node is being added at the end of the list
920                     ElementDescriptor desc = mDescriptor.findChildrenDescriptor(element_name,
921                             false /* recursive */);
922                     if (desc == null) {
923                         // Unknown node. Create a temporary descriptor for it.
924                         // We'll add unknown attributes to it later.
925                         IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
926                         desc = p.getDescriptor(element_name);
927                     }
928                     structure_changed = true;
929                     ui_node = appendNewUiChild(desc);
930                     ui_index++;
931                 } else {
932                     // A new node is being inserted or moved.
933                     // Note: mandatory nodes can be created without an XML node in which case
934                     // getXmlNode() is null.
935                     UiElementNode ui_child;
936                     int n = mUiChildren.size();
937                     for (int j = ui_index; j < n; j++) {
938                         ui_child = mUiChildren.get(j);
939                         if (ui_child.getXmlNode() != null && ui_child.getXmlNode() == xml_child) {
940                             if (j > ui_index) {
941                                 // Found the same XML node at some later index, now move it here.
942                                 mUiChildren.remove(j);
943                                 mUiChildren.add(ui_index, ui_child);
944                                 structure_changed = true;
945                             }
946                             ui_node = ui_child;
947                             ui_index++;
948                             break;
949                         }
950                     }
951 
952                     if (ui_node == null) {
953                         // Look for an unused mandatory node with no XML node attached
954                         // referencing the same XML element name
955                         for (int j = ui_index; j < n; j++) {
956                             ui_child = mUiChildren.get(j);
957                             if (ui_child.getXmlNode() == null &&
958                                     ui_child.getDescriptor().isMandatory() &&
959                                     ui_child.getDescriptor().getXmlName().equals(element_name)) {
960                                 if (j > ui_index) {
961                                     // Found it, now move it here
962                                     mUiChildren.remove(j);
963                                     mUiChildren.add(ui_index, ui_child);
964                                 }
965                                 // assign the XML node to this empty mandatory element.
966                                 ui_child.mXmlNode = xml_child;
967                                 structure_changed = true;
968                                 ui_node = ui_child;
969                                 ui_index++;
970                             }
971                         }
972                     }
973 
974                     if (ui_node == null) {
975                         // Inserting new node
976                         ElementDescriptor desc = mDescriptor.findChildrenDescriptor(element_name,
977                                 false /* recursive */);
978                         if (desc == null) {
979                             // Unknown element. Simply ignore it.
980                             AdtPlugin.log(IStatus.WARNING,
981                                     "AndroidManifest: Ignoring unknown '%s' XML element", //$NON-NLS-1$
982                                     element_name);
983                         } else {
984                             structure_changed = true;
985                             ui_node = insertNewUiChild(ui_index, desc);
986                             ui_index++;
987                         }
988                     }
989                 }
990                 if (ui_node != null) {
991                     // If we touched an UI Node, even an existing one, refresh its content.
992                     // For new nodes, this will populate them recursively.
993                     structure_changed |= ui_node.loadFromXmlNode(xml_child);
994                 }
995             }
996             xml_child = xml_child.getNextSibling();
997         }
998 
999         // There might be extra UI nodes at the end if the XML node list got shorter.
1000         for (int index = mUiChildren.size() - 1; index >= ui_index; --index) {
1001              structure_changed |= removeUiChildAtIndex(index);
1002         }
1003 
1004         return structure_changed;
1005     }
1006 
1007     /**
1008      * Internal helper to remove an UI child node given by its index in the
1009      * internal child list.
1010      *
1011      * Also invokes the update listener on the node to be deleted *after* the node has
1012      * been removed.
1013      *
1014      * @param ui_index The index of the UI child to remove, range 0 .. mUiChildren.size()-1
1015      * @return True if the structure has changed
1016      * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
1017      *         know that could never happen unless the computer is on fire or something.
1018      */
removeUiChildAtIndex(int ui_index)1019     private boolean removeUiChildAtIndex(int ui_index) {
1020         UiElementNode ui_node = mUiChildren.get(ui_index);
1021         ElementDescriptor desc = ui_node.getDescriptor();
1022 
1023         try {
1024             if (ui_node.getDescriptor().isMandatory()) {
1025                 // This is a mandatory node. Such a node must exist in the UiNode hierarchy
1026                 // even if there's no XML counterpart. However we only need to keep one.
1027 
1028                 // Check if the parent (e.g. this node) has another similar ui child node.
1029                 boolean keepNode = true;
1030                 for (UiElementNode child : mUiChildren) {
1031                     if (child != ui_node && child.getDescriptor() == desc) {
1032                         // We found another child with the same descriptor that is not
1033                         // the node we want to remove. This means we have one mandatory
1034                         // node so we can safely remove ui_node.
1035                         keepNode = false;
1036                         break;
1037                     }
1038                 }
1039 
1040                 if (keepNode) {
1041                     // We can't remove a mandatory node as we need to keep at least one
1042                     // mandatory node in the parent. Instead we just clear its content
1043                     // (including its XML Node reference).
1044 
1045                     // A mandatory node with no XML means it doesn't really exist, so it can't be
1046                     // deleted. So the structure will change only if the ui node is actually
1047                     // associated to an XML node.
1048                     boolean xml_exists = (ui_node.getXmlNode() != null);
1049 
1050                     ui_node.clearContent();
1051                     return xml_exists;
1052                 }
1053             }
1054 
1055             mUiChildren.remove(ui_index);
1056             return true;
1057         } finally {
1058             // Tell listeners that a node has been removed.
1059             // The model has already been modified.
1060             invokeUiUpdateListeners(UiUpdateState.DELETED);
1061         }
1062     }
1063 
1064     /**
1065      * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
1066      * and appends it to the end of the element children list.
1067      *
1068      * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
1069      * @return The new UI node that has been appended
1070      */
appendNewUiChild(ElementDescriptor descriptor)1071     public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
1072         UiElementNode ui_node;
1073         ui_node = descriptor.createUiNode();
1074         mUiChildren.add(ui_node);
1075         ui_node.setUiParent(this);
1076         ui_node.invokeUiUpdateListeners(UiUpdateState.CREATED);
1077         return ui_node;
1078     }
1079 
1080     /**
1081      * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
1082      * and inserts it in the element children list at the specified position.
1083      *
1084      * @param index The position where to insert in the element children list.
1085      * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
1086      * @return The new UI node.
1087      */
insertNewUiChild(int index, ElementDescriptor descriptor)1088     public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
1089         UiElementNode ui_node;
1090         ui_node = descriptor.createUiNode();
1091         mUiChildren.add(index, ui_node);
1092         ui_node.setUiParent(this);
1093         ui_node.invokeUiUpdateListeners(UiUpdateState.CREATED);
1094         return ui_node;
1095     }
1096 
1097     /**
1098      * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}.
1099      * <p/>
1100      * For a given {@link UiElementNode}, the attribute list always exists in
1101      * full and is totally independent of whether the XML model actually
1102      * has the corresponding attributes.
1103      * <p/>
1104      * For each attribute declared in this {@link UiElementNode}, get
1105      * the corresponding XML attribute. It may not exist, in which case the
1106      * value will be null. We don't really know if a value has changed, so
1107      * the updateValue() is called on the UI sattribute in all cases.
1108      *
1109      * @param xmlNode The XML node to mirror
1110      */
updateAttributeList(Node xmlNode)1111     protected void updateAttributeList(Node xmlNode) {
1112         NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
1113         HashSet<Node> visited = new HashSet<Node>();
1114 
1115         // For all known (i.e. expected) UI attributes, find an existing XML attribute of
1116         // same (uri, local name) and update the internal Ui attribute value.
1117         for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
1118             AttributeDescriptor desc = uiAttr.getDescriptor();
1119             if (!(desc instanceof SeparatorAttributeDescriptor)) {
1120                 Node xmlAttr = xmlAttrMap == null ? null :
1121                     xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
1122                 uiAttr.updateValue(xmlAttr);
1123                 visited.add(xmlAttr);
1124             }
1125         }
1126 
1127         // Clone the current list of unknown attributes. We'll then remove from this list when
1128         // we still attributes which are still unknown. What will be left are the old unknown
1129         // attributes that have been deleted in the current XML attribute list.
1130         @SuppressWarnings("unchecked") //$NON-NLS-1$
1131         HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();
1132 
1133         // We need to ignore hidden attributes.
1134         Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();
1135 
1136         // Traverse the actual XML attribute list to find unknown attributes
1137         if (xmlAttrMap != null) {
1138             for (int i = 0; i < xmlAttrMap.getLength(); i++) {
1139                 Node xmlAttr = xmlAttrMap.item(i);
1140                 // Ignore attributes which have actual descriptors
1141                 if (visited.contains(xmlAttr)) {
1142                     continue;
1143                 }
1144 
1145                 String xmlFullName = xmlAttr.getNodeName();
1146 
1147                 // Ignore attributes which are hidden (based on the prefix:localName key)
1148                 if (hiddenAttrDesc.containsKey(xmlFullName)) {
1149                     continue;
1150                 }
1151 
1152                 String xmlAttrLocalName = xmlAttr.getLocalName();
1153                 String xmlNsUri = xmlAttr.getNamespaceURI();
1154 
1155                 UiAttributeNode uiAttr = null;
1156                 for (UiAttributeNode a : mUnknownUiAttributes) {
1157                     String aLocalName = a.getDescriptor().getXmlLocalName();
1158                     String aNsUri = a.getDescriptor().getNamespaceUri();
1159                     if (aLocalName.equals(xmlAttrLocalName) &&
1160                             (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
1161                         // This attribute is still present in the unknown list
1162                         uiAttr = a;
1163                         // It has not been deleted
1164                         deleted.remove(a);
1165                         break;
1166                     }
1167                 }
1168                 if (uiAttr == null) {
1169                     uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri);
1170                 }
1171 
1172                 uiAttr.updateValue(xmlAttr);
1173             }
1174 
1175             // Remove from the internal list unknown attributes that have been deleted from the xml
1176             for (UiAttributeNode a : deleted) {
1177                 mUnknownUiAttributes.remove(a);
1178             }
1179         }
1180     }
1181 
addUnknownAttribute(String xmlFullName, String xmlAttrLocalName, String xmlNsUri)1182     private UiAttributeNode addUnknownAttribute(String xmlFullName,
1183             String xmlAttrLocalName, String xmlNsUri) {
1184         // Create a new unknown attribute of format string
1185         TextAttributeDescriptor desc = new TextAttributeDescriptor(
1186                 xmlAttrLocalName,           // xml name
1187                 xmlFullName,                // ui name
1188                 xmlNsUri,                   // NS uri
1189                 "Unknown XML attribute",    // tooltip, translatable
1190                 new AttributeInfo(xmlAttrLocalName, new Format[] { Format.STRING } )
1191                 );
1192         UiAttributeNode uiAttr = desc.createUiNode(this);
1193         mUnknownUiAttributes.add(uiAttr);
1194         return uiAttr;
1195     }
1196 
1197     /**
1198      * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node.
1199      */
invokeUiUpdateListeners(UiUpdateState state)1200     protected void invokeUiUpdateListeners(UiUpdateState state) {
1201         if (mUiUpdateListeners != null) {
1202             for (IUiUpdateListener listener : mUiUpdateListeners) {
1203                 try {
1204                     listener.uiElementNodeUpdated(this, state);
1205                 } catch (Exception e) {
1206                     // prevent a crashing listener from crashing the whole invocation chain
1207                     AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s",  //$NON-NLS-1$
1208                             getBreadcrumbTrailDescription(true),
1209                             state.toString());
1210                 }
1211             }
1212         }
1213     }
1214 
1215     // --- for derived implementations only ---
1216 
1217     // TODO doc
setXmlNode(Node xml_node)1218     protected void setXmlNode(Node xml_node) {
1219         mXmlNode = xml_node;
1220     }
1221 
1222     /**
1223      * Sets the temporary data used by the editors.
1224      * @param data the data.
1225      *
1226      * @since GLE1
1227      * @deprecated Used by GLE1. Should be deprecated for GLE2.
1228      */
setEditData(Object data)1229     public void setEditData(Object data) {
1230         mEditData = data;
1231     }
1232 
1233     /**
1234      * Returns the temporary data used by the editors for this object.
1235      * @return the data, or <code>null</code> if none has been set.
1236      */
getEditData()1237     public Object getEditData() {
1238         return mEditData;
1239     }
1240 
refreshUi()1241     public void refreshUi() {
1242         invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
1243     }
1244 
1245 
1246     // ------------- Helpers
1247 
1248     /**
1249      * Helper method to commit a single attribute value to XML.
1250      * <p/>
1251      * This method updates the XML regardless of the current XML value.
1252      * Callers should check first if an update is needed.
1253      * If the new value is empty, the XML attribute will be actually removed.
1254      * <p/>
1255      * Note that the caller MUST ensure that modifying the underlying XML model is
1256      * safe and must take care of marking the model as dirty if necessary.
1257      *
1258      * @see AndroidXmlEditor#editXmlModel(Runnable)
1259      *
1260      * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
1261      * @param newValue The new value to set.
1262      * @return True if the XML attribute was modified or removed, false if nothing changed.
1263      */
commitAttributeToXml(UiAttributeNode uiAttr, String newValue)1264     public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
1265         // Get (or create) the underlying XML element node that contains the attributes.
1266         Node element = prepareCommit();
1267         if (element != null && uiAttr != null) {
1268             String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
1269             String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();
1270 
1271             NamedNodeMap attrMap = element.getAttributes();
1272             if (newValue == null || newValue.length() == 0) {
1273                 // Remove attribute if it's empty
1274                 if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
1275                     attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
1276                     return true;
1277                 }
1278             } else {
1279                 // Add or replace an attribute
1280                 Document doc = element.getOwnerDocument();
1281                 if (doc != null) {
1282                     Attr attr = doc.createAttributeNS(attrNsUri, attrLocalName);
1283                     attr.setValue(newValue);
1284                     attr.setPrefix(lookupNamespacePrefix(element, attrNsUri));
1285                     attrMap.setNamedItemNS(attr);
1286                     return true;
1287                 }
1288             }
1289         }
1290         return false;
1291     }
1292 
1293     /**
1294      * Helper method to commit all dirty attributes values to XML.
1295      * <p/>
1296      * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has
1297      * been called more than once and all the attributes marked as dirty must be committed to
1298      * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
1299      * attribute.
1300      * <p/>
1301      * Note that the caller MUST ensure that modifying the underlying XML model is
1302      * safe and must take care of marking the model as dirty if necessary.
1303      *
1304      * @see AndroidXmlEditor#editXmlModel(Runnable)
1305      *
1306      * @return True if one or more values were actually modified or removed,
1307      *         false if nothing changed.
1308      */
commitDirtyAttributesToXml()1309     public boolean commitDirtyAttributesToXml() {
1310         boolean result = false;
1311         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1312 
1313         for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
1314             UiAttributeNode ui_attr = entry.getValue();
1315             if (ui_attr.isDirty()) {
1316                 result |= commitAttributeToXml(ui_attr, ui_attr.getCurrentValue());
1317                 ui_attr.setDirty(false);
1318             }
1319         }
1320         return result;
1321     }
1322 
1323     /**
1324      * Returns the namespace prefix matching the requested namespace URI.
1325      * If no such declaration is found, returns the default "android" prefix.
1326      *
1327      * @param node The current node. Must not be null.
1328      * @param nsUri The namespace URI of which the prefix is to be found,
1329      *              e.g. SdkConstants.NS_RESOURCES
1330      * @return The first prefix declared or the default "android" prefix.
1331      */
lookupNamespacePrefix(Node node, String nsUri)1332     private String lookupNamespacePrefix(Node node, String nsUri) {
1333         // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
1334         // The following code emulates this simple call:
1335         //   String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES);
1336 
1337         // if the requested URI is null, it denotes an attribute with no namespace.
1338         if (nsUri == null) {
1339             return null;
1340         }
1341 
1342         // per XML specification, the "xmlns" URI is reserved
1343         if (XmlnsAttributeDescriptor.XMLNS_URI.equals(nsUri)) {
1344             return "xmlns"; //$NON-NLS-1$
1345         }
1346 
1347         HashSet<String> visited = new HashSet<String>();
1348         Document doc = node == null ? null : node.getOwnerDocument();
1349 
1350         // Ask the document about it. This method may not be implemented by the Document.
1351         String nsPrefix = null;
1352         try {
1353             nsPrefix = doc.lookupPrefix(nsUri);
1354             if (nsPrefix != null) {
1355                 return nsPrefix;
1356             }
1357         } catch (Throwable t) {
1358             // ignore
1359         }
1360 
1361         // If that failed, try to look it up manually.
1362         // This also gathers prefixed in use in the case we want to generate a new one below.
1363         for (; node != null && node.getNodeType() == Node.ELEMENT_NODE;
1364                node = node.getParentNode()) {
1365             NamedNodeMap attrs = node.getAttributes();
1366             for (int n = attrs.getLength() - 1; n >= 0; --n) {
1367                 Node attr = attrs.item(n);
1368                 if ("xmlns".equals(attr.getPrefix())) {  //$NON-NLS-1$
1369                     String uri = attr.getNodeValue();
1370                     nsPrefix = attr.getLocalName();
1371                     // Is this the URI we are looking for? If yes, we found its prefix.
1372                     if (nsUri.equals(uri)) {
1373                         return nsPrefix;
1374                     }
1375                     visited.add(nsPrefix);
1376                 }
1377             }
1378         }
1379 
1380         // Failed the find a prefix. Generate a new sensible default prefix.
1381         //
1382         // We need to make sure the prefix is not one that was declared in the scope
1383         // visited above. Use a default namespace prefix "android" for the Android resource
1384         // NS and use "ns" for all other custom namespaces.
1385         String prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$
1386         String base = prefix;
1387         for (int i = 1; visited.contains(prefix); i++) {
1388             prefix = base + Integer.toString(i);
1389         }
1390         // Also create & define this prefix/URI in the XML document as an attribute in the
1391         // first element of the document.
1392         if (doc != null) {
1393             node = doc.getFirstChild();
1394             while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
1395                 node = node.getNextSibling();
1396             }
1397             if (node != null) {
1398                 Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI, prefix);
1399                 attr.setValue(nsUri);
1400                 attr.setPrefix("xmlns"); //$NON-NLS-1$
1401                 node.getAttributes().setNamedItemNS(attr);
1402             }
1403         }
1404 
1405         return prefix;
1406     }
1407 
1408     /**
1409      * Utility method to internally set the value of a text attribute for the current
1410      * UiElementNode.
1411      * <p/>
1412      * This method is a helper. It silently ignores the errors such as the requested
1413      * attribute not being present in the element or attribute not being settable.
1414      * It accepts inherited attributes (such as layout).
1415      * <p/>
1416      * This does not commit to the XML model. It does mark the attribute node as dirty.
1417      * This is up to the caller.
1418      *
1419      * @see #commitAttributeToXml(UiAttributeNode, String)
1420      * @see #commitDirtyAttributesToXml()
1421      *
1422      * @param attrXmlName The XML <em>local</em> name of the attribute to modify
1423      * @param attrNsUri The namespace URI of the attribute.
1424      *                  Can be null if the attribute uses the global namespace.
1425      * @param value The new value for the attribute. If set to null, the attribute is removed.
1426      * @param override True if the value must be set even if one already exists.
1427      * @return The {@link UiAttributeNode} that has been modified or null.
1428      */
setAttributeValue( String attrXmlName, String attrNsUri, String value, boolean override)1429     public UiAttributeNode setAttributeValue(
1430             String attrXmlName,
1431             String attrNsUri,
1432             String value,
1433             boolean override) {
1434         if (value == null) {
1435             value = ""; //$NON-NLS-1$ -- this removes an attribute
1436         }
1437 
1438         // Try with all internal attributes
1439         UiAttributeNode uiAttr = setInternalAttrValue(
1440                 getInternalUiAttributes().values(), attrXmlName, attrNsUri, value, override);
1441 
1442         // Look at existing unknown (aka custom) attributes
1443         uiAttr = setInternalAttrValue(
1444                 getUnknownUiAttributes(), attrXmlName, attrNsUri, value, override);
1445 
1446         if (uiAttr == null) {
1447             // Failed to find the attribute. For non-android attributes that is mostly expected,
1448             // in which case we just create a new custom one.
1449 
1450             uiAttr = addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri);
1451         }
1452 
1453         return uiAttr;
1454     }
1455 
setInternalAttrValue( Collection<UiAttributeNode> attributes, String attrXmlName, String attrNsUri, String value, boolean override)1456     private UiAttributeNode setInternalAttrValue(
1457             Collection<UiAttributeNode> attributes,
1458             String attrXmlName,
1459             String attrNsUri,
1460             String value,
1461             boolean override) {
1462         for (UiAttributeNode uiAttr : attributes) {
1463             AttributeDescriptor uiDesc = uiAttr.getDescriptor();
1464 
1465             if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
1466                 // Both NS URI must be either null or equal.
1467                 if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) ||
1468                         (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) {
1469 
1470                     // Not all attributes are editable, ignore those which are not.
1471                     if (uiAttr instanceof IUiSettableAttributeNode) {
1472                         String current = uiAttr.getCurrentValue();
1473                         // Only update (and mark as dirty) if the attribute did not have any
1474                         // value or if the value was different.
1475                         if (override || current == null || !current.equals(value)) {
1476                             ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value);
1477                             // mark the attribute as dirty since their internal content
1478                             // as been modified, but not the underlying XML model
1479                             uiAttr.setDirty(true);
1480                             return uiAttr;
1481                         }
1482                     }
1483 
1484                     // We found the attribute but it's not settable. Since attributes are
1485                     // not duplicated, just abandon here.
1486                     break;
1487                 }
1488             }
1489         }
1490 
1491         return null;
1492     }
1493 
1494     /**
1495      * Utility method to retrieve the internal value of an attribute.
1496      * <p/>
1497      * Note that this retrieves the *field* value if the attribute has some UI, and
1498      * not the actual XML value. They may differ if the attribute is dirty.
1499      *
1500      * @param attrXmlName The XML name of the attribute to modify
1501      * @return The current internal value for the attribute or null in case of error.
1502      */
getAttributeValue(String attrXmlName)1503     public String getAttributeValue(String attrXmlName) {
1504         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1505 
1506         for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
1507             AttributeDescriptor ui_desc = entry.getKey();
1508             if (ui_desc.getXmlLocalName().equals(attrXmlName)) {
1509                 UiAttributeNode ui_attr = entry.getValue();
1510                 return ui_attr.getCurrentValue();
1511             }
1512         }
1513         return null;
1514     }
1515 
1516     // ------ IPropertySource methods
1517 
getEditableValue()1518     public Object getEditableValue() {
1519         return null;
1520     }
1521 
1522     /*
1523      * (non-Javadoc)
1524      * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
1525      *
1526      * Returns the property descriptor for this node. Since the descriptors are not linked to the
1527      * data, the AttributeDescriptor are used directly.
1528      */
getPropertyDescriptors()1529     public IPropertyDescriptor[] getPropertyDescriptors() {
1530         List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();
1531 
1532         // get the standard descriptors
1533         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1534         Set<AttributeDescriptor> keys = attributeMap.keySet();
1535 
1536 
1537         // we only want the descriptor that do implement the IPropertyDescriptor interface.
1538         for (AttributeDescriptor key : keys) {
1539             if (key instanceof IPropertyDescriptor) {
1540                 propDescs.add((IPropertyDescriptor)key);
1541             }
1542         }
1543 
1544         // now get the descriptor from the unknown attributes
1545         for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
1546             if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
1547                 propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
1548             }
1549         }
1550 
1551         // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
1552         return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
1553     }
1554 
1555     /*
1556      * (non-Javadoc)
1557      * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
1558      *
1559      * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
1560      * which return the AttributeDescriptor itself.
1561      */
getPropertyValue(Object id)1562     public Object getPropertyValue(Object id) {
1563         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1564 
1565         UiAttributeNode attribute = attributeMap.get(id);
1566 
1567         if (attribute == null) {
1568             // look for the id in the unknown attributes.
1569             for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
1570                 if (id == unknownAttr.getDescriptor()) {
1571                     return unknownAttr;
1572                 }
1573             }
1574         }
1575 
1576         return attribute;
1577     }
1578 
1579     /*
1580      * (non-Javadoc)
1581      * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
1582      *
1583      * Returns whether the property is set. In our case this is if the string is non empty.
1584      */
isPropertySet(Object id)1585     public boolean isPropertySet(Object id) {
1586         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1587 
1588         UiAttributeNode attribute = attributeMap.get(id);
1589 
1590         if (attribute != null) {
1591             return attribute.getCurrentValue().length() > 0;
1592         }
1593 
1594         // look for the id in the unknown attributes.
1595         for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
1596             if (id == unknownAttr.getDescriptor()) {
1597                 return unknownAttr.getCurrentValue().length() > 0;
1598             }
1599         }
1600 
1601         return false;
1602     }
1603 
1604     /*
1605      * (non-Javadoc)
1606      * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
1607      *
1608      * Reset the property to its default value. For now we simply empty it.
1609      */
resetPropertyValue(Object id)1610     public void resetPropertyValue(Object id) {
1611         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1612 
1613         UiAttributeNode attribute = attributeMap.get(id);
1614         if (attribute != null) {
1615             // TODO: reset the value of the attribute
1616 
1617             return;
1618         }
1619 
1620         // look for the id in the unknown attributes.
1621         for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
1622             if (id == unknownAttr.getDescriptor()) {
1623                 // TODO: reset the value of the attribute
1624 
1625                 return;
1626             }
1627         }
1628     }
1629 
1630     /*
1631      * (non-Javadoc)
1632      * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
1633      *
1634      * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
1635      * AttributeDescriptor itself. Value should be a String.
1636      */
setPropertyValue(Object id, Object value)1637     public void setPropertyValue(Object id, Object value) {
1638         HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
1639 
1640         UiAttributeNode attribute = attributeMap.get(id);
1641 
1642         if (attribute == null) {
1643             // look for the id in the unknown attributes.
1644             for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
1645                 if (id == unknownAttr.getDescriptor()) {
1646                     attribute = unknownAttr;
1647                     break;
1648                 }
1649             }
1650         }
1651 
1652         if (attribute != null) {
1653 
1654             // get the current value and compare it to the new value
1655             String oldValue = attribute.getCurrentValue();
1656             final String newValue = (String)value;
1657 
1658             if (oldValue.equals(newValue)) {
1659                 return;
1660             }
1661 
1662             final UiAttributeNode fAttribute = attribute;
1663             AndroidXmlEditor editor = getEditor();
1664             editor.editXmlModel(new Runnable() {
1665                 public void run() {
1666                     commitAttributeToXml(fAttribute, newValue);
1667                 }
1668             });
1669         }
1670     }
1671 }
1672