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 android.sax; 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.util.Log; 30 import android.util.Xml; 31 import com.android.internal.util.XmlUtils; 32 import org.xml.sax.Attributes; 33 import org.xml.sax.ContentHandler; 34 import org.xml.sax.SAXException; 35 import org.xml.sax.helpers.DefaultHandler; 36 37 import java.io.ByteArrayInputStream; 38 import java.io.ByteArrayOutputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.time.Instant; 42 43 import com.android.frameworks.saxtests.R; 44 45 public 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 start(Attributes attributes)58 public void start(Attributes attributes) { 59 starts++; 60 } 61 end()62 public void end() { 63 ends++; 64 } 65 } 66 67 private static class TextElementCounter implements TextElementListener { 68 int starts = 0; 69 String bodies = ""; 70 start(Attributes attributes)71 public void start(Attributes attributes) { 72 starts++; 73 } 74 end(String body)75 public void end(String body) { 76 this.bodies += body; 77 } 78 } 79 80 @SmallTest testListener()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 testMissingRequiredChild()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 testMixedContent()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 testPerformance()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 pureSaxTest(InputStream inputStream)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 saxyModelTest(InputStream inputStream)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 saxyModelTest(InputStream inputStream, ContentHandler contentHandler)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 { addVideo(YouTubeVideo video)197 public void addVideo(YouTubeVideo video) { 198 } 199 } 200 newContentHandler(VideoAdapter videoAdapter)201 private static ContentHandler newContentHandler(VideoAdapter videoAdapter) { 202 return new HandlerFactory().newContentHandler(videoAdapter); 203 } 204 205 private static class HandlerFactory { 206 YouTubeVideo video; 207 newContentHandler(VideoAdapter videoAdapter)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 = Instant.parse(body); 229 } 230 }); 231 232 Element author = entry.getChild(ATOM_NAMESPACE, "author"); 233 author.getChild(ATOM_NAMESPACE, "name") 234 .setEndTextElementListener(new EndTextElementListener() { 235 public void end(String body) { 236 video.authorName = body; 237 } 238 }); 239 240 Element mediaGroup = entry.getChild(MEDIA_NAMESPACE, "group"); 241 242 mediaGroup.getChild(MEDIA_NAMESPACE, "thumbnail") 243 .setStartElementListener(new StartElementListener() { 244 public void start(Attributes attributes) { 245 String url = attributes.getValue("", "url"); 246 if (video.thumbnailUrl == null && url.length() > 0) { 247 video.thumbnailUrl = url; 248 } 249 } 250 }); 251 252 mediaGroup.getChild(MEDIA_NAMESPACE, "content") 253 .setStartElementListener(new StartElementListener() { 254 public void start(Attributes attributes) { 255 String url = attributes.getValue("", "url"); 256 if (url != null) { 257 video.videoUrl = url; 258 } 259 } 260 }); 261 262 mediaGroup.getChild(MEDIA_NAMESPACE, "player") 263 .setStartElementListener(new StartElementListener() { 264 public void start(Attributes attributes) { 265 String url = attributes.getValue("", "url"); 266 if (url != null) { 267 video.playbackUrl = url; 268 } 269 } 270 }); 271 272 mediaGroup.getChild(MEDIA_NAMESPACE, "title") 273 .setEndTextElementListener(new EndTextElementListener() { 274 public void end(String body) { 275 video.title = body; 276 } 277 }); 278 279 mediaGroup.getChild(MEDIA_NAMESPACE, "category") 280 .setEndTextElementListener(new EndTextElementListener() { 281 public void end(String body) { 282 video.category = body; 283 } 284 }); 285 286 mediaGroup.getChild(MEDIA_NAMESPACE, "description") 287 .setEndTextElementListener(new EndTextElementListener() { 288 public void end(String body) { 289 video.description = body; 290 } 291 }); 292 293 mediaGroup.getChild(MEDIA_NAMESPACE, "keywords") 294 .setEndTextElementListener(new EndTextElementListener() { 295 public void end(String body) { 296 video.tags = body; 297 } 298 }); 299 300 mediaGroup.getChild(YOUTUBE_NAMESPACE, "duration") 301 .setStartElementListener(new StartElementListener() { 302 public void start(Attributes attributes) { 303 String seconds = attributes.getValue("", "seconds"); 304 video.lengthInSeconds 305 = XmlUtils.convertValueToInt(seconds, 0); 306 } 307 }); 308 309 mediaGroup.getChild(YOUTUBE_NAMESPACE, "statistics") 310 .setStartElementListener(new StartElementListener() { 311 public void start(Attributes attributes) { 312 String viewCount = attributes.getValue("", "viewCount"); 313 video.viewCount 314 = XmlUtils.convertValueToInt(viewCount, 0); 315 } 316 }); 317 318 entry.getChild(GDATA_NAMESPACE, "rating") 319 .setStartElementListener(new StartElementListener() { 320 public void start(Attributes attributes) { 321 String average = attributes.getValue("", "average"); 322 video.rating = average == null 323 ? 0.0f : Float.parseFloat(average); 324 } 325 }); 326 327 return root.getContentHandler(); 328 } 329 330 class VideoListener implements ElementListener { 331 332 final VideoAdapter videoAdapter; 333 VideoListener(VideoAdapter videoAdapter)334 public VideoListener(VideoAdapter videoAdapter) { 335 this.videoAdapter = videoAdapter; 336 } 337 start(Attributes attributes)338 public void start(Attributes attributes) { 339 video = new YouTubeVideo(); 340 } 341 end()342 public void end() { 343 videoAdapter.addVideo(video); 344 video = null; 345 } 346 } 347 } 348 349 private static class YouTubeContentHandler extends DefaultHandler { 350 351 final VideoAdapter videoAdapter; 352 353 YouTubeVideo video = null; 354 StringBuilder builder = null; 355 YouTubeContentHandler(VideoAdapter videoAdapter)356 public YouTubeContentHandler(VideoAdapter videoAdapter) { 357 this.videoAdapter = videoAdapter; 358 } 359 360 @Override startElement(String uri, String localName, String qName, Attributes attributes)361 public void startElement(String uri, String localName, String qName, 362 Attributes attributes) throws SAXException { 363 if (uri.equals(ATOM_NAMESPACE)) { 364 if (localName.equals("entry")) { 365 video = new YouTubeVideo(); 366 return; 367 } 368 369 if (video == null) { 370 return; 371 } 372 373 if (!localName.equals("id") 374 && !localName.equals("published") 375 && !localName.equals("name")) { 376 return; 377 } 378 this.builder = new StringBuilder(); 379 return; 380 381 } 382 383 if (video == null) { 384 return; 385 } 386 387 if (uri.equals(MEDIA_NAMESPACE)) { 388 if (localName.equals("thumbnail")) { 389 String url = attributes.getValue("", "url"); 390 if (video.thumbnailUrl == null && url.length() > 0) { 391 video.thumbnailUrl = url; 392 } 393 return; 394 } 395 396 if (localName.equals("content")) { 397 String url = attributes.getValue("", "url"); 398 if (url != null) { 399 video.videoUrl = url; 400 } 401 return; 402 } 403 404 if (localName.equals("player")) { 405 String url = attributes.getValue("", "url"); 406 if (url != null) { 407 video.playbackUrl = url; 408 } 409 return; 410 } 411 412 if (localName.equals("title") 413 || localName.equals("category") 414 || localName.equals("description") 415 || localName.equals("keywords")) { 416 this.builder = new StringBuilder(); 417 return; 418 } 419 420 return; 421 } 422 423 if (uri.equals(YOUTUBE_NAMESPACE)) { 424 if (localName.equals("duration")) { 425 video.lengthInSeconds = XmlUtils.convertValueToInt( 426 attributes.getValue("", "seconds"), 0); 427 return; 428 } 429 430 if (localName.equals("statistics")) { 431 video.viewCount = XmlUtils.convertValueToInt( 432 attributes.getValue("", "viewCount"), 0); 433 return; 434 } 435 436 return; 437 } 438 439 if (uri.equals(GDATA_NAMESPACE)) { 440 if (localName.equals("rating")) { 441 String average = attributes.getValue("", "average"); 442 video.rating = average == null 443 ? 0.0f : Float.parseFloat(average); 444 } 445 } 446 } 447 448 @Override characters(char text[], int start, int length)449 public void characters(char text[], int start, int length) 450 throws SAXException { 451 if (builder != null) { 452 builder.append(text, start, length); 453 } 454 } 455 takeText()456 String takeText() { 457 try { 458 return builder.toString(); 459 } finally { 460 builder = null; 461 } 462 } 463 464 @Override endElement(String uri, String localName, String qName)465 public void endElement(String uri, String localName, String qName) 466 throws SAXException { 467 if (video == null) { 468 return; 469 } 470 471 if (uri.equals(ATOM_NAMESPACE)) { 472 if (localName.equals("published")) { 473 // TODO(tomtaylor): programmatically get the timezone 474 video.dateAdded = Instant.parse(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 Instant dateAdded; 534 public String category; 535 public String tags; 536 public String description; 537 } 538 } 539 540