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.common.resources.platform;
18 
19 import static com.android.SdkConstants.DOT_LAYOUT_PARAMS;
20 import static com.android.ide.eclipse.adt.AdtConstants.DOC_HIDE;
21 
22 import com.android.ide.common.api.IAttributeInfo.Format;
23 import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
24 import com.android.ide.eclipse.adt.AdtUtils;
25 import com.android.utils.ILogger;
26 import com.google.common.collect.Maps;
27 
28 import org.w3c.dom.Document;
29 import org.w3c.dom.Node;
30 import org.xml.sax.SAXException;
31 
32 import java.io.File;
33 import java.io.IOException;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.EnumSet;
37 import java.util.HashMap;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.Map.Entry;
41 
42 import javax.xml.parsers.DocumentBuilder;
43 import javax.xml.parsers.DocumentBuilderFactory;
44 import javax.xml.parsers.ParserConfigurationException;
45 
46 /**
47  * Parser for attributes description files.
48  */
49 public final class AttrsXmlParser {
50 
51     public static final String ANDROID_MANIFEST_STYLEABLE = "AndroidManifest";  //$NON-NLS-1$
52 
53     private Document mDocument;
54     private String mOsAttrsXmlPath;
55 
56     // all attributes that have the same name are supposed to have the same
57     // parameters so we'll keep a cache of them to avoid processing them twice.
58     private Map<String, AttributeInfo> mAttributeMap;
59 
60     /** Map of all attribute names for a given element */
61     private final Map<String, DeclareStyleableInfo> mStyleMap =
62         new HashMap<String, DeclareStyleableInfo>();
63 
64     /** Map from format name (lower case) to the uppercase version */
65     private Map<String, Format> mFormatNames = new HashMap<String, Format>(10);
66 
67     /**
68      * Map of all (constant, value) pairs for attributes of format enum or flag.
69      * E.g. for attribute name=gravity, this tells us there's an enum/flag called "center"
70      * with value 0x11.
71      */
72     private Map<String, Map<String, Integer>> mEnumFlagValues;
73 
74     /**
75      * A logger object. Must not be null.
76      */
77     private final ILogger mLog;
78 
79     /**
80      * Creates a new {@link AttrsXmlParser}, set to load things from the given
81      * XML file. Nothing has been parsed yet. Callers should call {@link #preload()}
82      * next.
83      *
84      * @param osAttrsXmlPath The path of the <code>attrs.xml</code> file to parse.
85      *              Must not be null. Should point to an existing valid XML document.
86      * @param log A logger object. Must not be null.
87      * @param expectedAttributeCount expected number of attributes in the file
88      */
AttrsXmlParser(String osAttrsXmlPath, ILogger log, int expectedAttributeCount)89     public AttrsXmlParser(String osAttrsXmlPath, ILogger log, int expectedAttributeCount) {
90         this(osAttrsXmlPath, null /* inheritableAttributes */, log, expectedAttributeCount);
91     }
92 
93     /**
94      * Returns the parsed map of attribute infos
95      *
96      * @return a map from string name to {@link AttributeInfo}
97      */
getAttributeMap()98     public Map<String, AttributeInfo> getAttributeMap() {
99         return mAttributeMap;
100     }
101 
102     /**
103      * Creates a new {@link AttrsXmlParser} set to load things from the given
104      * XML file.
105      * <p/>
106      * If inheritableAttributes is non-null, it must point to a preloaded
107      * {@link AttrsXmlParser} which attributes will be used for this one. Since
108      * already defined attributes are not modifiable, they are thus "inherited".
109      *
110      * @param osAttrsXmlPath The path of the <code>attrs.xml</code> file to parse.
111      *              Must not be null. Should point to an existing valid XML document.
112      * @param inheritableAttributes An optional parser with attributes to inherit. Can be null.
113      *              If not null, the parser must have had its {@link #preload()} method
114      *              invoked prior to being used here.
115      * @param log A logger object. Must not be null.
116      * @param expectedAttributeCount expected number of attributes in the file
117      */
AttrsXmlParser( String osAttrsXmlPath, AttrsXmlParser inheritableAttributes, ILogger log, int expectedAttributeCount)118     public AttrsXmlParser(
119             String osAttrsXmlPath,
120             AttrsXmlParser inheritableAttributes,
121             ILogger log,
122             int expectedAttributeCount) {
123         mOsAttrsXmlPath = osAttrsXmlPath;
124         mLog = log;
125 
126         assert osAttrsXmlPath != null;
127         assert log != null;
128 
129         mAttributeMap = Maps.newHashMapWithExpectedSize(expectedAttributeCount);
130         if (inheritableAttributes == null) {
131             mEnumFlagValues = new HashMap<String, Map<String,Integer>>();
132         } else {
133             mAttributeMap.putAll(inheritableAttributes.mAttributeMap);
134             mEnumFlagValues = new HashMap<String, Map<String,Integer>>(
135                                                          inheritableAttributes.mEnumFlagValues);
136         }
137 
138         // Pre-compute the set of format names such that we don't have to compute the uppercase
139         // version of the same format string names again and again
140         for (Format f : Format.values()) {
141             mFormatNames.put(f.name().toLowerCase(Locale.US), f);
142         }
143     }
144 
145     /**
146      * Returns the OS path of the attrs.xml file parsed.
147      */
getOsAttrsXmlPath()148     public String getOsAttrsXmlPath() {
149         return mOsAttrsXmlPath;
150     }
151 
152     /**
153      * Preloads the document, parsing all attributes and declared styles.
154      *
155      * @return Self, for command chaining.
156      */
preload()157     public AttrsXmlParser preload() {
158         Document doc = getDocument();
159 
160         if (doc == null) {
161             mLog.warning("Failed to find %1$s", //$NON-NLS-1$
162                     mOsAttrsXmlPath);
163             return this;
164         }
165 
166         Node res = doc.getFirstChild();
167         while (res != null &&
168                 res.getNodeType() != Node.ELEMENT_NODE &&
169                 !res.getNodeName().equals("resources")) { //$NON-NLS-1$
170             res = res.getNextSibling();
171         }
172 
173         if (res == null) {
174             mLog.warning("Failed to find a <resources> node in %1$s", //$NON-NLS-1$
175                     mOsAttrsXmlPath);
176             return this;
177         }
178 
179         parseResources(res);
180         return this;
181     }
182 
183     /**
184      * Loads all attributes & javadoc for the view class info based on the class name.
185      */
loadViewAttributes(ViewClassInfo info)186     public void loadViewAttributes(ViewClassInfo info) {
187         if (getDocument() != null) {
188             String xmlName = info.getShortClassName();
189             DeclareStyleableInfo style = mStyleMap.get(xmlName);
190             if (style != null) {
191                 String definedBy = info.getFullClassName();
192                 AttributeInfo[] attributes = style.getAttributes();
193                 for (AttributeInfo attribute : attributes) {
194                     if (attribute.getDefinedBy() == null) {
195                         attribute.setDefinedBy(definedBy);
196                     }
197                 }
198                 info.setAttributes(attributes);
199                 info.setJavaDoc(style.getJavaDoc());
200             }
201         }
202     }
203 
204     /**
205      * Loads all attributes for the layout data info based on the class name.
206      */
loadLayoutParamsAttributes(LayoutParamsInfo info)207     public void loadLayoutParamsAttributes(LayoutParamsInfo info) {
208         if (getDocument() != null) {
209             // Transforms "LinearLayout" and "LayoutParams" into "LinearLayout_Layout".
210             ViewClassInfo viewLayoutClass = info.getViewLayoutClass();
211             String xmlName = String.format("%1$s_%2$s", //$NON-NLS-1$
212                     viewLayoutClass.getShortClassName(),
213                     info.getShortClassName());
214             xmlName = AdtUtils.stripSuffix(xmlName, "Params"); //$NON-NLS-1$
215 
216             DeclareStyleableInfo style = mStyleMap.get(xmlName);
217             if (style != null) {
218                 // For defined by, use the actual class name, e.g.
219                 //   android.widget.LinearLayout.LayoutParams
220                 String definedBy = viewLayoutClass.getFullClassName() + DOT_LAYOUT_PARAMS;
221                 AttributeInfo[] attributes = style.getAttributes();
222                 for (AttributeInfo attribute : attributes) {
223                     if (attribute.getDefinedBy() == null) {
224                         attribute.setDefinedBy(definedBy);
225                     }
226                 }
227                 info.setAttributes(attributes);
228             }
229         }
230     }
231 
232     /**
233      * Returns a list of all <code>declare-styleable</code> found in the XML file.
234      */
getDeclareStyleableList()235     public Map<String, DeclareStyleableInfo> getDeclareStyleableList() {
236         return Collections.unmodifiableMap(mStyleMap);
237     }
238 
239     /**
240      * Returns a map of all enum and flag constants sorted by parent attribute name.
241      * The map is attribute_name => (constant_name => integer_value).
242      */
getEnumFlagValues()243     public Map<String, Map<String, Integer>> getEnumFlagValues() {
244         return mEnumFlagValues;
245     }
246 
247     //-------------------------
248 
249     /**
250      * Creates an XML document from the attrs.xml OS path.
251      * May return null if the file doesn't exist or cannot be parsed.
252      */
getDocument()253     private Document getDocument() {
254         if (mDocument == null) {
255             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
256             factory.setIgnoringComments(false);
257             try {
258                 DocumentBuilder builder = factory.newDocumentBuilder();
259                 mDocument = builder.parse(new File(mOsAttrsXmlPath));
260             } catch (ParserConfigurationException e) {
261                 mLog.error(e, "Failed to create XML document builder for %1$s", //$NON-NLS-1$
262                         mOsAttrsXmlPath);
263             } catch (SAXException e) {
264                 mLog.error(e, "Failed to parse XML document %1$s", //$NON-NLS-1$
265                         mOsAttrsXmlPath);
266             } catch (IOException e) {
267                 mLog.error(e, "Failed to read XML document %1$s", //$NON-NLS-1$
268                         mOsAttrsXmlPath);
269             }
270         }
271         return mDocument;
272     }
273 
274     /**
275      * Finds all the &lt;declare-styleable&gt; and &lt;attr&gt; nodes
276      * in the top &lt;resources&gt; node.
277      */
parseResources(Node res)278     private void parseResources(Node res) {
279 
280         Map<String, String> unknownParents = new HashMap<String, String>();
281 
282         Node lastComment = null;
283         for (Node node = res.getFirstChild(); node != null; node = node.getNextSibling()) {
284             switch (node.getNodeType()) {
285             case Node.COMMENT_NODE:
286                 lastComment = node;
287                 break;
288             case Node.ELEMENT_NODE:
289                 if (node.getNodeName().equals("declare-styleable")) {          //$NON-NLS-1$
290                     Node nameNode = node.getAttributes().getNamedItem("name"); //$NON-NLS-1$
291                     if (nameNode != null) {
292                         String name = nameNode.getNodeValue();
293 
294                         Node parentNode = node.getAttributes().getNamedItem("parent"); //$NON-NLS-1$
295                         String parents = parentNode == null ? null : parentNode.getNodeValue();
296 
297                         if (name != null && !mStyleMap.containsKey(name)) {
298                             DeclareStyleableInfo style = parseDeclaredStyleable(name, node);
299                             if (parents != null) {
300                                 String[] parentsArray =
301                                     parseStyleableParents(parents, mStyleMap, unknownParents);
302                                 style.setParents(parentsArray);
303                             }
304                             mStyleMap.put(name, style);
305                             unknownParents.remove(name);
306                             if (lastComment != null) {
307                                 String nodeValue = lastComment.getNodeValue();
308                                 if (nodeValue.contains(DOC_HIDE)) {
309                                     mStyleMap.remove(name);
310                                 } else {
311                                     style.setJavaDoc(parseJavadoc(nodeValue));
312                                 }
313                             }
314                         }
315                     }
316                 } else if (node.getNodeName().equals("attr")) {                //$NON-NLS-1$
317                     parseAttr(node, lastComment);
318                 }
319                 lastComment = null;
320                 break;
321             }
322         }
323 
324         // If we have any unknown parent, re-create synthetic styleable for them.
325         for (Entry<String, String> entry : unknownParents.entrySet()) {
326             String name = entry.getKey();
327             String parent = entry.getValue();
328 
329             DeclareStyleableInfo style = new DeclareStyleableInfo(name, (AttributeInfo[])null);
330             if (parent != null) {
331                 style.setParents(new String[] { parent });
332             }
333             mStyleMap.put(name, style);
334 
335             // Simplify parents names. See SDK Bug 3125910.
336             // Implementation detail: that since we want to delete and add to the map,
337             // we can't just use an iterator.
338             for (String key : new ArrayList<String>(mStyleMap.keySet())) {
339                 if (key.startsWith(name) && !key.equals(name)) {
340                     // We found a child which name starts with the full name of the
341                     // parent. Simplify the children name.
342                     String newName = ANDROID_MANIFEST_STYLEABLE + key.substring(name.length());
343 
344                     DeclareStyleableInfo newStyle =
345                         new DeclareStyleableInfo(newName, mStyleMap.get(key));
346                     mStyleMap.remove(key);
347                     mStyleMap.put(newName, newStyle);
348                 }
349             }
350         }
351     }
352 
353     /**
354      * Parses the "parents" attribute from a &lt;declare-styleable&gt;.
355      * <p/>
356      * The syntax is the following:
357      * <pre>
358      *   parent[.parent]* [[space|,] parent[.parent]* ]
359      * </pre>
360      * <p/>
361      * In English: </br>
362      * - There can be one or more parents, separated by whitespace or commas. </br>
363      * - Whitespace is ignored and trimmed. </br>
364      * - A parent name is actually composed of one or more identifiers joined by a dot.
365      * <p/>
366      * Styleables do not usually need to declare their parent chain (e.g. the grand-parents
367      * of a parent.) Parent names are unique, so in most cases a styleable will only declare
368      * its immediate parent.
369      * <p/>
370      * However it is possible for a styleable's parent to not exist, e.g. if you have a
371      * styleable "A" that is the root and then styleable "C" declares its parent to be "A.B".
372      * In this case we record "B" as the parent, even though it is unknown and will never be
373      * known. Any parent that is currently not in the knownParent map is thus added to the
374      * unknownParent set. The caller will remove the name from the unknownParent set when it
375      * sees a declaration for it.
376      *
377      * @param parents The parents string to parse. Must not be null or empty.
378      * @param knownParents The map of all declared styles known so far.
379      * @param unknownParents A map of all unknown parents collected here.
380      * @return The array of terminal parent names parsed from the parents string.
381      */
parseStyleableParents(String parents, Map<String, DeclareStyleableInfo> knownParents, Map<String, String> unknownParents)382     private String[] parseStyleableParents(String parents,
383             Map<String, DeclareStyleableInfo> knownParents,
384             Map<String, String> unknownParents) {
385 
386         ArrayList<String> result = new ArrayList<String>();
387 
388         for (String parent : parents.split("[ \t\n\r\f,|]")) {          //$NON-NLS-1$
389             parent = parent.trim();
390             if (parent.length() == 0) {
391                 continue;
392             }
393             if (parent.indexOf('.') >= 0) {
394                 // This is a grand-parent/parent chain. Make sure we know about the
395                 // parents and only record the terminal one.
396                 String last = null;
397                 for (String name : parent.split("\\.")) {          //$NON-NLS-1$
398                     if (name.length() > 0) {
399                         if (!knownParents.containsKey(name)) {
400                             // Record this unknown parent and its grand parent.
401                             unknownParents.put(name, last);
402                         }
403                         last = name;
404                     }
405                 }
406                 parent = last;
407             }
408 
409             result.add(parent);
410         }
411 
412         return result.toArray(new String[result.size()]);
413     }
414 
415     /**
416      * Parses an &lt;attr&gt; node and convert it into an {@link AttributeInfo} if it is valid.
417      */
parseAttr(Node attrNode, Node lastComment)418     private AttributeInfo parseAttr(Node attrNode, Node lastComment) {
419         AttributeInfo info = null;
420         Node nameNode = attrNode.getAttributes().getNamedItem("name"); //$NON-NLS-1$
421         if (nameNode != null) {
422             String name = nameNode.getNodeValue();
423             if (name != null) {
424                 info = mAttributeMap.get(name);
425                 // If the attribute is unknown yet, parse it.
426                 // If the attribute is know but its format is unknown, parse it too.
427                 if (info == null || info.getFormats().size() == 0) {
428                     info = parseAttributeTypes(attrNode, name);
429                     if (info != null) {
430                         mAttributeMap.put(name, info);
431                     }
432                 } else if (lastComment != null) {
433                     info = new AttributeInfo(info);
434                 }
435                 if (info != null) {
436                     if (lastComment != null) {
437                         String nodeValue = lastComment.getNodeValue();
438                         if (nodeValue.contains(DOC_HIDE)) {
439                             return null;
440                         }
441                         info.setJavaDoc(parseJavadoc(nodeValue));
442                         info.setDeprecatedDoc(parseDeprecatedDoc(nodeValue));
443                     }
444                 }
445             }
446         }
447         return info;
448     }
449 
450     /**
451      * Finds all the attributes for a particular style node,
452      * e.g. a declare-styleable of name "TextView" or "LinearLayout_Layout".
453      *
454      * @param styleName The name of the declare-styleable node
455      * @param declareStyleableNode The declare-styleable node itself
456      */
parseDeclaredStyleable(String styleName, Node declareStyleableNode)457     private DeclareStyleableInfo parseDeclaredStyleable(String styleName,
458             Node declareStyleableNode) {
459         ArrayList<AttributeInfo> attrs = new ArrayList<AttributeInfo>();
460         Node lastComment = null;
461         for (Node node = declareStyleableNode.getFirstChild();
462              node != null;
463              node = node.getNextSibling()) {
464 
465             switch (node.getNodeType()) {
466             case Node.COMMENT_NODE:
467                 lastComment = node;
468                 break;
469             case Node.ELEMENT_NODE:
470                 if (node.getNodeName().equals("attr")) {                       //$NON-NLS-1$
471                     AttributeInfo info = parseAttr(node, lastComment);
472                     if (info != null) {
473                         attrs.add(info);
474                     }
475                 }
476                 lastComment = null;
477                 break;
478             }
479 
480         }
481 
482         return new DeclareStyleableInfo(styleName, attrs.toArray(new AttributeInfo[attrs.size()]));
483     }
484 
485     /**
486      * Returns the {@link AttributeInfo} for a specific <attr> XML node.
487      * This gets the javadoc, the type, the name and the enum/flag values if any.
488      * <p/>
489      * The XML node is expected to have the following attributes:
490      * <ul>
491      * <li>"name", which is mandatory. The node is skipped if this is missing.</li>
492      * <li>"format".</li>
493      * </ul>
494      * The format may be one type or two types (e.g. "reference|color").
495      * An extra format can be implied: "enum" or "flag" are not specified in the "format" attribute,
496      * they are implicitly stated by the presence of sub-nodes <enum> or <flag>.
497      * <p/>
498      * By design, attr nodes of the same name MUST have the same type.
499      * Attribute nodes are thus cached by name and reused as much as possible.
500      * When reusing a node, it is duplicated and its javadoc reassigned.
501      */
parseAttributeTypes(Node attrNode, String name)502     private AttributeInfo parseAttributeTypes(Node attrNode, String name) {
503         EnumSet<Format> formats = null;
504         String[] enumValues = null;
505         String[] flagValues = null;
506 
507         Node attrFormat = attrNode.getAttributes().getNamedItem("format"); //$NON-NLS-1$
508         if (attrFormat != null) {
509             for (String f : attrFormat.getNodeValue().split("\\|")) { //$NON-NLS-1$
510                 Format format = mFormatNames.get(f);
511                 if (format == null) {
512                     mLog.info(
513                         "Unknown format name '%s' in <attr name=\"%s\">, file '%s'.", //$NON-NLS-1$
514                         f, name, getOsAttrsXmlPath());
515                 } else if (format != AttributeInfo.Format.ENUM &&
516                         format != AttributeInfo.Format.FLAG) {
517                     if (formats == null) {
518                         formats = format.asSet();
519                     } else {
520                         if (formats.size() == 1) {
521                             formats = EnumSet.copyOf(formats);
522                         }
523                         formats.add(format);
524                     }
525                 }
526             }
527         }
528 
529         // does this <attr> have <enum> children?
530         enumValues = parseEnumFlagValues(attrNode, "enum", name); //$NON-NLS-1$
531         if (enumValues != null) {
532             if (formats == null) {
533                 formats = Format.ENUM_SET;
534             } else {
535                 if (formats.size() == 1) {
536                     formats = EnumSet.copyOf(formats);
537                 }
538                 formats.add(Format.ENUM);
539             }
540         }
541 
542         // does this <attr> have <flag> children?
543         flagValues = parseEnumFlagValues(attrNode, "flag", name); //$NON-NLS-1$
544         if (flagValues != null) {
545             if (formats == null) {
546                 formats = Format.FLAG_SET;
547             } else {
548                 if (formats.size() == 1) {
549                     formats = EnumSet.copyOf(formats);
550                 }
551                 formats.add(Format.FLAG);
552             }
553         }
554 
555         if (formats == null) {
556             formats = Format.NONE;
557         }
558 
559         AttributeInfo info = new AttributeInfo(name, formats);
560         info.setEnumValues(enumValues);
561         info.setFlagValues(flagValues);
562         return info;
563     }
564 
565     /**
566      * Given an XML node that represents an <attr> node, this method searches
567      * if the node has any children nodes named "target" (e.g. "enum" or "flag").
568      * Such nodes must have a "name" attribute.
569      * <p/>
570      * If "attrNode" is null, look for any <attr> that has the given attrNode
571      * and the requested children nodes.
572      * <p/>
573      * This method collects all the possible names of these children nodes and
574      * return them.
575      *
576      * @param attrNode The <attr> XML node
577      * @param filter The child node to look for, either "enum" or "flag".
578      * @param attrName The value of the name attribute of <attr>
579      *
580      * @return Null if there are no such children nodes, otherwise an array of length >= 1
581      *         of all the names of these children nodes.
582      */
parseEnumFlagValues(Node attrNode, String filter, String attrName)583     private String[] parseEnumFlagValues(Node attrNode, String filter, String attrName) {
584         ArrayList<String> names = null;
585         for (Node child = attrNode.getFirstChild(); child != null; child = child.getNextSibling()) {
586             if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(filter)) {
587                 Node nameNode = child.getAttributes().getNamedItem("name");  //$NON-NLS-1$
588                 if (nameNode == null) {
589                     mLog.warning(
590                             "Missing name attribute in <attr name=\"%s\"><%s></attr>", //$NON-NLS-1$
591                             attrName, filter);
592                 } else {
593                     if (names == null) {
594                         names = new ArrayList<String>();
595                     }
596                     String name = nameNode.getNodeValue();
597                     names.add(name);
598 
599                     Node valueNode = child.getAttributes().getNamedItem("value");  //$NON-NLS-1$
600                     if (valueNode == null) {
601                         mLog.warning(
602                             "Missing value attribute in <attr name=\"%s\"><%s name=\"%s\"></attr>", //$NON-NLS-1$
603                             attrName, filter, name);
604                     } else {
605                         String value = valueNode.getNodeValue();
606                         try {
607                             // Integer.decode cannot handle "ffffffff", see JDK issue 6624867
608                             int i = (int) (long) Long.decode(value);
609 
610                             Map<String, Integer> map = mEnumFlagValues.get(attrName);
611                             if (map == null) {
612                                 map = new HashMap<String, Integer>();
613                                 mEnumFlagValues.put(attrName, map);
614                             }
615                             map.put(name, Integer.valueOf(i));
616 
617                         } catch(NumberFormatException e) {
618                             mLog.error(e,
619                                     "Value in <attr name=\"%s\"><%s name=\"%s\" value=\"%s\"></attr> is not a valid decimal or hexadecimal", //$NON-NLS-1$
620                                     attrName, filter, name, value);
621                         }
622                     }
623                 }
624             }
625         }
626         return names == null ? null : names.toArray(new String[names.size()]);
627     }
628 
629     /**
630      * Parses the javadoc comment.
631      * Only keeps the first sentence.
632      * <p/>
633      * This does not remove nor simplify links and references.
634      */
parseJavadoc(String comment)635     private String parseJavadoc(String comment) {
636         if (comment == null) {
637             return null;
638         }
639 
640         // sanitize & collapse whitespace
641         comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
642 
643         // Explicitly remove any @deprecated tags since they are handled separately.
644         comment = comment.replaceAll("(?:\\{@deprecated[^}]*\\}|@deprecated[^@}]*)", "");
645 
646         // take everything up to the first dot that is followed by a space or the end of the line.
647         // I love regexps :-). For the curious, the regexp is:
648         // - start of line
649         // - ignore whitespace
650         // - group:
651         //   - everything, not greedy
652         //   - non-capturing group (?: )
653         //      - end of string
654         //      or
655         //      - not preceded by a letter, a dot and another letter (for "i.e" and "e.g" )
656         //                            (<! non-capturing zero-width negative look-behind)
657         //      - a dot
658         //      - followed by a space (?= non-capturing zero-width positive look-ahead)
659         // - anything else is ignored
660         comment = comment.replaceFirst("^\\s*(.*?(?:$|(?<![a-zA-Z]\\.[a-zA-Z])\\.(?=\\s))).*", "$1"); //$NON-NLS-1$ //$NON-NLS-2$
661 
662         return comment;
663     }
664 
665 
666     /**
667      * Parses the javadoc and extract the first @deprecated tag, if any.
668      * Returns null if there's no @deprecated tag.
669      * The deprecated tag can be of two forms:
670      * - {+@deprecated ...text till the next bracket }
671      *   Note: there should be no space or + between { and @. I need one in this comment otherwise
672      *   this method will be tagged as deprecated ;-)
673      * - @deprecated ...text till the next @tag or end of the comment.
674      * In both cases the comment can be multi-line.
675      */
parseDeprecatedDoc(String comment)676     private String parseDeprecatedDoc(String comment) {
677         // Skip if we can't even find the tag in the comment.
678         if (comment == null) {
679             return null;
680         }
681 
682         // sanitize & collapse whitespace
683         comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
684 
685         int pos = comment.indexOf("{@deprecated");
686         if (pos >= 0) {
687             comment = comment.substring(pos + 12 /* len of {@deprecated */);
688             comment = comment.replaceFirst("^([^}]*).*", "$1");
689         } else if ((pos = comment.indexOf("@deprecated")) >= 0) {
690             comment = comment.substring(pos + 11 /* len of @deprecated */);
691             comment = comment.replaceFirst("^(.*?)(?:@.*|$)", "$1");
692         } else {
693             return null;
694         }
695 
696         return comment.trim();
697     }
698 }
699