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