1package org.robolectric.android; 2 3import android.content.res.Resources; 4import android.content.res.XmlResourceParser; 5import com.android.internal.util.XmlUtils; 6import java.io.IOException; 7import java.io.InputStream; 8import java.io.Reader; 9import java.util.Arrays; 10import java.util.List; 11import org.robolectric.res.AttributeResource; 12import org.robolectric.res.ResName; 13import org.robolectric.res.ResourceTable; 14import org.w3c.dom.Document; 15import org.w3c.dom.Element; 16import org.w3c.dom.NamedNodeMap; 17import org.w3c.dom.Node; 18import org.xmlpull.v1.XmlPullParserException; 19 20/** 21 * Concrete implementation of the {@link XmlResourceParser}. 22 * 23 * Clients expects a pull parser while the resource loader 24 * initialise this object with a {@link Document}. 25 * This implementation navigates the dom and emulates a pull 26 * parser by raising all the opportune events. 27 * 28 * Note that the original android implementation is based on 29 * a set of native methods calls. Here those methods are 30 * re-implemented in java when possible. 31 */ 32public class XmlResourceParserImpl implements XmlResourceParser { 33 34 /** 35 * All the parser features currently supported by Android. 36 */ 37 public static final String[] AVAILABLE_FEATURES = { 38 XmlResourceParser.FEATURE_PROCESS_NAMESPACES, 39 XmlResourceParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES 40 }; 41 /** 42 * All the parser features currently NOT supported by Android. 43 */ 44 public static final String[] UNAVAILABLE_FEATURES = { 45 XmlResourceParser.FEATURE_PROCESS_DOCDECL, 46 XmlResourceParser.FEATURE_VALIDATION 47 }; 48 49 private final Document document; 50 private final String fileName; 51 private final String packageName; 52 private final ResourceTable resourceTable; 53 private final String applicationNamespace; 54 55 private Node currentNode; 56 57 private boolean mStarted = false; 58 private boolean mDecNextDepth = false; 59 private int mDepth = 0; 60 private int mEventType = START_DOCUMENT; 61 62 public XmlResourceParserImpl(Document document, String fileName, String packageName, 63 String applicationPackageName, ResourceTable resourceTable) { 64 this.document = document; 65 this.fileName = fileName; 66 this.packageName = packageName; 67 this.resourceTable = resourceTable; 68 this.applicationNamespace = AttributeResource.ANDROID_RES_NS_PREFIX + applicationPackageName; 69 } 70 71 @Override 72 public void setFeature(String name, boolean state) 73 throws XmlPullParserException { 74 if (isAndroidSupportedFeature(name) && state) { 75 return; 76 } 77 throw new XmlPullParserException("Unsupported feature: " + name); 78 } 79 80 @Override 81 public boolean getFeature(String name) { 82 return isAndroidSupportedFeature(name); 83 } 84 85 @Override 86 public void setProperty(String name, Object value) 87 throws XmlPullParserException { 88 throw new XmlPullParserException("setProperty() not supported"); 89 } 90 91 @Override 92 public Object getProperty(String name) { 93 // Properties are not supported. Android returns null 94 // instead of throwing an XmlPullParserException. 95 return null; 96 } 97 98 @Override 99 public void setInput(Reader in) throws XmlPullParserException { 100 throw new XmlPullParserException("setInput() not supported"); 101 } 102 103 @Override 104 public void setInput(InputStream inputStream, String inputEncoding) 105 throws XmlPullParserException { 106 throw new XmlPullParserException("setInput() not supported"); 107 } 108 109 @Override 110 public void defineEntityReplacementText( 111 String entityName, String replacementText) 112 throws XmlPullParserException { 113 throw new XmlPullParserException( 114 "defineEntityReplacementText() not supported"); 115 } 116 117 @Override 118 public String getNamespacePrefix(int pos) 119 throws XmlPullParserException { 120 throw new XmlPullParserException( 121 "getNamespacePrefix() not supported"); 122 } 123 124 @Override 125 public String getInputEncoding() { 126 return null; 127 } 128 129 @Override 130 public String getNamespace(String prefix) { 131 throw new RuntimeException( 132 "getNamespaceCount() not supported"); 133 } 134 135 @Override 136 public int getNamespaceCount(int depth) 137 throws XmlPullParserException { 138 throw new XmlPullParserException( 139 "getNamespaceCount() not supported"); 140 } 141 142 @Override 143 public String getPositionDescription() { 144 return "XML file " + fileName + " line #" + getLineNumber() + " (sorry, not yet implemented)"; 145 } 146 147 @Override 148 public String getNamespaceUri(int pos) 149 throws XmlPullParserException { 150 throw new XmlPullParserException( 151 "getNamespaceUri() not supported"); 152 } 153 154 @Override 155 public int getColumnNumber() { 156 // Android always returns -1 157 return -1; 158 } 159 160 @Override 161 public int getDepth() { 162 return mDepth; 163 } 164 165 @Override 166 public String getText() { 167 if (currentNode == null) { 168 return ""; 169 } 170 return currentNode.getTextContent(); 171 } 172 173 @Override 174 public int getLineNumber() { 175 // TODO(msama): The current implementation is 176 // unable to return line numbers. 177 return -1; 178 } 179 180 @Override 181 public int getEventType() 182 throws XmlPullParserException { 183 return mEventType; 184 } 185 186 /*package*/ 187 public boolean isWhitespace(String text) 188 throws XmlPullParserException { 189 if (text == null) { 190 return false; 191 } 192 return text.split("\\s").length == 0; 193 } 194 195 @Override 196 public boolean isWhitespace() 197 throws XmlPullParserException { 198 // Note: in android whitespaces are automatically stripped. 199 // Here we have to skip them manually 200 return isWhitespace(getText()); 201 } 202 203 @Override 204 public String getPrefix() { 205 throw new RuntimeException("getPrefix not supported"); 206 } 207 208 @Override 209 public char[] getTextCharacters(int[] holderForStartAndLength) { 210 String txt = getText(); 211 char[] chars = null; 212 if (txt != null) { 213 holderForStartAndLength[0] = 0; 214 holderForStartAndLength[1] = txt.length(); 215 chars = new char[txt.length()]; 216 txt.getChars(0, txt.length(), chars, 0); 217 } 218 return chars; 219 } 220 221 @Override 222 public String getNamespace() { 223 String namespace = currentNode != null ? currentNode.getNamespaceURI() : null; 224 if (namespace == null) { 225 return ""; 226 } 227 228 return maybeReplaceNamespace(namespace); 229 } 230 231 @Override 232 public String getName() { 233 if (currentNode == null) { 234 return ""; 235 } 236 return currentNode.getNodeName(); 237 } 238 239 Node getAttributeAt(int index) { 240 if (currentNode == null) { 241 throw new IndexOutOfBoundsException(String.valueOf(index)); 242 } 243 NamedNodeMap map = currentNode.getAttributes(); 244 if (index >= map.getLength()) { 245 throw new IndexOutOfBoundsException(String.valueOf(index)); 246 } 247 return map.item(index); 248 } 249 250 public String getAttribute(String namespace, String name) { 251 if (currentNode == null) { 252 return null; 253 } 254 255 Element element = (Element) currentNode; 256 if (element.hasAttributeNS(namespace, name)) { 257 return element.getAttributeNS(namespace, name).trim(); 258 } else if (applicationNamespace.equals(namespace) 259 && element.hasAttributeNS(AttributeResource.RES_AUTO_NS_URI, name)) { 260 return element.getAttributeNS(AttributeResource.RES_AUTO_NS_URI, name).trim(); 261 } 262 263 return null; 264 } 265 266 @Override 267 public String getAttributeNamespace(int index) { 268 Node attr = getAttributeAt(index); 269 if (attr == null) { 270 return null; 271 } 272 return maybeReplaceNamespace(attr.getNamespaceURI()); 273 } 274 275 private String maybeReplaceNamespace(String namespace) { 276 if (AttributeResource.RES_AUTO_NS_URI.equals(namespace)) { 277 return applicationNamespace; 278 } else { 279 return namespace; 280 } 281 } 282 283 @Override 284 public String getAttributeName(int index) { 285 try { 286 Node attr = getAttributeAt(index); 287 String namespace = maybeReplaceNamespace(attr.getNamespaceURI()); 288 return applicationNamespace.equals(namespace) ? 289 attr.getLocalName() : 290 attr.getNodeName(); 291 } catch (IndexOutOfBoundsException ex) { 292 return null; 293 } 294 } 295 296 @Override 297 public String getAttributePrefix(int index) { 298 throw new RuntimeException("getAttributePrefix not supported"); 299 } 300 301 @Override 302 public boolean isEmptyElementTag() throws XmlPullParserException { 303 // In Android this method is left unimplemented. 304 // This implementation is mirroring that. 305 return false; 306 } 307 308 @Override 309 public int getAttributeCount() { 310 if (currentNode == null) { 311 return -1; 312 } 313 return currentNode.getAttributes().getLength(); 314 } 315 316 @Override 317 public String getAttributeValue(int index) { 318 return qualify(getAttributeAt(index).getNodeValue()); 319 } 320 321 // for testing only... 322 public String qualify(String value) { 323 if (value == null) return null; 324 if (AttributeResource.isResourceReference(value)) { 325 return "@" + ResName.qualifyResourceName(value.substring(1).replace("+", ""), packageName, "attr"); 326 } else if (AttributeResource.isStyleReference(value)) { 327 return "?" + ResName.qualifyResourceName(value.substring(1), packageName, "attr"); 328 } else { 329 return value; 330 } 331 } 332 333 @Override 334 public String getAttributeType(int index) { 335 // Android always returns CDATA even if the 336 // node has no attribute. 337 return "CDATA"; 338 } 339 340 @Override 341 public boolean isAttributeDefault(int index) { 342 // The android implementation always returns false 343 return false; 344 } 345 346 @Override 347 public int nextToken() throws XmlPullParserException, IOException { 348 return next(); 349 } 350 351 @Override 352 public String getAttributeValue(String namespace, String name) { 353 return qualify(getAttribute(namespace, name)); 354 } 355 356 @Override 357 public int next() throws XmlPullParserException, IOException { 358 if (!mStarted) { 359 mStarted = true; 360 return START_DOCUMENT; 361 } 362 if (mEventType == END_DOCUMENT) { 363 return END_DOCUMENT; 364 } 365 int ev = nativeNext(); 366 if (mDecNextDepth) { 367 mDepth--; 368 mDecNextDepth = false; 369 } 370 switch (ev) { 371 case START_TAG: 372 mDepth++; 373 break; 374 case END_TAG: 375 mDecNextDepth = true; 376 break; 377 } 378 mEventType = ev; 379 if (ev == END_DOCUMENT) { 380 // Automatically close the parse when we reach the end of 381 // a document, since the standard XmlPullParser interface 382 // doesn't have such an API so most clients will leave us 383 // dangling. 384 close(); 385 } 386 return ev; 387 } 388 389 /** 390 * A twin implementation of the native android nativeNext(status) 391 * 392 * @throws XmlPullParserException 393 */ 394 private int nativeNext() throws XmlPullParserException { 395 switch (mEventType) { 396 case (CDSECT): { 397 throw new IllegalArgumentException( 398 "CDSECT is not handled by Android"); 399 } 400 case (COMMENT): { 401 throw new IllegalArgumentException( 402 "COMMENT is not handled by Android"); 403 } 404 case (DOCDECL): { 405 throw new IllegalArgumentException( 406 "DOCDECL is not handled by Android"); 407 } 408 case (ENTITY_REF): { 409 throw new IllegalArgumentException( 410 "ENTITY_REF is not handled by Android"); 411 } 412 case (END_DOCUMENT): { 413 // The end document event should have been filtered 414 // from the invoker. This should never happen. 415 throw new IllegalArgumentException( 416 "END_DOCUMENT should not be found here."); 417 } 418 case (END_TAG): { 419 return navigateToNextNode(currentNode); 420 } 421 case (IGNORABLE_WHITESPACE): { 422 throw new IllegalArgumentException( 423 "IGNORABLE_WHITESPACE"); 424 } 425 case (PROCESSING_INSTRUCTION): { 426 throw new IllegalArgumentException( 427 "PROCESSING_INSTRUCTION"); 428 } 429 case (START_DOCUMENT): { 430 currentNode = document.getDocumentElement(); 431 return START_TAG; 432 } 433 case (START_TAG): { 434 if (currentNode.hasChildNodes()) { 435 // The node has children, navigate down 436 return processNextNodeType( 437 currentNode.getFirstChild()); 438 } else { 439 // The node has no children 440 return END_TAG; 441 } 442 } 443 case (TEXT): { 444 return navigateToNextNode(currentNode); 445 } 446 default: { 447 // This can only happen if mEventType is 448 // assigned with an unmapped integer. 449 throw new RuntimeException( 450 "Robolectric-> Uknown XML event type: " + mEventType); 451 } 452 } 453 454 } 455 456 /*protected*/ int processNextNodeType(Node node) 457 throws XmlPullParserException { 458 switch (node.getNodeType()) { 459 case (Node.ATTRIBUTE_NODE): { 460 throw new IllegalArgumentException("ATTRIBUTE_NODE"); 461 } 462 case (Node.CDATA_SECTION_NODE): { 463 return navigateToNextNode(node); 464 } 465 case (Node.COMMENT_NODE): { 466 return navigateToNextNode(node); 467 } 468 case (Node.DOCUMENT_FRAGMENT_NODE): { 469 throw new IllegalArgumentException("DOCUMENT_FRAGMENT_NODE"); 470 } 471 case (Node.DOCUMENT_NODE): { 472 throw new IllegalArgumentException("DOCUMENT_NODE"); 473 } 474 case (Node.DOCUMENT_TYPE_NODE): { 475 throw new IllegalArgumentException("DOCUMENT_TYPE_NODE"); 476 } 477 case (Node.ELEMENT_NODE): { 478 currentNode = node; 479 return START_TAG; 480 } 481 case (Node.ENTITY_NODE): { 482 throw new IllegalArgumentException("ENTITY_NODE"); 483 } 484 case (Node.ENTITY_REFERENCE_NODE): { 485 throw new IllegalArgumentException("ENTITY_REFERENCE_NODE"); 486 } 487 case (Node.NOTATION_NODE): { 488 throw new IllegalArgumentException("DOCUMENT_TYPE_NODE"); 489 } 490 case (Node.PROCESSING_INSTRUCTION_NODE): { 491 throw new IllegalArgumentException("DOCUMENT_TYPE_NODE"); 492 } 493 case (Node.TEXT_NODE): { 494 if (isWhitespace(node.getNodeValue())) { 495 // Skip whitespaces 496 return navigateToNextNode(node); 497 } else { 498 currentNode = node; 499 return TEXT; 500 } 501 } 502 default: { 503 throw new RuntimeException( 504 "Robolectric -> Unknown node type: " + 505 node.getNodeType() + "."); 506 } 507 } 508 } 509 510 /** 511 * Navigate to the next node after a node and all of his 512 * children have been explored. 513 * 514 * If the node has unexplored siblings navigate to the 515 * next sibling. Otherwise return to its parent. 516 * 517 * @param node the node which was just explored. 518 * @return {@link XmlPullParserException#START_TAG} if the given 519 * node has siblings, {@link XmlPullParserException#END_TAG} 520 * if the node has no unexplored siblings or 521 * {@link XmlPullParserException#END_DOCUMENT} if the explored 522 * was the root document. 523 * @throws XmlPullParserException if the parser fails to 524 * parse the next node. 525 */ 526 int navigateToNextNode(Node node) 527 throws XmlPullParserException { 528 Node nextNode = node.getNextSibling(); 529 if (nextNode != null) { 530 // Move to the next siblings 531 return processNextNodeType(nextNode); 532 } else { 533 // Goes back to the parent 534 if (document.getDocumentElement().equals(node)) { 535 currentNode = null; 536 return END_DOCUMENT; 537 } 538 currentNode = node.getParentNode(); 539 return END_TAG; 540 } 541 } 542 543 @Override 544 public void require(int type, String namespace, String name) 545 throws XmlPullParserException, IOException { 546 if (type != getEventType() 547 || (namespace != null && !namespace.equals(getNamespace())) 548 || (name != null && !name.equals(getName()))) { 549 throw new XmlPullParserException( 550 "expected " + TYPES[type] + getPositionDescription()); 551 } 552 } 553 554 @Override 555 public String nextText() throws XmlPullParserException, IOException { 556 if (getEventType() != START_TAG) { 557 throw new XmlPullParserException( 558 getPositionDescription() 559 + ": parser must be on START_TAG to read next text", this, null); 560 } 561 int eventType = next(); 562 if (eventType == TEXT) { 563 String result = getText(); 564 eventType = next(); 565 if (eventType != END_TAG) { 566 throw new XmlPullParserException( 567 getPositionDescription() 568 + ": event TEXT it must be immediately followed by END_TAG", this, null); 569 } 570 return result; 571 } else if (eventType == END_TAG) { 572 return ""; 573 } else { 574 throw new XmlPullParserException( 575 getPositionDescription() 576 + ": parser must be on START_TAG or TEXT to read text", this, null); 577 } 578 } 579 580 @Override 581 public int nextTag() throws XmlPullParserException, IOException { 582 int eventType = next(); 583 if (eventType == TEXT && isWhitespace()) { // skip whitespace 584 eventType = next(); 585 } 586 if (eventType != START_TAG && eventType != END_TAG) { 587 throw new XmlPullParserException( 588 "Expected start or end tag. Found: " + eventType, this, null); 589 } 590 return eventType; 591 } 592 593 @Override 594 public int getAttributeNameResource(int index) { 595 return getResourceId(getAttributeName(index), packageName, "attr"); 596 } 597 598 @Override 599 public int getAttributeListValue(String namespace, String attribute, 600 String[] options, int defaultValue) { 601 String attr = getAttribute(namespace, attribute); 602 if (attr == null) { 603 return 0; 604 } 605 List<String> optList = Arrays.asList(options); 606 int index = optList.indexOf(attr); 607 if (index == -1) { 608 return defaultValue; 609 } 610 return index; 611 } 612 613 @Override 614 public boolean getAttributeBooleanValue(String namespace, String attribute, 615 boolean defaultValue) { 616 String attr = getAttribute(namespace, attribute); 617 if (attr == null) { 618 return defaultValue; 619 } 620 return Boolean.parseBoolean(attr); 621 } 622 623 @Override 624 public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { 625 String attr = getAttribute(namespace, attribute); 626 if (attr != null && attr.startsWith("@") && !AttributeResource.isNull(attr)) { 627 return getResourceId(attr, packageName, null); 628 } 629 return defaultValue; 630 } 631 632 @Override 633 public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { 634 return XmlUtils.convertValueToInt(this.getAttributeValue(namespace, attribute), defaultValue); 635 } 636 637 @Override 638 public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { 639 int value = getAttributeIntValue(namespace, attribute, defaultValue); 640 if (value < 0) { 641 return defaultValue; 642 } 643 return value; 644 } 645 646 @Override 647 public float getAttributeFloatValue(String namespace, String attribute, 648 float defaultValue) { 649 String attr = getAttribute(namespace, attribute); 650 if (attr == null) { 651 return defaultValue; 652 } 653 try { 654 return Float.parseFloat(attr); 655 } catch (NumberFormatException ex) { 656 return defaultValue; 657 } 658 } 659 660 @Override 661 public int getAttributeListValue( 662 int idx, String[] options, int defaultValue) { 663 try { 664 String value = getAttributeValue(idx); 665 List<String> optList = Arrays.asList(options); 666 int index = optList.indexOf(value); 667 if (index == -1) { 668 return defaultValue; 669 } 670 return index; 671 } catch (IndexOutOfBoundsException ex) { 672 return defaultValue; 673 } 674 } 675 676 @Override 677 public boolean getAttributeBooleanValue( 678 int idx, boolean defaultValue) { 679 try { 680 return Boolean.parseBoolean(getAttributeValue(idx)); 681 } catch (IndexOutOfBoundsException ex) { 682 return defaultValue; 683 } 684 } 685 686 @Override 687 public int getAttributeResourceValue(int idx, int defaultValue) { 688 String attributeValue = getAttributeValue(idx); 689 if (attributeValue != null && attributeValue.startsWith("@")) { 690 int resourceId = getResourceId(attributeValue.substring(1), packageName, null); 691 if (resourceId != 0) { 692 return resourceId; 693 } 694 } 695 return defaultValue; 696 } 697 698 @Override 699 public int getAttributeIntValue(int idx, int defaultValue) { 700 try { 701 return Integer.parseInt(getAttributeValue(idx)); 702 } catch (NumberFormatException ex) { 703 return defaultValue; 704 } catch (IndexOutOfBoundsException ex) { 705 return defaultValue; 706 } 707 } 708 709 @Override 710 public int getAttributeUnsignedIntValue(int idx, int defaultValue) { 711 int value = getAttributeIntValue(idx, defaultValue); 712 if (value < 0) { 713 return defaultValue; 714 } 715 return value; 716 } 717 718 @Override 719 public float getAttributeFloatValue(int idx, float defaultValue) { 720 try { 721 return Float.parseFloat(getAttributeValue(idx)); 722 } catch (NumberFormatException ex) { 723 return defaultValue; 724 } catch (IndexOutOfBoundsException ex) { 725 return defaultValue; 726 } 727 } 728 729 @Override 730 public String getIdAttribute() { 731 return getAttribute(null, "id"); 732 } 733 734 @Override 735 public String getClassAttribute() { 736 return getAttribute(null, "class"); 737 } 738 739 @Override 740 public int getIdAttributeResourceValue(int defaultValue) { 741 return getAttributeResourceValue(null, "id", defaultValue); 742 } 743 744 @Override 745 public int getStyleAttribute() { 746 String attr = getAttribute(null, "style"); 747 if (attr == null || 748 (!AttributeResource.isResourceReference(attr) && !AttributeResource.isStyleReference(attr))) { 749 return 0; 750 } 751 752 return getResourceId(attr, packageName, "style"); 753 } 754 755 @Override 756 public void close() { 757 // Nothing to do 758 } 759 760 @Override 761 protected void finalize() throws Throwable { 762 close(); 763 } 764 765 private int getResourceId(String possiblyQualifiedResourceName, String defaultPackageName, String defaultType) { 766 767 if (AttributeResource.isNull(possiblyQualifiedResourceName)) return 0; 768 769 if (AttributeResource.isStyleReference(possiblyQualifiedResourceName)) { 770 ResName styleReference = AttributeResource.getStyleReference(possiblyQualifiedResourceName, defaultPackageName, "attr"); 771 Integer resourceId = resourceTable.getResourceId(styleReference); 772 if (resourceId == null) { 773 throw new Resources.NotFoundException(styleReference.getFullyQualifiedName()); 774 } 775 return resourceId; 776 } 777 778 if (AttributeResource.isResourceReference(possiblyQualifiedResourceName)) { 779 ResName resourceReference = AttributeResource.getResourceReference(possiblyQualifiedResourceName, defaultPackageName, defaultType); 780 Integer resourceId = resourceTable.getResourceId(resourceReference); 781 if (resourceId == null) { 782 throw new Resources.NotFoundException(resourceReference.getFullyQualifiedName()); 783 } 784 return resourceId; 785 } 786 possiblyQualifiedResourceName = removeLeadingSpecialCharsIfAny(possiblyQualifiedResourceName); 787 ResName resName = ResName.qualifyResName(possiblyQualifiedResourceName, defaultPackageName, defaultType); 788 Integer resourceId = resourceTable.getResourceId(resName); 789 return resourceId == null ? 0 : resourceId; 790 } 791 792 private static String removeLeadingSpecialCharsIfAny(String name){ 793 if (name.startsWith("@+")) { 794 return name.substring(2); 795 } 796 if (name.startsWith("@")) { 797 return name.substring(1); 798 } 799 return name; 800 } 801 802 /** 803 * Tell is a given feature is supported by android. 804 * 805 * @param name Feature name. 806 * @return True if the feature is supported. 807 */ 808 private static boolean isAndroidSupportedFeature(String name) { 809 if (name == null) { 810 return false; 811 } 812 for (String feature : AVAILABLE_FEATURES) { 813 if (feature.equals(name)) { 814 return true; 815 } 816 } 817 return false; 818 } 819} 820