• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.descriptors;
18 
19 import com.android.ide.eclipse.adt.AndroidConstants;
20 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutConstants;
21 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
22 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
23 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
24 import com.android.ide.eclipse.adt.internal.resources.DeclareStyleableInfo.AttributeInfo;
25 import com.android.ide.eclipse.adt.internal.resources.DeclareStyleableInfo.AttributeInfo.Format;
26 import com.android.sdklib.SdkConstants;
27 
28 import org.eclipse.swt.graphics.Image;
29 
30 import java.lang.reflect.Constructor;
31 import java.lang.reflect.InvocationTargetException;
32 import java.util.ArrayList;
33 import java.util.HashSet;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.Map.Entry;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 
40 
41 /**
42  * Utility methods related to descriptors handling.
43  */
44 public final class DescriptorsUtils {
45 
46     private static final String DEFAULT_WIDGET_PREFIX = "widget";
47 
48     private static final int JAVADOC_BREAK_LENGTH = 60;
49 
50     /**
51      * The path in the online documentation for the manifest description.
52      * <p/>
53      * This is NOT a complete URL. To be used, it needs to be appended
54      * to {@link AndroidConstants#CODESITE_BASE_URL} or to the local SDK
55      * documentation.
56      */
57     public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#";  //$NON-NLS-1$
58 
59     public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
60 
61     private static final String CODE  = "$code";  //$NON-NLS-1$
62     private static final String LINK  = "$link";  //$NON-NLS-1$
63     private static final String ELEM  = "$elem";  //$NON-NLS-1$
64     private static final String BREAK = "$break"; //$NON-NLS-1$
65 
66     /**
67      * The {@link ITextAttributeCreator} interface is used by the appendAttribute() method
68      * to provide a way for caller to override the kind of {@link TextAttributeDescriptor}
69      * created for a give XML attribute name.
70      */
71     public interface ITextAttributeCreator {
72         /**
73          * Creates a new {@link TextAttributeDescriptor} instance for the given XML name,
74          * UI name and tooltip.
75          *
76          * @param xmlName The XML attribute name.
77          * @param uiName The UI attribute name.
78          * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
79          *              See {@link SdkConstants#NS_RESOURCES} for a common value.
80          * @param tooltip An optional tooltip.
81          * @return A new {@link TextAttributeDescriptor} (or derived) instance.
82          */
create(String xmlName, String uiName, String nsUri, String tooltip)83         public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri,
84                 String tooltip);
85     }
86 
87     /**
88      * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
89      *
90      * @param attributes The list of {@link AttributeDescriptor} to append to
91      * @param elementXmlName Optional XML local name of the element to which attributes are
92      *              being added. When not null, this is used to filter overrides.
93      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
94      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
95      * @param infos The array of {@link AttributeInfo} to read and append to attributes
96      * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append
97      *        a "*" to their UI name as a hint for the user.) If not null, must contains
98      *        entries in the form "elem-name/attr-name". Elem-name can be "*".
99      * @param overrides A map [attribute name => TextAttributeDescriptor creator]. A creator
100      *        can either by a Class<? extends TextAttributeDescriptor> or an instance of
101      *        {@link ITextAttributeCreator} that instantiates the right TextAttributeDescriptor.
102      */
appendAttributes(ArrayList<AttributeDescriptor> attributes, String elementXmlName, String nsUri, AttributeInfo[] infos, Set<String> requiredAttributes, Map<String, Object> overrides)103     public static void appendAttributes(ArrayList<AttributeDescriptor> attributes,
104             String elementXmlName,
105             String nsUri, AttributeInfo[] infos,
106             Set<String> requiredAttributes,
107             Map<String, Object> overrides) {
108         for (AttributeInfo info : infos) {
109             boolean required = false;
110             if (requiredAttributes != null) {
111                 String attr_name = info.getName();
112                 if (requiredAttributes.contains("*/" + attr_name) ||
113                         requiredAttributes.contains(elementXmlName + "/" + attr_name)) {
114                     required = true;
115                 }
116             }
117             appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides);
118         }
119     }
120 
121     /**
122      * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
123      *
124      * @param attributes The list of {@link AttributeDescriptor} to append to
125      * @param elementXmlName Optional XML local name of the element to which attributes are
126      *              being added. When not null, this is used to filter overrides.
127      * @param info The {@link AttributeInfo} to append to attributes
128      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
129      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
130      * @param required True if the attribute is to be marked as "required" (i.e. append
131      *        a "*" to its UI name as a hint for the user.)
132      * @param overrides A map [attribute name => TextAttributeDescriptor creator]. A creator
133      *        can either by a Class<? extends TextAttributeDescriptor> or an instance of
134      *        {@link ITextAttributeCreator} that instantiates the right TextAttributeDescriptor.
135      */
appendAttribute(ArrayList<AttributeDescriptor> attributes, String elementXmlName, String nsUri, AttributeInfo info, boolean required, Map<String, Object> overrides)136     public static void appendAttribute(ArrayList<AttributeDescriptor> attributes,
137             String elementXmlName,
138             String nsUri,
139             AttributeInfo info, boolean required,
140             Map<String, Object> overrides) {
141         AttributeDescriptor attr = null;
142 
143         String xmlLocalName = info.getName();
144         String uiName = prettyAttributeUiName(info.getName()); // ui_name
145         if (required) {
146             uiName += "*"; //$NON-NLS-1$
147         }
148 
149         String tooltip = null;
150         String rawTooltip = info.getJavaDoc();
151         if (rawTooltip == null) {
152             rawTooltip = "";
153         }
154 
155         String deprecated = info.getDeprecatedDoc();
156         if (deprecated != null) {
157             if (rawTooltip.length() > 0) {
158                 rawTooltip += "@@"; //$NON-NLS-1$ insert a break
159             }
160             rawTooltip += "* Deprecated";
161             if (deprecated.length() != 0) {
162                 rawTooltip += ": " + deprecated;                            //$NON-NLS-1$
163             }
164             if (deprecated.length() == 0 || !deprecated.endsWith(".")) {    //$NON-NLS-1$
165                 rawTooltip += ".";                                          //$NON-NLS-1$
166             }
167         }
168 
169         // Add the known types to the tooltip
170         Format[] formats_list = info.getFormats();
171         int flen = formats_list.length;
172         if (flen > 0) {
173             // Fill the formats in a set for faster access
174             HashSet<Format> formats_set = new HashSet<Format>();
175 
176             StringBuilder sb = new StringBuilder();
177             if (rawTooltip != null && rawTooltip.length() > 0) {
178                 sb.append(rawTooltip);
179                 sb.append(" ");     //$NON-NLS-1$
180             }
181             if (sb.length() > 0) {
182                 sb.append("@@");    //$NON-NLS-1$  @@ inserts a break before the types
183             }
184             sb.append("[");         //$NON-NLS-1$
185             for (int i = 0; i < flen; i++) {
186                 Format f = formats_list[i];
187                 formats_set.add(f);
188 
189                 sb.append(f.toString().toLowerCase());
190                 if (i < flen - 1) {
191                     sb.append(", "); //$NON-NLS-1$
192                 }
193             }
194             // The extra space at the end makes the tooltip more readable on Windows.
195             sb.append("]"); //$NON-NLS-1$
196 
197             if (required) {
198                 sb.append(".@@* ");          //$NON-NLS-1$ @@ inserts a break.
199                 sb.append("Required.");
200             }
201 
202             // The extra space at the end makes the tooltip more readable on Windows.
203             sb.append(" "); //$NON-NLS-1$
204 
205             rawTooltip = sb.toString();
206             tooltip = formatTooltip(rawTooltip);
207 
208             // Create a specialized attribute if we can
209             if (overrides != null) {
210                 for (Entry<String, Object> entry: overrides.entrySet()) {
211                     String key = entry.getKey();
212                     String elements[] = key.split("/");          //$NON-NLS-1$
213                     String overrideAttrLocalName = null;
214                     if (elements.length < 1) {
215                         continue;
216                     } else if (elements.length == 1) {
217                         overrideAttrLocalName = elements[0];
218                         elements = null;
219                     } else {
220                         overrideAttrLocalName = elements[elements.length - 1];
221                         elements = elements[0].split(",");       //$NON-NLS-1$
222                     }
223 
224                     if (overrideAttrLocalName == null ||
225                             !overrideAttrLocalName.equals(xmlLocalName)) {
226                         continue;
227                     }
228 
229                     boolean ok_element = elements.length < 1;
230                     if (!ok_element) {
231                         for (String element : elements) {
232                             if (element.equals("*")              //$NON-NLS-1$
233                                     || element.equals(elementXmlName)) {
234                                 ok_element = true;
235                                 break;
236                             }
237                         }
238                     }
239 
240                     if (!ok_element) {
241                         continue;
242                     }
243 
244                     Object override = entry.getValue();
245                     if (override instanceof Class) {
246                         try {
247                             // The override is instance of the class to create, which must
248                             // have a constructor compatible with TextAttributeDescriptor.
249                             @SuppressWarnings("unchecked") //$NON-NLS-1$
250                             Class<? extends TextAttributeDescriptor> clazz =
251                                 (Class<? extends TextAttributeDescriptor>) override;
252                             Constructor<? extends TextAttributeDescriptor> cons;
253                                 cons = clazz.getConstructor(new Class<?>[] {
254                                         String.class, String.class, String.class, String.class } );
255                             attr = cons.newInstance(
256                                     new Object[] { xmlLocalName, uiName, nsUri, tooltip });
257                         } catch (SecurityException e) {
258                             // ignore
259                         } catch (NoSuchMethodException e) {
260                             // ignore
261                         } catch (IllegalArgumentException e) {
262                             // ignore
263                         } catch (InstantiationException e) {
264                             // ignore
265                         } catch (IllegalAccessException e) {
266                             // ignore
267                         } catch (InvocationTargetException e) {
268                             // ignore
269                         }
270                     } else if (override instanceof ITextAttributeCreator) {
271                         attr = ((ITextAttributeCreator) override).create(
272                                 xmlLocalName, uiName, nsUri, tooltip);
273                     }
274                 }
275             } // if overrides
276 
277             // Create a specialized descriptor if we can, based on type
278             if (attr == null) {
279                 if (formats_set.contains(Format.REFERENCE)) {
280                     // This is either a multi-type reference or a generic reference.
281                     attr = new ReferenceAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip);
282                 } else if (formats_set.contains(Format.ENUM)) {
283                     attr = new ListAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip,
284                             info.getEnumValues());
285                 } else if (formats_set.contains(Format.FLAG)) {
286                     attr = new FlagAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip,
287                             info.getFlagValues());
288                 } else if (formats_set.contains(Format.BOOLEAN)) {
289                     attr = new BooleanAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip);
290                 } else if (formats_set.contains(Format.STRING)) {
291                     attr = new ReferenceAttributeDescriptor(ResourceType.STRING,
292                             xmlLocalName, uiName, nsUri,
293                             tooltip);
294                 }
295             }
296         }
297 
298         // By default a simple text field is used
299         if (attr == null) {
300             if (tooltip == null) {
301                 tooltip = formatTooltip(rawTooltip);
302             }
303             attr = new TextAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip);
304         }
305         attr.setDeprecated(info.getDeprecatedDoc() != null);
306         attributes.add(attr);
307     }
308 
309     /**
310      * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of
311      * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same
312      * XML name.
313      *
314      * @param attributes The list of {@link AttributeDescriptor} to compare to.
315      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
316      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
317      * @param info The {@link AttributeInfo} to know whether it is included in the above list.
318      * @return True if this {@link AttributeInfo} is already present in
319      *         the {@link AttributeDescriptor} list.
320      */
321     public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes,
322             String nsUri,
323             AttributeInfo info) {
324         String xmlLocalName = info.getName();
325         for (AttributeDescriptor desc : attributes) {
326             if (desc.getXmlLocalName().equals(xmlLocalName)) {
327                 if (nsUri == desc.getNamespaceUri() ||
328                         (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) {
329                     return true;
330                 }
331             }
332         }
333         return false;
334     }
335 
336     /**
337      * Create a pretty attribute UI name from an XML name.
338      * <p/>
339      * The original xml name starts with a lower case and is camel-case,
340      * e.g. "maxWidthForView". The pretty name starts with an upper case
341      * and has space separators, e.g. "Max width for view".
342      */
343     public static String prettyAttributeUiName(String name) {
344         if (name.length() < 1) {
345             return name;
346         }
347         StringBuffer buf = new StringBuffer();
348 
349         char c = name.charAt(0);
350         // Use upper case initial letter
351         buf.append((char)(c >= 'a' && c <= 'z' ? c + 'A' - 'a' : c));
352         int len = name.length();
353         for (int i = 1; i < len; i++) {
354             c = name.charAt(i);
355             if (c >= 'A' && c <= 'Z') {
356                 // Break camel case into separate words
357                 buf.append(' ');
358                 // Use a lower case initial letter for the next word, except if the
359                 // word is solely X, Y or Z.
360                 if (c >= 'X' && c <= 'Z' &&
361                         (i == len-1 ||
362                             (i < len-1 && name.charAt(i+1) >= 'A' && name.charAt(i+1) <= 'Z'))) {
363                     buf.append(c);
364                 } else {
365                     buf.append((char)(c - 'A' + 'a'));
366                 }
367             } else if (c == '_') {
368                 buf.append(' ');
369             } else {
370                 buf.append(c);
371             }
372         }
373 
374         name = buf.toString();
375 
376         // Replace these acronyms by upper-case versions
377         // - (?<=^| ) means "if preceded by a space or beginning of string"
378         // - (?=$| )  means "if followed by a space or end of string"
379         name = name.replaceAll("(?<=^| )sdk(?=$| )", "SDK");
380         name = name.replaceAll("(?<=^| )uri(?=$| )", "URI");
381 
382         return name;
383     }
384 
385     /**
386      * Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z].
387      * Returns the string unmodified if the first character is not [a-z].
388      *
389      * @param str The string to capitalize.
390      * @return The capitalized string
391      */
392     public static String capitalize(String str) {
393         if (str == null || str.length() < 1 || str.charAt(0) < 'a' || str.charAt(0) > 'z') {
394             return str;
395         }
396 
397         StringBuilder sb = new StringBuilder();
398         sb.append((char)(str.charAt(0) + 'A' - 'a'));
399         sb.append(str.substring(1));
400         return sb.toString();
401     }
402 
403     /**
404      * Formats the javadoc tooltip to be usable in a tooltip.
405      */
406     public static String formatTooltip(String javadoc) {
407         ArrayList<String> spans = scanJavadoc(javadoc);
408 
409         StringBuilder sb = new StringBuilder();
410         boolean needBreak = false;
411 
412         for (int n = spans.size(), i = 0; i < n; ++i) {
413             String s = spans.get(i);
414             if (CODE.equals(s)) {
415                 s = spans.get(++i);
416                 if (s != null) {
417                     sb.append('"').append(s).append('"');
418                 }
419             } else if (LINK.equals(s)) {
420                 String base   = spans.get(++i);
421                 String anchor = spans.get(++i);
422                 String text   = spans.get(++i);
423 
424                 if (base != null) {
425                     base = base.trim();
426                 }
427                 if (anchor != null) {
428                     anchor = anchor.trim();
429                 }
430                 if (text != null) {
431                     text = text.trim();
432                 }
433 
434                 // If there's no text, use the anchor if there's one
435                 if (text == null || text.length() == 0) {
436                     text = anchor;
437                 }
438 
439                 if (base != null && base.length() > 0) {
440                     if (text == null || text.length() == 0) {
441                         // If we still have no text, use the base as text
442                         text = base;
443                     }
444                 }
445 
446                 if (text != null) {
447                     sb.append(text);
448                 }
449 
450             } else if (ELEM.equals(s)) {
451                 s = spans.get(++i);
452                 if (s != null) {
453                     sb.append(s);
454                 }
455             } else if (BREAK.equals(s)) {
456                 needBreak = true;
457             } else if (s != null) {
458                 if (needBreak && s.trim().length() > 0) {
459                     sb.append('\r');
460                 }
461                 sb.append(s);
462                 needBreak = false;
463             }
464         }
465 
466         return sb.toString();
467     }
468 
469     /**
470      * Formats the javadoc tooltip to be usable in a FormText.
471      * <p/>
472      * If the descriptor can provide an icon, the caller should provide
473      * elementsDescriptor.getIcon() as "image" to FormText, e.g.:
474      * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code>
475      *
476      * @param javadoc The javadoc to format. Cannot be null.
477      * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null.
478      * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be
479      *   <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code>
480      */
481     public static String formatFormText(String javadoc,
482             ElementDescriptor elementDescriptor,
483             String androidDocBaseUrl) {
484         ArrayList<String> spans = scanJavadoc(javadoc);
485 
486         String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL;
487         String sdkUrl = elementDescriptor.getSdkUrl();
488         if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) {
489             fullSdkUrl = androidDocBaseUrl + sdkUrl;
490         }
491 
492         StringBuilder sb = new StringBuilder();
493 
494         Image icon = elementDescriptor.getIcon();
495         if (icon != null) {
496             sb.append("<form><li style=\"image\" value=\"" +        //$NON-NLS-1$
497                     IMAGE_KEY + "\">");                             //$NON-NLS-1$
498         } else {
499             sb.append("<form><p>");                                 //$NON-NLS-1$
500         }
501 
502         for (int n = spans.size(), i = 0; i < n; ++i) {
503             String s = spans.get(i);
504             if (CODE.equals(s)) {
505                 s = spans.get(++i);
506                 if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) {
507                     sb.append("<a href=\"");                        //$NON-NLS-1$
508                     sb.append(fullSdkUrl);
509                     sb.append("\">");                               //$NON-NLS-1$
510                     sb.append(s);
511                     sb.append("</a>");                              //$NON-NLS-1$
512                 } else if (s != null) {
513                     sb.append('"').append(s).append('"');
514                 }
515             } else if (LINK.equals(s)) {
516                 String base   = spans.get(++i);
517                 String anchor = spans.get(++i);
518                 String text   = spans.get(++i);
519 
520                 if (base != null) {
521                     base = base.trim();
522                 }
523                 if (anchor != null) {
524                     anchor = anchor.trim();
525                 }
526                 if (text != null) {
527                     text = text.trim();
528                 }
529 
530                 // If there's no text, use the anchor if there's one
531                 if (text == null || text.length() == 0) {
532                     text = anchor;
533                 }
534 
535                 // TODO specialize with a base URL for views, menus & other resources
536                 // Base is empty for a local page anchor, in which case we'll replace it
537                 // by the element SDK URL if it exists.
538                 if ((base == null || base.length() == 0) && fullSdkUrl != null) {
539                     base = fullSdkUrl;
540                 }
541 
542                 String url = null;
543                 if (base != null && base.length() > 0) {
544                     if (base.startsWith("http")) {                  //$NON-NLS-1$
545                         // If base looks an URL, use it, with the optional anchor
546                         url = base;
547                         if (anchor != null && anchor.length() > 0) {
548                             // If the base URL already has an anchor, it needs to be
549                             // removed first. If there's no anchor, we need to add "#"
550                             int pos = url.lastIndexOf('#');
551                             if (pos < 0) {
552                                 url += "#";                         //$NON-NLS-1$
553                             } else if (pos < url.length() - 1) {
554                                 url = url.substring(0, pos + 1);
555                             }
556 
557                             url += anchor;
558                         }
559                     } else if (text == null || text.length() == 0) {
560                         // If we still have no text, use the base as text
561                         text = base;
562                     }
563                 }
564 
565                 if (url != null && text != null) {
566                     sb.append("<a href=\"");                        //$NON-NLS-1$
567                     sb.append(url);
568                     sb.append("\">");                               //$NON-NLS-1$
569                     sb.append(text);
570                     sb.append("</a>");                              //$NON-NLS-1$
571                 } else if (text != null) {
572                     sb.append("<b>").append(text).append("</b>");   //$NON-NLS-1$ //$NON-NLS-2$
573                 }
574 
575             } else if (ELEM.equals(s)) {
576                 s = spans.get(++i);
577                 if (sdkUrl != null && s != null) {
578                     sb.append("<a href=\"");                        //$NON-NLS-1$
579                     sb.append(sdkUrl);
580                     sb.append("\">");                               //$NON-NLS-1$
581                     sb.append(s);
582                     sb.append("</a>");                              //$NON-NLS-1$
583                 } else if (s != null) {
584                     sb.append("<b>").append(s).append("</b>");      //$NON-NLS-1$ //$NON-NLS-2$
585                 }
586             } else if (BREAK.equals(s)) {
587                 // ignore line breaks in pseudo-HTML rendering
588             } else if (s != null) {
589                 sb.append(s);
590             }
591         }
592 
593         if (icon != null) {
594             sb.append("</li></form>");                              //$NON-NLS-1$
595         } else {
596             sb.append("</p></form>");                               //$NON-NLS-1$
597         }
598         return sb.toString();
599     }
600 
601     private static ArrayList<String> scanJavadoc(String javadoc) {
602         ArrayList<String> spans = new ArrayList<String>();
603 
604         // Standardize all whitespace in the javadoc to single spaces.
605         if (javadoc != null) {
606             javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$
607         }
608 
609         // Detects {@link <base>#<name> <text>} where all 3 are optional
610         Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$
611         // Detects <code>blah</code>
612         Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)");                 //$NON-NLS-1$
613         // Detects @blah@, used in hard-coded tooltip descriptors
614         Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)");                       //$NON-NLS-1$
615         // Detects a buffer that starts by @@ (request for a break)
616         Pattern p_break = Pattern.compile("@@(.*)");                                //$NON-NLS-1$
617         // Detects a buffer that starts by @ < or { (one that was not matched above)
618         Pattern p_open = Pattern.compile("([@<\\{])(.*)");                          //$NON-NLS-1$
619         // Detects everything till the next potential separator, i.e. @ < or {
620         Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)");                        //$NON-NLS-1$
621 
622         int currentLength = 0;
623         String text = null;
624 
625         while(javadoc != null && javadoc.length() > 0) {
626             Matcher m;
627             String s = null;
628             if ((m = p_code.matcher(javadoc)).matches()) {
629                 spans.add(CODE);
630                 spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text
631                 javadoc = m.group(2);
632                 if (text != null) {
633                     currentLength += text.length();
634                 }
635             } else if ((m = p_link.matcher(javadoc)).matches()) {
636                 spans.add(LINK);
637                 spans.add(m.group(1)); // @link base
638                 spans.add(m.group(2)); // @link anchor
639                 spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text
640                 javadoc = m.group(4);
641                 if (text != null) {
642                     currentLength += text.length();
643                 }
644             } else if ((m = p_elem.matcher(javadoc)).matches()) {
645                 spans.add(ELEM);
646                 spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@
647                 javadoc = m.group(2);
648                 if (text != null) {
649                     currentLength += text.length() - 2;
650                 }
651             } else if ((m = p_break.matcher(javadoc)).matches()) {
652                 spans.add(BREAK);
653                 currentLength = 0;
654                 javadoc = m.group(1);
655             } else if ((m = p_open.matcher(javadoc)).matches()) {
656                 s = m.group(1);
657                 javadoc = m.group(2);
658             } else if ((m = p_text.matcher(javadoc)).matches()) {
659                 s = m.group(1);
660                 javadoc = m.group(2);
661             } else {
662                 // This is not supposed to happen. In case of, just use everything.
663                 s = javadoc;
664                 javadoc = null;
665             }
666             if (s != null && s.length() > 0) {
667                 s = cleanupJavadocHtml(s);
668 
669                 if (currentLength >= JAVADOC_BREAK_LENGTH) {
670                     spans.add(BREAK);
671                     currentLength = 0;
672                 }
673                 while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) {
674                     int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength);
675                     if (pos <= 0) {
676                         break;
677                     }
678                     spans.add(s.substring(0, pos + 1));
679                     spans.add(BREAK);
680                     currentLength = 0;
681                     s = s.substring(pos + 1);
682                 }
683 
684                 spans.add(s);
685                 currentLength += s.length();
686             }
687         }
688 
689         return spans;
690     }
691 
692     /**
693      * Remove anything that looks like HTML from a javadoc snippet, as it is supported
694      * neither by FormText nor a standard text tooltip.
695      */
696     private static String cleanupJavadocHtml(String s) {
697         if (s != null) {
698             s = s.replaceAll("&lt;", "\"");     //$NON-NLS-1$ $NON-NLS-2$
699             s = s.replaceAll("&gt;", "\"");     //$NON-NLS-1$ $NON-NLS-2$
700             s = s.replaceAll("<[^>]+>", "");    //$NON-NLS-1$ $NON-NLS-2$
701         }
702         return s;
703     }
704 
705     /**
706      * Sets the default layout attributes for the a new UiElementNode.
707      * <p/>
708      * Note that ideally the node should already be part of a hierarchy so that its
709      * parent layout and previous sibling can be determined, if any.
710      * <p/>
711      * This does not override attributes which are not empty.
712      */
713     public static void setDefaultLayoutAttributes(UiElementNode ui_node, boolean updateLayout) {
714         // if this ui_node is a layout and we're adding it to a document, use fill_parent for
715         // both W/H. Otherwise default to wrap_layout.
716         boolean fill = ui_node.getDescriptor().hasChildren() &&
717                        ui_node.getUiParent() instanceof UiDocumentNode;
718         ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_WIDTH,
719                 fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT,
720                 false /* override */);
721         ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_HEIGHT,
722                 fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT,
723                 false /* override */);
724 
725         String widget_id = getFreeWidgetId(ui_node);
726         if (widget_id != null) {
727             ui_node.setAttributeValue(LayoutConstants.ATTR_ID, widget_id, false /* override */);
728         }
729 
730         ui_node.setAttributeValue(LayoutConstants.ATTR_TEXT, widget_id, false /*override*/);
731 
732         if (updateLayout) {
733             UiElementNode ui_parent = ui_node.getUiParent();
734             if (ui_parent != null &&
735                     ui_parent.getDescriptor().getXmlLocalName().equals(
736                             LayoutConstants.RELATIVE_LAYOUT)) {
737                 UiElementNode ui_previous = ui_node.getUiPreviousSibling();
738                 if (ui_previous != null) {
739                     String id = ui_previous.getAttributeValue(LayoutConstants.ATTR_ID);
740                     if (id != null && id.length() > 0) {
741                         id = id.replace("@+", "@");                     //$NON-NLS-1$ //$NON-NLS-2$
742                         ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_BELOW, id,
743                                 false /* override */);
744                     }
745                 }
746             }
747         }
748     }
749 
750     /**
751      * Given a UI root node, returns the first available id that matches the
752      * pattern "prefix%02d".
753      * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
754      *
755      * @param uiNode The UI node that gives the prefix to match.
756      * @return A suitable generated id in the attribute form needed by the XML id tag
757      * (e.g. "@+id/something")
758      */
759     public static String getFreeWidgetId(UiElementNode uiNode) {
760         String name = uiNode.getDescriptor().getXmlLocalName();
761         if ("TabWidget".equals(name)) {                        //$NON-NLS-1$
762             return "@android:id/tabs";                         //$NON-NLS-1$
763         }
764 
765         return "@+id/" + getFreeWidgetId(uiNode.getUiRoot(),   //$NON-NLS-1$
766                 new Object[] { name, null, null, null });
767     }
768 
769     /**
770      * Given a UI root node, returns the first available id that matches the
771      * pattern "prefix%02d".
772      *
773      * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters
774      * in methods and we're not going to do a dedicated type, we just use an object array which
775      * must contain one initial item and several are built on the fly just for internal storage:
776      * <ul>
777      * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null.
778      * <li> index(Integer): The minimum index of the generated id. Must start with null.
779      * <li> generated(String): The generated widget currently being searched. Must start with null.
780      * <li> map(Set<String>): A set of the ids collected so far when walking through the widget
781      *                        hierarchy. Must start with null.
782      * </ul>
783      *
784      * @param uiRoot The Ui root node where to start searching recursively. For the initial call
785      *               you want to pass the document root.
786      * @param params An in-out context of parameters used during recursion, as explained above.
787      * @return A suitable generated id
788      */
789     @SuppressWarnings("unchecked")
790     private static String getFreeWidgetId(UiElementNode uiRoot,
791             Object[] params) {
792 
793         Set<String> map = (Set<String>)params[3];
794         if (map == null) {
795             params[3] = map = new HashSet<String>();
796         }
797 
798         int num = params[1] == null ? 0 : ((Integer)params[1]).intValue();
799 
800         String generated = (String) params[2];
801         String prefix = (String) params[0];
802         if (generated == null) {
803             int pos = prefix.indexOf('.');
804             if (pos >= 0) {
805                 prefix = prefix.substring(pos + 1);
806             }
807             pos = prefix.indexOf('$');
808             if (pos >= 0) {
809                 prefix = prefix.substring(pos + 1);
810             }
811             prefix = prefix.replaceAll("[^a-zA-Z]", "");                //$NON-NLS-1$ $NON-NLS-2$
812             if (prefix.length() == 0) {
813                 prefix = DEFAULT_WIDGET_PREFIX;
814             }
815 
816             do {
817                 num++;
818                 generated = String.format("%1$s%2$02d", prefix, num);   //$NON-NLS-1$
819             } while (map.contains(generated));
820 
821             params[0] = prefix;
822             params[1] = num;
823             params[2] = generated;
824         }
825 
826         String id = uiRoot.getAttributeValue(LayoutConstants.ATTR_ID);
827         if (id != null) {
828             id = id.replace("@+id/", "");                               //$NON-NLS-1$ $NON-NLS-2$
829             id = id.replace("@id/", "");                                //$NON-NLS-1$ $NON-NLS-2$
830             if (map.add(id) && map.contains(generated)) {
831 
832                 do {
833                     num++;
834                     generated = String.format("%1$s%2$02d", prefix, num);   //$NON-NLS-1$
835                 } while (map.contains(generated));
836 
837                 params[1] = num;
838                 params[2] = generated;
839             }
840         }
841 
842         for (UiElementNode uiChild : uiRoot.getUiChildren()) {
843             getFreeWidgetId(uiChild, params);
844         }
845 
846         // Note: return params[2] (not "generated") since it could have changed during recursion.
847         return (String) params[2];
848     }
849 
850 }
851