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