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
17package com.android.ide.eclipse.adt.internal.editors.descriptors;
18
19import static com.android.SdkConstants.ANDROID_URI;
20import static com.android.SdkConstants.ATTR_ID;
21import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
22import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
23import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
24import static com.android.SdkConstants.ATTR_TEXT;
25import static com.android.SdkConstants.EDIT_TEXT;
26import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
27import static com.android.SdkConstants.FQCN_ADAPTER_VIEW;
28import static com.android.SdkConstants.GALLERY;
29import static com.android.SdkConstants.GRID_LAYOUT;
30import static com.android.SdkConstants.GRID_VIEW;
31import static com.android.SdkConstants.GT_ENTITY;
32import static com.android.SdkConstants.ID_PREFIX;
33import static com.android.SdkConstants.LIST_VIEW;
34import static com.android.SdkConstants.LT_ENTITY;
35import static com.android.SdkConstants.NEW_ID_PREFIX;
36import static com.android.SdkConstants.RELATIVE_LAYOUT;
37import static com.android.SdkConstants.REQUEST_FOCUS;
38import static com.android.SdkConstants.SPACE;
39import static com.android.SdkConstants.VALUE_FILL_PARENT;
40import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
41import static com.android.SdkConstants.VIEW_INCLUDE;
42import static com.android.SdkConstants.VIEW_MERGE;
43
44import com.android.SdkConstants;
45import com.android.annotations.NonNull;
46import com.android.ide.common.api.IAttributeInfo.Format;
47import com.android.ide.common.resources.platform.AttributeInfo;
48import com.android.ide.eclipse.adt.AdtConstants;
49import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
50import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
51import com.android.resources.ResourceType;
52
53import org.eclipse.swt.graphics.Image;
54
55import java.util.ArrayList;
56import java.util.EnumSet;
57import java.util.HashSet;
58import java.util.List;
59import java.util.Locale;
60import java.util.Map;
61import java.util.Map.Entry;
62import java.util.Set;
63import java.util.regex.Matcher;
64import java.util.regex.Pattern;
65
66
67/**
68 * Utility methods related to descriptors handling.
69 */
70public 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     */
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     */
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     */
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