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