• 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.util.Arrays;
13 import java.util.List;
14 import org.robolectric.res.AttributeResource;
15 import org.robolectric.res.ResName;
16 import org.robolectric.res.ResourceTable;
17 import org.robolectric.res.StringResources;
18 import org.w3c.dom.Document;
19 import org.w3c.dom.Element;
20 import org.w3c.dom.NamedNodeMap;
21 import org.w3c.dom.Node;
22 import org.xmlpull.v1.XmlPullParserException;
23 
24 /**
25  * Concrete implementation of the {@link XmlResourceParser}.
26  *
27  * Clients expects a pull parser while the resource loader
28  * initialise this object with a {@link Document}.
29  * This implementation navigates the dom and emulates a pull
30  * parser by raising all the opportune events.
31  *
32  * Note that the original android implementation is based on
33  * a set of native methods calls. Here those methods are
34  * re-implemented in java when possible.
35  */
36 public class XmlResourceParserImpl implements XmlResourceParser {
37 
38   /**
39    * All the parser features currently supported by Android.
40    */
41   public static final String[] AVAILABLE_FEATURES = {
42       XmlResourceParser.FEATURE_PROCESS_NAMESPACES,
43       XmlResourceParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES
44   };
45   /**
46    * All the parser features currently NOT supported by Android.
47    */
48   public static final String[] UNAVAILABLE_FEATURES = {
49       XmlResourceParser.FEATURE_PROCESS_DOCDECL,
50       XmlResourceParser.FEATURE_VALIDATION
51   };
52 
53   private final Document document;
54   private final String fileName;
55   private final String packageName;
56   private final ResourceTable resourceTable;
57   private final String applicationNamespace;
58 
59   private Node currentNode;
60 
61   private boolean mStarted = false;
62   private boolean mDecNextDepth = false;
63   private int mDepth = 0;
64   private int mEventType = START_DOCUMENT;
65 
XmlResourceParserImpl(Document document, String fileName, String packageName, String applicationPackageName, ResourceTable resourceTable)66   public XmlResourceParserImpl(Document document, String fileName, String packageName,
67                                String applicationPackageName, ResourceTable resourceTable) {
68     this.document = document;
69     this.fileName = fileName;
70     this.packageName = packageName;
71     this.resourceTable = resourceTable;
72     this.applicationNamespace = ANDROID_RES_NS_PREFIX + applicationPackageName;
73   }
74 
75   @Override
setFeature(String name, boolean state)76   public void setFeature(String name, boolean state)
77       throws XmlPullParserException {
78     if (isAndroidSupportedFeature(name) && state) {
79       return;
80     }
81     throw new XmlPullParserException("Unsupported feature: " + name);
82   }
83 
84   @Override
getFeature(String name)85   public boolean getFeature(String name) {
86     return isAndroidSupportedFeature(name);
87   }
88 
89   @Override
setProperty(String name, Object value)90   public void setProperty(String name, Object value)
91       throws XmlPullParserException {
92     throw new XmlPullParserException("setProperty() not supported");
93   }
94 
95   @Override
getProperty(String name)96   public Object getProperty(String name) {
97     // Properties are not supported. Android returns null
98     // instead of throwing an XmlPullParserException.
99     return null;
100   }
101 
102   @Override
setInput(Reader in)103   public void setInput(Reader in) throws XmlPullParserException {
104     throw new XmlPullParserException("setInput() not supported");
105   }
106 
107   @Override
setInput(InputStream inputStream, String inputEncoding)108   public void setInput(InputStream inputStream, String inputEncoding)
109       throws XmlPullParserException {
110     throw new XmlPullParserException("setInput() not supported");
111   }
112 
113   @Override
defineEntityReplacementText( String entityName, String replacementText)114   public void defineEntityReplacementText(
115       String entityName, String replacementText)
116       throws XmlPullParserException {
117     throw new XmlPullParserException(
118         "defineEntityReplacementText() not supported");
119   }
120 
121   @Override
getNamespacePrefix(int pos)122   public String getNamespacePrefix(int pos)
123       throws XmlPullParserException {
124     throw new XmlPullParserException(
125         "getNamespacePrefix() not supported");
126   }
127 
128   @Override
getInputEncoding()129   public String getInputEncoding() {
130     return null;
131   }
132 
133   @Override
getNamespace(String prefix)134   public String getNamespace(String prefix) {
135     throw new RuntimeException(
136         "getNamespaceCount() not supported");
137   }
138 
139   @Override
getNamespaceCount(int depth)140   public int getNamespaceCount(int depth)
141       throws XmlPullParserException {
142     throw new XmlPullParserException(
143         "getNamespaceCount() not supported");
144   }
145 
146   @Override
getPositionDescription()147   public String getPositionDescription() {
148     return "XML file " + fileName + " line #" + getLineNumber() + " (sorry, not yet implemented)";
149   }
150 
151   @Override
getNamespaceUri(int pos)152   public String getNamespaceUri(int pos)
153       throws XmlPullParserException {
154     throw new XmlPullParserException(
155         "getNamespaceUri() not supported");
156   }
157 
158   @Override
getColumnNumber()159   public int getColumnNumber() {
160     // Android always returns -1
161     return -1;
162   }
163 
164   @Override
getDepth()165   public int getDepth() {
166     return mDepth;
167   }
168 
169   @Override
getText()170   public String getText() {
171     if (currentNode == null) {
172       return "";
173     }
174     return StringResources.processStringResources(currentNode.getTextContent());
175   }
176 
177   @Override
getLineNumber()178   public int getLineNumber() {
179     // TODO(msama): The current implementation is
180     //   unable to return line numbers.
181     return -1;
182   }
183 
184   @Override
getEventType()185   public int getEventType()
186       throws XmlPullParserException {
187     return mEventType;
188   }
189 
190   /*package*/
isWhitespace(String text)191   public boolean isWhitespace(String text)
192       throws XmlPullParserException {
193     if (text == null) {
194       return false;
195     }
196     return text.split("\\s").length == 0;
197   }
198 
199   @Override
isWhitespace()200   public boolean isWhitespace()
201       throws XmlPullParserException {
202     // Note: in android whitespaces are automatically stripped.
203     // Here we have to skip them manually
204     return isWhitespace(getText());
205   }
206 
207   @Override
getPrefix()208   public String getPrefix() {
209     throw new RuntimeException("getPrefix not supported");
210   }
211 
212   @Override
getTextCharacters(int[] holderForStartAndLength)213   public char[] getTextCharacters(int[] holderForStartAndLength) {
214     String txt = getText();
215     char[] chars = null;
216     if (txt != null) {
217       holderForStartAndLength[0] = 0;
218       holderForStartAndLength[1] = txt.length();
219       chars = new char[txt.length()];
220       txt.getChars(0, txt.length(), chars, 0);
221     }
222     return chars;
223   }
224 
225   @Override
getNamespace()226   public String getNamespace() {
227     String namespace = currentNode != null ? currentNode.getNamespaceURI() : null;
228     if (namespace == null) {
229       return "";
230     }
231 
232     return maybeReplaceNamespace(namespace);
233   }
234 
235   @Override
getName()236   public String getName() {
237     if (currentNode == null) {
238       return null;
239     }
240     return currentNode.getNodeName();
241   }
242 
getAttributeAt(int index)243   Node getAttributeAt(int index) {
244     if (currentNode == null) {
245       throw new IndexOutOfBoundsException(String.valueOf(index));
246     }
247     NamedNodeMap map = currentNode.getAttributes();
248     if (index >= map.getLength()) {
249       throw new IndexOutOfBoundsException(String.valueOf(index));
250     }
251     return map.item(index);
252   }
253 
getAttribute(String namespace, String name)254   public String getAttribute(String namespace, String name) {
255     if (currentNode == null) {
256       return null;
257     }
258 
259     Element element = (Element) currentNode;
260     if (element.hasAttributeNS(namespace, name)) {
261       return element.getAttributeNS(namespace, name).trim();
262     } else if (applicationNamespace.equals(namespace)
263         && element.hasAttributeNS(AttributeResource.RES_AUTO_NS_URI, name)) {
264       return element.getAttributeNS(AttributeResource.RES_AUTO_NS_URI, name).trim();
265     }
266 
267     return null;
268   }
269 
270   @Override
getAttributeNamespace(int index)271   public String getAttributeNamespace(int index) {
272     Node attr = getAttributeAt(index);
273     if (attr == null) {
274       return "";
275     }
276     return maybeReplaceNamespace(attr.getNamespaceURI());
277   }
278 
maybeReplaceNamespace(String namespace)279   private String maybeReplaceNamespace(String namespace) {
280     if (namespace == null) {
281       return "";
282     } else if (namespace.equals(applicationNamespace)) {
283       return AttributeResource.RES_AUTO_NS_URI;
284     } else {
285       return namespace;
286     }
287   }
288 
289   @Override
getAttributeName(int index)290   public String getAttributeName(int index) {
291     Node attr = getAttributeAt(index);
292     String name = attr.getLocalName();
293     return name == null ? attr.getNodeName() : name;
294   }
295 
296   @Override
getAttributePrefix(int index)297   public String getAttributePrefix(int index) {
298     throw new RuntimeException("getAttributePrefix not supported");
299   }
300 
301   @Override
isEmptyElementTag()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
getAttributeCount()309   public int getAttributeCount() {
310     if (currentNode == null) {
311       return -1;
312     }
313     return currentNode.getAttributes().getLength();
314   }
315 
316   @Override
getAttributeValue(int index)317   public String getAttributeValue(int index) {
318     return qualify(getAttributeAt(index).getNodeValue());
319   }
320 
321   // for testing only...
qualify(String value)322   public String qualify(String value) {
323     if (value == null) return null;
324     if (AttributeResource.isResourceReference(value)) {
325       return "@" + ResName.qualifyResourceName(value.trim().substring(1).replace("+", ""), packageName, "attr");
326     } else if (AttributeResource.isStyleReference(value)) {
327       return "?" + ResName.qualifyResourceName(value.trim().substring(1), packageName, "attr");
328     } else {
329       return StringResources.processStringResources(value);
330     }
331   }
332 
333   @Override
getAttributeType(int index)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
isAttributeDefault(int index)341   public boolean isAttributeDefault(int index) {
342     // The android implementation always returns false
343     return false;
344   }
345 
346   @Override
nextToken()347   public int nextToken() throws XmlPullParserException, IOException {
348     return next();
349   }
350 
351   @Override
getAttributeValue(String namespace, String name)352   public String getAttributeValue(String namespace, String name) {
353     return qualify(getAttribute(namespace, name));
354   }
355 
356   @Override
next()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    */
nativeNext()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 
processNextNodeType(Node node)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    */
navigateToNextNode(Node node)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
require(int type, String namespace, String name)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
nextText()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
nextTag()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
getAttributeNameResource(int index)594   public int getAttributeNameResource(int index) {
595     String attributeNamespace = getAttributeNamespace(index);
596     if (attributeNamespace.equals(RES_AUTO_NS_URI)) {
597       attributeNamespace = packageName;
598     } else if (attributeNamespace.startsWith(ANDROID_RES_NS_PREFIX)) {
599       attributeNamespace = attributeNamespace.substring(ANDROID_RES_NS_PREFIX.length());
600     }
601     return getResourceId(getAttributeName(index), attributeNamespace, "attr");
602   }
603 
604   @Override
getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue)605   public int getAttributeListValue(String namespace, String attribute,
606       String[] options, int defaultValue) {
607     String attr = getAttribute(namespace, attribute);
608     if (attr == null) {
609       return 0;
610     }
611     List<String> optList = Arrays.asList(options);
612     int index = optList.indexOf(attr);
613     if (index == -1) {
614       return defaultValue;
615     }
616     return index;
617   }
618 
619   @Override
getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue)620   public boolean getAttributeBooleanValue(String namespace, String attribute,
621       boolean defaultValue) {
622     String attr = getAttribute(namespace, attribute);
623     if (attr == null) {
624       return defaultValue;
625     }
626     return Boolean.parseBoolean(attr);
627   }
628 
629   @Override
getAttributeResourceValue(String namespace, String attribute, int defaultValue)630   public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) {
631     String attr = getAttribute(namespace, attribute);
632     if (attr != null && attr.startsWith("@") && !AttributeResource.isNull(attr)) {
633       return getResourceId(attr, packageName, null);
634     }
635     return defaultValue;
636   }
637 
638   @Override
getAttributeIntValue(String namespace, String attribute, int defaultValue)639   public int getAttributeIntValue(String namespace, String attribute, int defaultValue) {
640     return XmlUtils.convertValueToInt(this.getAttributeValue(namespace, attribute), defaultValue);
641   }
642 
643   @Override
getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue)644   public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) {
645     int value = getAttributeIntValue(namespace, attribute, defaultValue);
646     if (value < 0) {
647       return defaultValue;
648     }
649     return value;
650   }
651 
652   @Override
getAttributeFloatValue(String namespace, String attribute, float defaultValue)653   public float getAttributeFloatValue(String namespace, String attribute,
654       float defaultValue) {
655     String attr = getAttribute(namespace, attribute);
656     if (attr == null) {
657       return defaultValue;
658     }
659     try {
660       return Float.parseFloat(attr);
661     } catch (NumberFormatException ex) {
662       return defaultValue;
663     }
664   }
665 
666   @Override
getAttributeListValue( int idx, String[] options, int defaultValue)667   public int getAttributeListValue(
668       int idx, String[] options, int defaultValue) {
669     try {
670       String value = getAttributeValue(idx);
671       List<String> optList = Arrays.asList(options);
672       int index = optList.indexOf(value);
673       if (index == -1) {
674         return defaultValue;
675       }
676       return index;
677     } catch (IndexOutOfBoundsException ex) {
678       return defaultValue;
679     }
680   }
681 
682   @Override
getAttributeBooleanValue( int idx, boolean defaultValue)683   public boolean getAttributeBooleanValue(
684       int idx, boolean defaultValue) {
685     try {
686       return Boolean.parseBoolean(getAttributeValue(idx));
687     } catch (IndexOutOfBoundsException ex) {
688       return defaultValue;
689     }
690   }
691 
692   @Override
getAttributeResourceValue(int idx, int defaultValue)693   public int getAttributeResourceValue(int idx, int defaultValue) {
694     String attributeValue = getAttributeValue(idx);
695     if (attributeValue != null && attributeValue.startsWith("@")) {
696       int resourceId = getResourceId(attributeValue.substring(1), packageName, null);
697       if (resourceId != 0) {
698         return resourceId;
699       }
700     }
701     return defaultValue;
702   }
703 
704   @Override
getAttributeIntValue(int idx, int defaultValue)705   public int getAttributeIntValue(int idx, int defaultValue) {
706     try {
707       return Integer.parseInt(getAttributeValue(idx));
708     } catch (NumberFormatException ex) {
709       return defaultValue;
710     } catch (IndexOutOfBoundsException ex) {
711       return defaultValue;
712     }
713   }
714 
715   @Override
getAttributeUnsignedIntValue(int idx, int defaultValue)716   public int getAttributeUnsignedIntValue(int idx, int defaultValue) {
717     int value = getAttributeIntValue(idx, defaultValue);
718     if (value < 0) {
719       return defaultValue;
720     }
721     return value;
722   }
723 
724   @Override
getAttributeFloatValue(int idx, float defaultValue)725   public float getAttributeFloatValue(int idx, float defaultValue) {
726     try {
727       return Float.parseFloat(getAttributeValue(idx));
728     } catch (NumberFormatException ex) {
729       return defaultValue;
730     } catch (IndexOutOfBoundsException ex) {
731       return defaultValue;
732     }
733   }
734 
735   @Override
getIdAttribute()736   public String getIdAttribute() {
737     return getAttribute(null, "id");
738   }
739 
740   @Override
getClassAttribute()741   public String getClassAttribute() {
742     return getAttribute(null, "class");
743   }
744 
745   @Override
getIdAttributeResourceValue(int defaultValue)746   public int getIdAttributeResourceValue(int defaultValue) {
747     return getAttributeResourceValue(null, "id", defaultValue);
748   }
749 
750   @Override
getStyleAttribute()751   public int getStyleAttribute() {
752     String attr = getAttribute(null, "style");
753     if (attr == null ||
754         (!AttributeResource.isResourceReference(attr) && !AttributeResource.isStyleReference(attr))) {
755       return 0;
756     }
757 
758     int style = getResourceId(attr, packageName, "style");
759     if (style == 0) {
760       // try again with underscores...
761       style = getResourceId(attr.replace('.', '_'), packageName, "style");
762     }
763     return style;
764   }
765 
766   @Override
close()767   public void close() {
768     // Nothing to do
769   }
770 
771   @Override
finalize()772   protected void finalize() throws Throwable {
773     close();
774   }
775 
getResourceId(String possiblyQualifiedResourceName, String defaultPackageName, String defaultType)776   private int getResourceId(String possiblyQualifiedResourceName, String defaultPackageName, String defaultType) {
777 
778     if (AttributeResource.isNull(possiblyQualifiedResourceName)) return 0;
779 
780     if (AttributeResource.isStyleReference(possiblyQualifiedResourceName)) {
781       ResName styleReference = AttributeResource.getStyleReference(possiblyQualifiedResourceName, defaultPackageName, "attr");
782       Integer resourceId = resourceTable.getResourceId(styleReference);
783       if (resourceId == null) {
784         throw new Resources.NotFoundException(styleReference.getFullyQualifiedName());
785       }
786       return resourceId;
787     }
788 
789     if (AttributeResource.isResourceReference(possiblyQualifiedResourceName)) {
790       ResName resourceReference = AttributeResource.getResourceReference(possiblyQualifiedResourceName, defaultPackageName, defaultType);
791       Integer resourceId = resourceTable.getResourceId(resourceReference);
792       if (resourceId == null) {
793         throw new Resources.NotFoundException(resourceReference.getFullyQualifiedName());
794       }
795       return resourceId;
796     }
797     possiblyQualifiedResourceName = removeLeadingSpecialCharsIfAny(possiblyQualifiedResourceName);
798     ResName resName = ResName.qualifyResName(possiblyQualifiedResourceName, defaultPackageName, defaultType);
799     Integer resourceId = resourceTable.getResourceId(resName);
800     return resourceId == null ? 0 : resourceId;
801   }
802 
removeLeadingSpecialCharsIfAny(String name)803   private static String removeLeadingSpecialCharsIfAny(String name){
804     if (name.startsWith("@+")) {
805       return name.substring(2);
806     }
807     if (name.startsWith("@")) {
808       return name.substring(1);
809     }
810     return name;
811   }
812 
813   /**
814    * Tell is a given feature is supported by android.
815    *
816    * @param name Feature name.
817    * @return True if the feature is supported.
818    */
isAndroidSupportedFeature(String name)819   private static boolean isAndroidSupportedFeature(String name) {
820     if (name == null) {
821       return false;
822     }
823     for (String feature : AVAILABLE_FEATURES) {
824       if (feature.equals(name)) {
825         return true;
826       }
827     }
828     return false;
829   }
830 }
831