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