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