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