Home | History | Annotate | Download | only in platform
      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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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