1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.unit_tests; 18 19 import android.graphics.Bitmap; 20 import android.sax.Element; 21 import android.sax.ElementListener; 22 import android.sax.EndTextElementListener; 23 import android.sax.RootElement; 24 import android.sax.StartElementListener; 25 import android.sax.TextElementListener; 26 import android.test.AndroidTestCase; 27 import android.test.suitebuilder.annotation.LargeTest; 28 import android.test.suitebuilder.annotation.SmallTest; 29 import android.text.format.Time; 30 import android.util.Log; 31 import android.util.Xml; 32 import com.android.internal.util.XmlUtils; 33 import org.xml.sax.Attributes; 34 import org.xml.sax.ContentHandler; 35 import org.xml.sax.SAXException; 36 import org.xml.sax.helpers.DefaultHandler; 37 38 import java.io.ByteArrayInputStream; 39 import java.io.ByteArrayOutputStream; 40 import java.io.IOException; 41 import java.io.InputStream; 42 43 public class SafeSaxTest extends AndroidTestCase { 44 45 private static final String TAG = SafeSaxTest.class.getName(); 46 47 private static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"; 48 private static final String MEDIA_NAMESPACE = "http://search.yahoo.com/mrss/"; 49 private static final String YOUTUBE_NAMESPACE = "http://gdata.youtube.com/schemas/2007"; 50 private static final String GDATA_NAMESPACE = "http://schemas.google.com/g/2005"; 51 52 private static class ElementCounter implements ElementListener { 53 int starts = 0; 54 int ends = 0; 55 start(Attributes attributes)56 public void start(Attributes attributes) { 57 starts++; 58 } 59 end()60 public void end() { 61 ends++; 62 } 63 } 64 65 private static class TextElementCounter implements TextElementListener { 66 int starts = 0; 67 String bodies = ""; 68 start(Attributes attributes)69 public void start(Attributes attributes) { 70 starts++; 71 } 72 end(String body)73 public void end(String body) { 74 this.bodies += body; 75 } 76 } 77 78 @SmallTest testListener()79 public void testListener() throws Exception { 80 String xml = "<feed xmlns='http://www.w3.org/2005/Atom'>\n" 81 + "<entry>\n" 82 + "<id>a</id>\n" 83 + "</entry>\n" 84 + "<entry>\n" 85 + "<id>b</id>\n" 86 + "</entry>\n" 87 + "</feed>\n"; 88 89 RootElement root = new RootElement(ATOM_NAMESPACE, "feed"); 90 Element entry = root.requireChild(ATOM_NAMESPACE, "entry"); 91 Element id = entry.requireChild(ATOM_NAMESPACE, "id"); 92 93 ElementCounter rootCounter = new ElementCounter(); 94 ElementCounter entryCounter = new ElementCounter(); 95 TextElementCounter idCounter = new TextElementCounter(); 96 97 root.setElementListener(rootCounter); 98 entry.setElementListener(entryCounter); 99 id.setTextElementListener(idCounter); 100 101 Xml.parse(xml, root.getContentHandler()); 102 103 assertEquals(1, rootCounter.starts); 104 assertEquals(1, rootCounter.ends); 105 assertEquals(2, entryCounter.starts); 106 assertEquals(2, entryCounter.ends); 107 assertEquals(2, idCounter.starts); 108 assertEquals("ab", idCounter.bodies); 109 } 110 111 @SmallTest testMissingRequiredChild()112 public void testMissingRequiredChild() throws Exception { 113 String xml = "<feed></feed>"; 114 RootElement root = new RootElement("feed"); 115 root.requireChild("entry"); 116 117 try { 118 Xml.parse(xml, root.getContentHandler()); 119 fail("expected exception not thrown"); 120 } catch (SAXException e) { 121 // Expected. 122 } 123 } 124 125 @SmallTest testMixedContent()126 public void testMixedContent() throws Exception { 127 String xml = "<feed><entry></entry></feed>"; 128 129 RootElement root = new RootElement("feed"); 130 root.setEndTextElementListener(new EndTextElementListener() { 131 public void end(String body) { 132 } 133 }); 134 135 try { 136 Xml.parse(xml, root.getContentHandler()); 137 fail("expected exception not thrown"); 138 } catch (SAXException e) { 139 // Expected. 140 } 141 } 142 143 @LargeTest testPerformance()144 public void testPerformance() throws Exception { 145 InputStream in = mContext.getResources().openRawResource(R.raw.youtube); 146 byte[] xmlBytes; 147 try { 148 ByteArrayOutputStream out = new ByteArrayOutputStream(); 149 byte[] buffer = new byte[1024]; 150 int length; 151 while ((length = in.read(buffer)) != -1) { 152 out.write(buffer, 0, length); 153 } 154 xmlBytes = out.toByteArray(); 155 } finally { 156 in.close(); 157 } 158 159 Log.i("***", "File size: " + (xmlBytes.length / 1024) + "k"); 160 161 VideoAdapter videoAdapter = new VideoAdapter(); 162 ContentHandler handler = newContentHandler(videoAdapter); 163 for (int i = 0; i < 2; i++) { 164 pureSaxTest(new ByteArrayInputStream(xmlBytes)); 165 saxyModelTest(new ByteArrayInputStream(xmlBytes)); 166 saxyModelTest(new ByteArrayInputStream(xmlBytes), handler); 167 } 168 } 169 pureSaxTest(InputStream inputStream)170 private static void pureSaxTest(InputStream inputStream) throws IOException, SAXException { 171 long start = System.currentTimeMillis(); 172 VideoAdapter videoAdapter = new VideoAdapter(); 173 Xml.parse(inputStream, Xml.Encoding.UTF_8, new YouTubeContentHandler(videoAdapter)); 174 long elapsed = System.currentTimeMillis() - start; 175 Log.i(TAG, "pure SAX: " + elapsed + "ms"); 176 } 177 saxyModelTest(InputStream inputStream)178 private static void saxyModelTest(InputStream inputStream) throws IOException, SAXException { 179 long start = System.currentTimeMillis(); 180 VideoAdapter videoAdapter = new VideoAdapter(); 181 Xml.parse(inputStream, Xml.Encoding.UTF_8, newContentHandler(videoAdapter)); 182 long elapsed = System.currentTimeMillis() - start; 183 Log.i(TAG, "Saxy Model: " + elapsed + "ms"); 184 } 185 saxyModelTest(InputStream inputStream, ContentHandler contentHandler)186 private static void saxyModelTest(InputStream inputStream, ContentHandler contentHandler) 187 throws IOException, SAXException { 188 long start = System.currentTimeMillis(); 189 Xml.parse(inputStream, Xml.Encoding.UTF_8, contentHandler); 190 long elapsed = System.currentTimeMillis() - start; 191 Log.i(TAG, "Saxy Model (preloaded): " + elapsed + "ms"); 192 } 193 194 private static class VideoAdapter { addVideo(YouTubeVideo video)195 public void addVideo(YouTubeVideo video) { 196 } 197 } 198 newContentHandler(VideoAdapter videoAdapter)199 private static ContentHandler newContentHandler(VideoAdapter videoAdapter) { 200 return new HandlerFactory().newContentHandler(videoAdapter); 201 } 202 203 private static class HandlerFactory { 204 YouTubeVideo video; 205 newContentHandler(VideoAdapter videoAdapter)206 public ContentHandler newContentHandler(VideoAdapter videoAdapter) { 207 RootElement root = new RootElement(ATOM_NAMESPACE, "feed"); 208 209 final VideoListener videoListener = new VideoListener(videoAdapter); 210 211 Element entry = root.getChild(ATOM_NAMESPACE, "entry"); 212 213 entry.setElementListener(videoListener); 214 215 entry.getChild(ATOM_NAMESPACE, "id") 216 .setEndTextElementListener(new EndTextElementListener() { 217 public void end(String body) { 218 video.videoId = body; 219 } 220 }); 221 222 entry.getChild(ATOM_NAMESPACE, "published") 223 .setEndTextElementListener(new EndTextElementListener() { 224 public void end(String body) { 225 // TODO(tomtaylor): programmatically get the timezone 226 video.dateAdded = new Time(Time.TIMEZONE_UTC); 227 video.dateAdded.parse3339(body); 228 } 229 }); 230 231 Element author = entry.getChild(ATOM_NAMESPACE, "author"); 232 author.getChild(ATOM_NAMESPACE, "name") 233 .setEndTextElementListener(new EndTextElementListener() { 234 public void end(String body) { 235 video.authorName = body; 236 } 237 }); 238 239 Element mediaGroup = entry.getChild(MEDIA_NAMESPACE, "group"); 240 241 mediaGroup.getChild(MEDIA_NAMESPACE, "thumbnail") 242 .setStartElementListener(new StartElementListener() { 243 public void start(Attributes attributes) { 244 String url = attributes.getValue("", "url"); 245 if (video.thumbnailUrl == null && url.length() > 0) { 246 video.thumbnailUrl = url; 247 } 248 } 249 }); 250 251 mediaGroup.getChild(MEDIA_NAMESPACE, "content") 252 .setStartElementListener(new StartElementListener() { 253 public void start(Attributes attributes) { 254 String url = attributes.getValue("", "url"); 255 if (url != null) { 256 video.videoUrl = url; 257 } 258 } 259 }); 260 261 mediaGroup.getChild(MEDIA_NAMESPACE, "player") 262 .setStartElementListener(new StartElementListener() { 263 public void start(Attributes attributes) { 264 String url = attributes.getValue("", "url"); 265 if (url != null) { 266 video.playbackUrl = url; 267 } 268 } 269 }); 270 271 mediaGroup.getChild(MEDIA_NAMESPACE, "title") 272 .setEndTextElementListener(new EndTextElementListener() { 273 public void end(String body) { 274 video.title = body; 275 } 276 }); 277 278 mediaGroup.getChild(MEDIA_NAMESPACE, "category") 279 .setEndTextElementListener(new EndTextElementListener() { 280 public void end(String body) { 281 video.category = body; 282 } 283 }); 284 285 mediaGroup.getChild(MEDIA_NAMESPACE, "description") 286 .setEndTextElementListener(new EndTextElementListener() { 287 public void end(String body) { 288 video.description = body; 289 } 290 }); 291 292 mediaGroup.getChild(MEDIA_NAMESPACE, "keywords") 293 .setEndTextElementListener(new EndTextElementListener() { 294 public void end(String body) { 295 video.tags = body; 296 } 297 }); 298 299 mediaGroup.getChild(YOUTUBE_NAMESPACE, "duration") 300 .setStartElementListener(new StartElementListener() { 301 public void start(Attributes attributes) { 302 String seconds = attributes.getValue("", "seconds"); 303 video.lengthInSeconds 304 = XmlUtils.convertValueToInt(seconds, 0); 305 } 306 }); 307 308 mediaGroup.getChild(YOUTUBE_NAMESPACE, "statistics") 309 .setStartElementListener(new StartElementListener() { 310 public void start(Attributes attributes) { 311 String viewCount = attributes.getValue("", "viewCount"); 312 video.viewCount 313 = XmlUtils.convertValueToInt(viewCount, 0); 314 } 315 }); 316 317 entry.getChild(GDATA_NAMESPACE, "rating") 318 .setStartElementListener(new StartElementListener() { 319 public void start(Attributes attributes) { 320 String average = attributes.getValue("", "average"); 321 video.rating = average == null 322 ? 0.0f : Float.parseFloat(average); 323 } 324 }); 325 326 return root.getContentHandler(); 327 } 328 329 class VideoListener implements ElementListener { 330 331 final VideoAdapter videoAdapter; 332 VideoListener(VideoAdapter videoAdapter)333 public VideoListener(VideoAdapter videoAdapter) { 334 this.videoAdapter = videoAdapter; 335 } 336 start(Attributes attributes)337 public void start(Attributes attributes) { 338 video = new YouTubeVideo(); 339 } 340 end()341 public void end() { 342 videoAdapter.addVideo(video); 343 video = null; 344 } 345 } 346 } 347 348 private static class YouTubeContentHandler extends DefaultHandler { 349 350 final VideoAdapter videoAdapter; 351 352 YouTubeVideo video = null; 353 StringBuilder builder = null; 354 YouTubeContentHandler(VideoAdapter videoAdapter)355 public YouTubeContentHandler(VideoAdapter videoAdapter) { 356 this.videoAdapter = videoAdapter; 357 } 358 359 @Override startElement(String uri, String localName, String qName, Attributes attributes)360 public void startElement(String uri, String localName, String qName, 361 Attributes attributes) throws SAXException { 362 if (uri.equals(ATOM_NAMESPACE)) { 363 if (localName.equals("entry")) { 364 video = new YouTubeVideo(); 365 return; 366 } 367 368 if (video == null) { 369 return; 370 } 371 372 if (!localName.equals("id") 373 && !localName.equals("published") 374 && !localName.equals("name")) { 375 return; 376 } 377 this.builder = new StringBuilder(); 378 return; 379 380 } 381 382 if (video == null) { 383 return; 384 } 385 386 if (uri.equals(MEDIA_NAMESPACE)) { 387 if (localName.equals("thumbnail")) { 388 String url = attributes.getValue("", "url"); 389 if (video.thumbnailUrl == null && url.length() > 0) { 390 video.thumbnailUrl = url; 391 } 392 return; 393 } 394 395 if (localName.equals("content")) { 396 String url = attributes.getValue("", "url"); 397 if (url != null) { 398 video.videoUrl = url; 399 } 400 return; 401 } 402 403 if (localName.equals("player")) { 404 String url = attributes.getValue("", "url"); 405 if (url != null) { 406 video.playbackUrl = url; 407 } 408 return; 409 } 410 411 if (localName.equals("title") 412 || localName.equals("category") 413 || localName.equals("description") 414 || localName.equals("keywords")) { 415 this.builder = new StringBuilder(); 416 return; 417 } 418 419 return; 420 } 421 422 if (uri.equals(YOUTUBE_NAMESPACE)) { 423 if (localName.equals("duration")) { 424 video.lengthInSeconds = XmlUtils.convertValueToInt( 425 attributes.getValue("", "seconds"), 0); 426 return; 427 } 428 429 if (localName.equals("statistics")) { 430 video.viewCount = XmlUtils.convertValueToInt( 431 attributes.getValue("", "viewCount"), 0); 432 return; 433 } 434 435 return; 436 } 437 438 if (uri.equals(GDATA_NAMESPACE)) { 439 if (localName.equals("rating")) { 440 String average = attributes.getValue("", "average"); 441 video.rating = average == null 442 ? 0.0f : Float.parseFloat(average); 443 } 444 } 445 } 446 447 @Override characters(char text[], int start, int length)448 public void characters(char text[], int start, int length) 449 throws SAXException { 450 if (builder != null) { 451 builder.append(text, start, length); 452 } 453 } 454 takeText()455 String takeText() { 456 try { 457 return builder.toString(); 458 } finally { 459 builder = null; 460 } 461 } 462 463 @Override endElement(String uri, String localName, String qName)464 public void endElement(String uri, String localName, String qName) 465 throws SAXException { 466 if (video == null) { 467 return; 468 } 469 470 if (uri.equals(ATOM_NAMESPACE)) { 471 if (localName.equals("published")) { 472 // TODO(tomtaylor): programmatically get the timezone 473 video.dateAdded = new Time(Time.TIMEZONE_UTC); 474 video.dateAdded.parse3339(takeText()); 475 return; 476 } 477 478 if (localName.equals("name")) { 479 video.authorName = takeText(); 480 return; 481 } 482 483 if (localName.equals("id")) { 484 video.videoId = takeText(); 485 return; 486 } 487 488 if (localName.equals("entry")) { 489 // Add the video! 490 videoAdapter.addVideo(video); 491 video = null; 492 return; 493 } 494 495 return; 496 } 497 498 if (uri.equals(MEDIA_NAMESPACE)) { 499 if (localName.equals("description")) { 500 video.description = takeText(); 501 return; 502 } 503 504 if (localName.equals("keywords")) { 505 video.tags = takeText(); 506 return; 507 } 508 509 if (localName.equals("category")) { 510 video.category = takeText(); 511 return; 512 } 513 514 if (localName.equals("title")) { 515 video.title = takeText(); 516 } 517 } 518 } 519 } 520 521 private static class YouTubeVideo { 522 public String videoId; // the id used to lookup on YouTube 523 public String videoUrl; // the url to play the video 524 public String playbackUrl; // the url to share for users to play video 525 public String thumbnailUrl; // the url of the thumbnail image 526 public String title; 527 public Bitmap bitmap; // cached bitmap of the thumbnail 528 public int lengthInSeconds; 529 public int viewCount; // number of times the video has been viewed 530 public float rating; // ranges from 0.0 to 5.0 531 public Boolean triedToLoadThumbnail; 532 public String authorName; 533 public Time dateAdded; 534 public String category; 535 public String tags; 536 public String description; 537 } 538 } 539 540