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