xref: /aosp_15_r20/frameworks/base/sax/tests/saxtests/src/android/sax/SafeSaxTest.java (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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