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