Scott
Scott

Reputation: 115

How to do a simple XML Parser with Android

Hell there, I have been stuck on this for a bit now. I know it should be simple but I can't seem to find where I went wrong. I built my little XML parser after following and trying to adapt the DOM Parser example here: http://www.ibm.com/developerworks/opensource/library/x-android/index.html I have it recognising the nodes but I, for the life of me, can't figureout why it is telling me the value of the nodes is "null". Help would be greatly appreciated.

My XML test file.

<?xml version="1.0"?>
<Person>
    <Name>Scott</Name>
    <Gender>Male</Gender>
    <More>And So On..</More>
</Person>

My Parser code is.

public class XMLParser {
    InputStream xmlDocument;
    TextView tv;

    public XMLParser(InputStream xmlDocument, TextView tv) {
        this.xmlDocument = xmlDocument;
        this.tv = tv;
    }

    public HashMap<String, String> parse() {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        HashMap<String, String> xmlItems = new HashMap<String, String>();
        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document dom = builder.parse(xmlDocument);
            Element root = dom.getDocumentElement();
            NodeList items = root.getElementsByTagName("Person");
            Element rootElement = (Element)items.item(0);
            items = rootElement.getChildNodes();
            tv.append("\nParser, # of Items: " + String.valueOf(items.getLength()));
            for (int i = 0; i < items.getLength(); i++){
                Node item = items.item(i);
                xmlItems.put(item.getNodeName(), item.getNodeValue());
                tv.append("\nNM: " + item.getNodeName() + " NV: " + item.getNodeValue());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } 
        return xmlItems;
    }
}

Upvotes: 0

Views: 2588

Answers (7)

Geekswordsman
Geekswordsman

Reputation: 1307

I'm using XmlPullFactory, and it's not so bad.

Edit for Converting to Hashmap

Note that this isn't really recommended. This code does not check for duplicate keys in the hashmap, and it will overwrite any existing keys!!!

public HashMap<String, String> parseXml(String xml) {
    XmlPullParserFactory factory;
    String tagName = "";
    String text = "";
            HashMap<String, String> hm = new HashMap<String, String>();
    
    try {
        factory = XmlPullParserFactory.newInstance();
        factory.setNamespaceAware(true);
        XmlPullParser xpp = factory.newPullParser();
        StringReader sr = new StringReader(xml);
        xpp.setInput(sr);
        int eventType = xpp.getEventType();

        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.TEXT) {
                text = xpp.getText(); //Pulling out node text
            } else if (eventType == XmlPullParser.END_TAG) {
                tagName = xpp.getName();
                hm.put(tagName, text);
                text = ""; //Reset text for the next node
            }
            eventType = xpp.next();
        }
    }  catch (XmlPullParserException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        Log.d("Exception attribute", e + "+" + tagName);
    }
}

Upvotes: 4

I_4m_Z3r0
I_4m_Z3r0

Reputation: 1080

I share because I didn't found good codes about this answer and I won't add another external library to my project so I did a simple XML Parser which converts XML -> List Obviously by using the concept anyone can do a better implementation than this lol

I did another XML parser in PhP which only use objects (php is more free about creating customized on the fly objects.) if anyone need just write a comment above and I will share it.

XML parsing recursive functions to parse a Full Trimmed XML String (No space between XML Tags and no \n and \t) and get a List of HashMaps Tree (an HashMap tree is an HashMap which can contains others HashMaps which can contains others HashMaps ecc ecc, so in code: HashMap where T can be HashMap or, for example, String so HashMap>>

Usage:

DataUtils.parseStringXML(XML.replaceAll("[\\n\\t ]*", "")));

Library Code:

