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