/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.unit_tests; import android.graphics.Bitmap; import android.sax.Element; import android.sax.ElementListener; import android.sax.EndTextElementListener; import android.sax.RootElement; import android.sax.StartElementListener; import android.sax.TextElementListener; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; import android.text.format.Time; import android.util.Log; import android.util.Xml; import com.android.internal.util.XmlUtils; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; public class SafeSaxTest extends AndroidTestCase { private static final String TAG = SafeSaxTest.class.getName(); private static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"; private static final String MEDIA_NAMESPACE = "http://search.yahoo.com/mrss/"; private static final String YOUTUBE_NAMESPACE = "http://gdata.youtube.com/schemas/2007"; private static final String GDATA_NAMESPACE = "http://schemas.google.com/g/2005"; private static class ElementCounter implements ElementListener { int starts = 0; int ends = 0; public void start(Attributes attributes) { starts++; } public void end() { ends++; } } private static class TextElementCounter implements TextElementListener { int starts = 0; String bodies = ""; public void start(Attributes attributes) { starts++; } public void end(String body) { this.bodies += body; } } @SmallTest public void testListener() throws Exception { String xml = "\n" + "\n" + "a\n" + "\n" + "\n" + "b\n" + "\n" + "\n"; RootElement root = new RootElement(ATOM_NAMESPACE, "feed"); Element entry = root.requireChild(ATOM_NAMESPACE, "entry"); Element id = entry.requireChild(ATOM_NAMESPACE, "id"); ElementCounter rootCounter = new ElementCounter(); ElementCounter entryCounter = new ElementCounter(); TextElementCounter idCounter = new TextElementCounter(); root.setElementListener(rootCounter); entry.setElementListener(entryCounter); id.setTextElementListener(idCounter); Xml.parse(xml, root.getContentHandler()); assertEquals(1, rootCounter.starts); assertEquals(1, rootCounter.ends); assertEquals(2, entryCounter.starts); assertEquals(2, entryCounter.ends); assertEquals(2, idCounter.starts); assertEquals("ab", idCounter.bodies); } @SmallTest public void testMissingRequiredChild() throws Exception { String xml = ""; RootElement root = new RootElement("feed"); root.requireChild("entry"); try { Xml.parse(xml, root.getContentHandler()); fail("expected exception not thrown"); } catch (SAXException e) { // Expected. } } @SmallTest public void testMixedContent() throws Exception { String xml = ""; RootElement root = new RootElement("feed"); root.setEndTextElementListener(new EndTextElementListener() { public void end(String body) { } }); try { Xml.parse(xml, root.getContentHandler()); fail("expected exception not thrown"); } catch (SAXException e) { // Expected. } } @LargeTest public void testPerformance() throws Exception { InputStream in = mContext.getResources().openRawResource(R.raw.youtube); byte[] xmlBytes; try { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } xmlBytes = out.toByteArray(); } finally { in.close(); } Log.i("***", "File size: " + (xmlBytes.length / 1024) + "k"); VideoAdapter videoAdapter = new VideoAdapter(); ContentHandler handler = newContentHandler(videoAdapter); for (int i = 0; i < 2; i++) { pureSaxTest(new ByteArrayInputStream(xmlBytes)); saxyModelTest(new ByteArrayInputStream(xmlBytes)); saxyModelTest(new ByteArrayInputStream(xmlBytes), handler); } } private static void pureSaxTest(InputStream inputStream) throws IOException, SAXException { long start = System.currentTimeMillis(); VideoAdapter videoAdapter = new VideoAdapter(); Xml.parse(inputStream, Xml.Encoding.UTF_8, new YouTubeContentHandler(videoAdapter)); long elapsed = System.currentTimeMillis() - start; Log.i(TAG, "pure SAX: " + elapsed + "ms"); } private static void saxyModelTest(InputStream inputStream) throws IOException, SAXException { long start = System.currentTimeMillis(); VideoAdapter videoAdapter = new VideoAdapter(); Xml.parse(inputStream, Xml.Encoding.UTF_8, newContentHandler(videoAdapter)); long elapsed = System.currentTimeMillis() - start; Log.i(TAG, "Saxy Model: " + elapsed + "ms"); } private static void saxyModelTest(InputStream inputStream, ContentHandler contentHandler) throws IOException, SAXException { long start = System.currentTimeMillis(); Xml.parse(inputStream, Xml.Encoding.UTF_8, contentHandler); long elapsed = System.currentTimeMillis() - start; Log.i(TAG, "Saxy Model (preloaded): " + elapsed + "ms"); } private static class VideoAdapter { public void addVideo(YouTubeVideo video) { } } private static ContentHandler newContentHandler(VideoAdapter videoAdapter) { return new HandlerFactory().newContentHandler(videoAdapter); } private static class HandlerFactory { YouTubeVideo video; public ContentHandler newContentHandler(VideoAdapter videoAdapter) { RootElement root = new RootElement(ATOM_NAMESPACE, "feed"); final VideoListener videoListener = new VideoListener(videoAdapter); Element entry = root.getChild(ATOM_NAMESPACE, "entry"); entry.setElementListener(videoListener); entry.getChild(ATOM_NAMESPACE, "id") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { video.videoId = body; } }); entry.getChild(ATOM_NAMESPACE, "published") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { // TODO(tomtaylor): programmatically get the timezone video.dateAdded = new Time(Time.TIMEZONE_UTC); video.dateAdded.parse3339(body); } }); Element author = entry.getChild(ATOM_NAMESPACE, "author"); author.getChild(ATOM_NAMESPACE, "name") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { video.authorName = body; } }); Element mediaGroup = entry.getChild(MEDIA_NAMESPACE, "group"); mediaGroup.getChild(MEDIA_NAMESPACE, "thumbnail") .setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String url = attributes.getValue("", "url"); if (video.thumbnailUrl == null && url.length() > 0) { video.thumbnailUrl = url; } } }); mediaGroup.getChild(MEDIA_NAMESPACE, "content") .setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String url = attributes.getValue("", "url"); if (url != null) { video.videoUrl = url; } } }); mediaGroup.getChild(MEDIA_NAMESPACE, "player") .setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String url = attributes.getValue("", "url"); if (url != null) { video.playbackUrl = url; } } }); mediaGroup.getChild(MEDIA_NAMESPACE, "title") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { video.title = body; } }); mediaGroup.getChild(MEDIA_NAMESPACE, "category") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { video.category = body; } }); mediaGroup.getChild(MEDIA_NAMESPACE, "description") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { video.description = body; } }); mediaGroup.getChild(MEDIA_NAMESPACE, "keywords") .setEndTextElementListener(new EndTextElementListener() { public void end(String body) { video.tags = body; } }); mediaGroup.getChild(YOUTUBE_NAMESPACE, "duration") .setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String seconds = attributes.getValue("", "seconds"); video.lengthInSeconds = XmlUtils.convertValueToInt(seconds, 0); } }); mediaGroup.getChild(YOUTUBE_NAMESPACE, "statistics") .setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String viewCount = attributes.getValue("", "viewCount"); video.viewCount = XmlUtils.convertValueToInt(viewCount, 0); } }); entry.getChild(GDATA_NAMESPACE, "rating") .setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String average = attributes.getValue("", "average"); video.rating = average == null ? 0.0f : Float.parseFloat(average); } }); return root.getContentHandler(); } class VideoListener implements ElementListener { final VideoAdapter videoAdapter; public VideoListener(VideoAdapter videoAdapter) { this.videoAdapter = videoAdapter; } public void start(Attributes attributes) { video = new YouTubeVideo(); } public void end() { videoAdapter.addVideo(video); video = null; } } } private static class YouTubeContentHandler extends DefaultHandler { final VideoAdapter videoAdapter; YouTubeVideo video = null; StringBuilder builder = null; public YouTubeContentHandler(VideoAdapter videoAdapter) { this.videoAdapter = videoAdapter; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (uri.equals(ATOM_NAMESPACE)) { if (localName.equals("entry")) { video = new YouTubeVideo(); return; } if (video == null) { return; } if (!localName.equals("id") && !localName.equals("published") && !localName.equals("name")) { return; } this.builder = new StringBuilder(); return; } if (video == null) { return; } if (uri.equals(MEDIA_NAMESPACE)) { if (localName.equals("thumbnail")) { String url = attributes.getValue("", "url"); if (video.thumbnailUrl == null && url.length() > 0) { video.thumbnailUrl = url; } return; } if (localName.equals("content")) { String url = attributes.getValue("", "url"); if (url != null) { video.videoUrl = url; } return; } if (localName.equals("player")) { String url = attributes.getValue("", "url"); if (url != null) { video.playbackUrl = url; } return; } if (localName.equals("title") || localName.equals("category") || localName.equals("description") || localName.equals("keywords")) { this.builder = new StringBuilder(); return; } return; } if (uri.equals(YOUTUBE_NAMESPACE)) { if (localName.equals("duration")) { video.lengthInSeconds = XmlUtils.convertValueToInt( attributes.getValue("", "seconds"), 0); return; } if (localName.equals("statistics")) { video.viewCount = XmlUtils.convertValueToInt( attributes.getValue("", "viewCount"), 0); return; } return; } if (uri.equals(GDATA_NAMESPACE)) { if (localName.equals("rating")) { String average = attributes.getValue("", "average"); video.rating = average == null ? 0.0f : Float.parseFloat(average); } } } @Override public void characters(char text[], int start, int length) throws SAXException { if (builder != null) { builder.append(text, start, length); } } String takeText() { try { return builder.toString(); } finally { builder = null; } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (video == null) { return; } if (uri.equals(ATOM_NAMESPACE)) { if (localName.equals("published")) { // TODO(tomtaylor): programmatically get the timezone video.dateAdded = new Time(Time.TIMEZONE_UTC); video.dateAdded.parse3339(takeText()); return; } if (localName.equals("name")) { video.authorName = takeText(); return; } if (localName.equals("id")) { video.videoId = takeText(); return; } if (localName.equals("entry")) { // Add the video! videoAdapter.addVideo(video); video = null; return; } return; } if (uri.equals(MEDIA_NAMESPACE)) { if (localName.equals("description")) { video.description = takeText(); return; } if (localName.equals("keywords")) { video.tags = takeText(); return; } if (localName.equals("category")) { video.category = takeText(); return; } if (localName.equals("title")) { video.title = takeText(); } } } } private static class YouTubeVideo { public String videoId; // the id used to lookup on YouTube public String videoUrl; // the url to play the video public String playbackUrl; // the url to share for users to play video public String thumbnailUrl; // the url of the thumbnail image public String title; public Bitmap bitmap; // cached bitmap of the thumbnail public int lengthInSeconds; public int viewCount; // number of times the video has been viewed public float rating; // ranges from 0.0 to 5.0 public Boolean triedToLoadThumbnail; public String authorName; public Time dateAdded; public String category; public String tags; public String description; } }