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 <declare-styleable> and <attr> nodes 262 * in the top <resources> 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 <declare-styleable>. 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 <attr> 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