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