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