(# LoL Edited 10/07/2019 18:00 -> added "lastEvent" to manage empty XML tag)

(# LoL Edited Again 10/07/2019 18:52 -> fixed "lastEvent" to manage empty XML tags)

/** XML Parsing Methods **/
public static <T extends Object> List<HashMap<String, T>> parseStringXML(String xml){
    List<HashMap<String, T>> ret = new ArrayList<>();
    if(xml != null && !TextUtils.isEmpty(xml)){
        try{
            XmlPullParserFactory xppFactory = XmlPullParserFactory.newInstance();
            xppFactory.setNamespaceAware(true);
            XmlPullParser xpp = xppFactory.newPullParser();
            xpp.setInput(new StringReader(xml));
            ret = parseTagsXML(xpp);
        } catch (XmlPullParserException xppE){
            EMaxLogger.onException(TAG, xppE);
        } catch (Exception e){
            EMaxLogger.onException(TAG, e);
        }
    }
    return ret;
}

/** XML Parsing Methods **/
private static <T extends Object> T parseTagsXML(XmlPullParser xpp) {
    int index = 0x0;
    List<HashMap<String, T>> tree = new ArrayList<HashMap<String, T>>(){{add(new HashMap<>());}};
    try{
        List<String> tags = new ArrayList<>();
        int event = 0x0; int lastEvent;
        while(event != XmlPullParser.END_DOCUMENT){
            lastEvent = xpp.getEventType();
            if(lastEvent == XmlPullParser.END_TAG && tags.contains(xpp.getName())){
                tags.remove(xpp.getName());
                if(tags.size() == 0x0){
                    return (T) new HashMap<String, T>(){{put(xpp.getName(), null);}};
                }
            }
            event = xpp.next();
            switch (event){
                case XmlPullParser.START_TAG:
                    tags.add(xpp.getName());
                    if(tags.size() >= 0x2 && containsStringKeyInMapsTree(tree.get(index), tags.get(tags.size() - 0x2))){
                        tree.set(index, putMapElementInTreeMaps(tags.get(tags.size() - 0x2), tree.get(index), tags.get(tags.size() - 0x1), parseTagsXML(xpp)));
                    } else {
                        tree.get(index).put(tags.get(tags.size() - 0x1), parseTagsXML(xpp));
                    }
                    break;
                case XmlPullParser.TEXT:
                    return (T) xpp.getText();
                case XmlPullParser.END_TAG:
                    if(tags.size() > 0x0 && tags.contains(xpp.getName())) {
                        tags.remove(xpp.getName());
                        if(tags.size() == 0x0){
                            if(xpp.getDepth() == 0x1) {
                                index++;
                                tree.add(new HashMap<>());
                                break;
                            } else {
                                return (T) tree.get(index);
                            }
                        }
                    }
                    if(lastEvent == XmlPullParser.START_TAG){
                        return null;
                    }
                    break;
            }
        }
        if(tree.size() >= index && (tree.get(index) == null || tree.get(index).isEmpty())) {
            tree.remove(index);
        }
    } catch(IOException ioE){
        EMaxLogger.onException(TAG, ioE);
    } catch(XmlPullParserException xppE){
        EMaxLogger.onException(TAG, xppE);
    }
    return (T) tree;
}

/** Tree HashMap Methods **/
private static <T extends Object> boolean containsStringKeyInMapsTree(HashMap<String, T> tree, String key) {
    if(tree != null){
        if(tree.containsKey(key)){
            return true;
        } else if(tree.size() > 0x0){
            for(String k : tree.keySet()){
                if(k != null && !TextUtils.isEmpty(k.trim()) && tree.get(k) != null && tree.get(k) instanceof HashMap && containsStringKeyInMapsTree((HashMap<String, T>) tree.get(k), key)){
                    return true;
                }
            }
        }
    }
    return false;
}

private static <T extends Object> HashMap<String, T> putMapElementInTreeMaps(String parentKey, HashMap<String, T> tree, String elementKey, T element){
    if(tree != null){
        if(tree.containsKey(parentKey) && tree.get(parentKey) != null && tree.get(parentKey) instanceof HashMap){
            ((HashMap<String, T>) tree.get(parentKey)).put(elementKey, element);
        } else if(tree.size() > 0x0){
            for(String key : tree.keySet()){
                if(key != null && !TextUtils.isEmpty(key.trim()) && tree.get(key) != null && tree.get(key) instanceof HashMap){
                    tree.put(key, (T) putMapElementInTreeMaps(parentKey, (HashMap<String, T>) tree.get(key), elementKey, element));
                }

            }
        }
    }
    return tree;
}

It uses recursion. Maybe if I need I will do an XML parser which converts XML to Generic Objects. To do that you need to use "pre-determinate" "setter & getter" methods in the Object. For example for every tag XML you will have a "getTAG_NAME()" and a "setTAG_NAME" method which you will use to set the value inside the object.

To do that you need to use Java - Field and Method classes, for example to set a Object field using is name:

public static void setFieldValue(Object obj, Field field, int value){
    try {
        field.setInt(obj, value);
    } catch (IllegalAccessException iacE) {
        EMaxLogger.onException(TAG, iacE);
    } catch (IllegalArgumentException iarE) {
        EMaxLogger.onException(TAG, iarE);
    } catch (Exception e) {
        EMaxLogger.onException(TAG, e);
    }
}

So every time you have a new XML_TAG and a new TAG_VALUE then you call the respective "setXML_TAG(TAG_VALUE)" method by using the function above.

The function prototype will be something like:

public static <T extends Object> T parseStringXmlToGenericObject(T myObj, String xml){ [...] }

Where T is the specific object built to store the XML TAGs Value, for example:

 <root_xml_tag>
      <xml_tag_0x1>lol</xml_tag_0x1> 
      <xml_tag_0x2>
           <xml_tag_0x2a>asd</xml_tag_0x2a>
           <xml_ag_0x2b>lmao</xml_tag_0x2b>
      </xml_tag_0x2>
      <xml_tag_0x3>rotfl</xml_tag_0x3>
 </root_xml_tag>

If you have anything like the xml Above your object to store XML data will be:

public class RootXmlTag {
  private String mXmlTag0x1;
  private XmlTag0x2 mXmlTag0x2;
  private String mXmlTag0x3;

  /** Setter & Getter Methods **/
  public void setxml_tag_0x1(String val){ 
     mXmlTag0x1 = val;
  }

  public String getxml_tag_0x1(){
     return mXmlTag0x1;
  }

  public void setxml_tag_0x2(XmlTag0x2 val){ 
     mXmlTag0x2 = val;
  }

  public XmlTag0x2 getxml_tag_0x2(){
     return mXmlTag0x2;
  }

  [... Ecc Ecc Setter & Getter Methods 4 Every Objects Properties ...]

  [... Other Methods you will need ....]


  /** Inner Classes For Parents XML Tags (Tags with Tags Sons ecc..) **/
  class XmlTag0x2 {

     private String mXmlTag0x2a;
     private String mXmlTag0x2b;

     /** Getter & Setter Methods **/
     [... See Outer Parent Class ...]
  }

}

So then when you get an xml tag you just do, really simple example (Proof-Of-Concept [PoC):

// Assume having "T myObj" where T = RootXmlTag. 

String xmlTagName = xpp.getName();
event = xpp.next();
String tagValue = xpp.getText();
Utilities.setFieldValue(yourObj, "set".concat(xmlTagName), tagValue);

Obviously to do that you need to know how the XML is before parse it (when you won't know that? lol. Obviously you will know how it is, maybe you will have different versions of the xml with differents tags, but you will always know the structure you will get.)

Personally actually I'm using the HashMaps Trees way just because I don't like to create a lot of class just to parse 2 or 3 XMLs, so actually I didn't implemented the Object Way. If I will do that I will share it.

GG Bye, have a Nice Coding!

(I'd like if anybody post others solution using recursive functions just to compare and learn more methods (: Thank you! bye all)

Upvotes: 0

Martin Vysny
Martin Vysny

Reputation: 3201

The Stax/pull is really clunky since the API is pretty low-level and hard to use directly. Please try Konsume-XML instead:

val file = File("person.xml")
file.konsumeXml().use { k ->
    k.child("Person") {
        println(childText("Name"))
        println(childText("Gender"))
        println(childText("More"))
    }
}

This will print

Scott
Male
And So On..

Upvotes: 0

james
james

Reputation: 26271

I found the IBM example clunky and messy. I wrote my own thing to handle RSS feeds, it can be adapted to accommodate custom XML feeds.

an example of using this:

save the contents of this yahoo feed into a file and place it on your project. read the file into a string.

String fileContents = ...;
XMLFeed feed = XMLUtils.getXmlFeed(fileContents);

you now have an object containing a list of each entry from the RSS feed

There are 4 classes below. I have commented some for my own good but it may be confusing for others.

Basically, the DefaultHandler looks through the XML string for common RSS names such as description, URL, title, etc. it saves each entry into its own object and adds it to the master list. the constant(final) fields in the DefaultHandler class can be changed(add/remove strings) to fit your structure - although you may need to change the structure of the XmlFeedItem class too.

You should be able to use this with no changes to standard RSS feeds.

hope it helps

public class XMLUtils {
    public static XmlFeed getXmlFeed(String xmlString) {
        XMLHandler handler = null;
        try {
            XMLReader xr = SAXParserFactory.newInstance().newSAXParser().getXMLReader();

            handler = new XMLHandler();
            xr.setContentHandler(handler);

            InputSource is = new InputSource();
            is.setEncoding("UTF-8");
            is.setCharacterStream(new StringReader(xmlString));

            xr.parse(is);
        }
        catch(SAXException e) {
            return null;
        }
        catch(ParserConfigurationException e) {
            return null;
        }
        catch(IOException e) {
            return null;
        }
        return handler.getXmlFeed();
    }
}

public class XMLHandler extends DefaultHandler {
    /**
     * entity names in the XML document such as <item> which contain many fields
     */
    private static final String OBJECTS[] = new String[] {"item", "entry"};

    /**
     * field names which correspond to a "description"
     */
    private static final String DESCRIPTIONS[] = new String[] {"description", "summary"};

    /**
     * field names which correspond to a "url"
     */
    private static final String URLS[] = new String[] {"link", "id", "guid"};

    /**
     * field names which correspond to "date"
     */
    private static final String PUB_DATES[] = new String[] {"pubDate", "date", "updated", "published", "timestamp"};

    /**
     * field names which correspond to "title"
     */
    private static final String TITLES[] = new String[] {"title"};

    /**
     * the current element being read in
     */
    private String currentElement;
    private boolean foundItem;
    private XmlFeed xmlFeed;
    private XmlFeedItem xmlFeedItem;

    private String object, description, url, pubDate, title;
    public XMLHandler() {
        currentElement = "";
        object = description = url = pubDate = title = null;
        foundItem = false;
        xmlFeed = new XmlFeed();
    }
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
        String s = new String(ch, start, length);
        if(foundItem && s.trim().length() > 0) {
            if(isFieldAvailable(currentElement, DESCRIPTIONS, description)) {
                xmlFeedItem.setDescription(xmlFeedItem.getDescription() + s);
            }
            else if(isFieldAvailable(currentElement, URLS, url)) {
                xmlFeedItem.setUrl(xmlFeedItem.getUrl() + s);
            }
            else if(isFieldAvailable(currentElement, PUB_DATES, pubDate)) {
                xmlFeedItem.setPubDate(xmlFeedItem.getPubDate() + s);
            }
            else if(isFieldAvailable(currentElement, TITLES, title)) {
                xmlFeedItem.setTitle(xmlFeedItem.getTitle() + s);
            }
        }
    }
    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
        if(isFieldAvailable(localName, OBJECTS, object)) {
            xmlFeed.getItems().add(new XmlFeedItem(xmlFeedItem));
            xmlFeedItem = new XmlFeedItem();
        }
    }
    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
    }
    /**
     * @param fieldToTest the current element found in the XML string while parsing
     * @param options the array of elements available to match fieldToTest to
     * @param currentField the element that we're currently inside
     * @return <p>if <strong>fieldToTest</strong> is contained in <strong>options</strong> and if <strong>currentField</strong> 
     * is either null or contained in <strong>options</strong>.  This allows us to specify a number of different 
     * fields which mean the same thing in an XML feed.  Example: <strong>summary</strong> may not be included 
     * in a feed but <strong>description</strong> is.  Both <strong>summary</strong> and <strong>description</strong> are contained 
     * in the available <strong>options</strong>, so it is still matched up and used.  Once one element is used 
     * and is contained in <strong>options</strong> it will always use the same element.  <strong>currentField</strong> 
     * is assigned to <strong>fieldToTest</strong> if returning true and if its null(hasn't been matched before)</p>
     */
    private boolean isFieldAvailable(String fieldToTest, String[] options, String currentField) {
        for(String field: options) {
            if(field.equalsIgnoreCase(fieldToTest) && (currentField == null || currentField.equalsIgnoreCase(field))) {
                if(currentField == null) {
                    currentField = new String(fieldToTest);
                }
                return true;
            }
        }
        return false;
    }
    @Override
    public void startElement(String uri, String localName, String qName,
            Attributes attributes) throws SAXException {
        super.startElement(uri, localName, qName, attributes);
        currentElement = new String(localName);
        if(!foundItem && isFieldAvailable(localName, OBJECTS, object)) {
            foundItem = true;
            xmlFeedItem = new XmlFeedItem();
        }
    }
    public XmlFeed getXmlFeed() {
        return xmlFeed;
    }
}

public class XmlFeed {
    private List<XmlFeedItem> items;
    public XmlFeed() {
        items = new ArrayList<XmlFeedItem>();
    }
    public List<XmlFeedItem> getItems() {
        return items;
    }
    public void setItems(List<XmlFeedItem> items) {
        this.items = items;
    }
}

public class XmlFeedItem {
    private String title;
    private String description;
    private String pubDate;
    private String url;
    public XmlFeedItem() {
        title = description = pubDate = url = "";
    }
    public XmlFeedItem(XmlFeedItem rssFeedItem) {
        this.title = rssFeedItem.getTitle();
        this.description = rssFeedItem.getDescription();
        this.pubDate = rssFeedItem.getPubDate();
        this.url = rssFeedItem.getUrl();
    }
    public String getPubDate() {
        return pubDate;
    }
    public void setPubDate(String pubDate) {
        this.pubDate = pubDate;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
}

Upvotes: 0

FunkTheMonk
FunkTheMonk

Reputation: 10938

Could use a SAX parser something like:

private void parse(String xml) {
        final ArrayList<Person> people = new ArrayList<Person>();
        Xml.parse(xml, new DefaultHandler() {
            private Person person;
            private StringBuilder builder;

            @Override
            public void startElement(String uri, String localName,
                    String qName, Attributes attributes) throws SAXException {
                builder = new StringBuilder();
                if(localName.equals("Person")) {
                    person = new Person();
                }
            }

            @Override
            public void endElement(String uri, String localName, String qName)
                    throws SAXException {
                if(localName.equals("Person")) {
                    people.add(person);
                }
                else if(localName.equals("Name")){
                    person.setName(builder.toString());
                }
                else if(localName.equals...) {
                    ... etc
                }
            }

            @Override
            public void characters(char[] ch, int start, int length)
                    throws SAXException {
                builder.append(ch, start, length);
            }
        });
    }

Upvotes: 0

Plastic Sturgeon
Plastic Sturgeon

Reputation: 12527

Maybe move away from the IBM example to a simpler example like this one: http://p-xr.com/android-tutorial-how-to-parseread-xml-data-into-android-listview/

Upvotes: 0

sgarman
sgarman

Reputation: 6182

It looks like Person is actually the root node here, maybe you don't need root.getElementsByTagName("Person");

If your planning on having multiple people maybe change the xml file to and then change it to root.getElementsByTagName("Person");

Upvotes: 0

Related Questions