diff --git a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java index 0ac1b7ae2..5ed9ff2b0 100644 --- a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java +++ b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java @@ -17,7 +17,7 @@ import java.io.Reader; public class TypeGetter { private static final String TAG = "TypeGetter"; - enum Type { + public enum Type { RSS20, RSS091, ATOM, INVALID } diff --git a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java index 2c1cff914..3138f087a 100644 --- a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java +++ b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java @@ -79,7 +79,7 @@ public class SyndDateUtils { second = date.substring(date.indexOf("-")); } } - + date = first + second; } if (isLocal) { @@ -131,4 +131,23 @@ public class SyndDateUtils { result += (Float.valueOf(parts[idx])) * 1000L; return result; } + + public static String formatRFC822Date(Date date) { + SimpleDateFormat format = RFC822Formatter.get(); + return format.format(date); + } + + public static String formatRFC3339Local(Date date) { + SimpleDateFormat format = RFC3339Formatter.get(); + format.applyPattern(RFC3339LOCAL); + String result = format.format(date); + format.applyPattern(RFC3339UTC); + return result; + } + + public static String formatRFC3339UTC(Date date) { + SimpleDateFormat format = RFC3339Formatter.get(); + format.applyPattern(RFC3339UTC); + return format.format(date); + } } diff --git a/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java b/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java index 95c3a3dba..5489002bc 100644 --- a/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java +++ b/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java @@ -1,170 +1,168 @@ package instrumentationTest.de.test.antennapod.syndication.handler; -import java.io.BufferedOutputStream; +import android.content.Context; +import android.test.InstrumentationTestCase; +import de.danoeh.antennapod.feed.*; +import de.danoeh.antennapod.syndication.handler.FeedHandler; +import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; +import instrumentationTest.de.test.antennapod.util.syndication.feedgenerator.FeedGenerator; +import instrumentationTest.de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.Date; +import java.util.List; -import android.test.AndroidTestCase; -import android.util.Log; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.syndication.handler.FeedHandler; +/** + * Tests for FeedHandler + */ +public class FeedHandlerTest extends InstrumentationTestCase { + private static final String FEEDS_DIR = "testfeeds"; -/** Enqueues a list of Feeds and tests if they are parsed correctly */ -public class FeedHandlerTest extends AndroidTestCase { - private static final String TAG = "FeedHandlerTest"; - private static final String FEEDS_DIR = "testfeeds"; + File file = null; + OutputStream outputStream = null; - private ArrayList feeds; + protected void setUp() throws Exception { + super.setUp(); + Context context = getInstrumentation().getContext(); + File destDir = context.getExternalFilesDir(FEEDS_DIR); + assertNotNull(destDir); - protected void setUp() throws Exception { - super.setUp(); - feeds = new ArrayList(); - for (int i = 0; i < TestFeeds.urls.length; i++) { - Feed f = new Feed(TestFeeds.urls[i], new Date()); - f.setFile_url(new File(getContext().getExternalFilesDir(FEEDS_DIR) - .getAbsolutePath(), "R" + i).getAbsolutePath()); - feeds.add(f); - } - } + file = new File(destDir, "feed.xml"); + file.delete(); - private InputStream getInputStream(String url) - throws MalformedURLException, IOException { - HttpURLConnection connection = (HttpURLConnection) (new URL(url)) - .openConnection(); - int rc = connection.getResponseCode(); - if (rc == HttpURLConnection.HTTP_OK) { - return connection.getInputStream(); - } else { - return null; - } - } + assertNotNull(file); + assertFalse(file.exists()); - private boolean downloadFeed(Feed feed) throws IOException { - int num_retries = 20; - boolean successful = false; + outputStream = new FileOutputStream(file); + } - for (int i = 0; i < num_retries; i++) { - InputStream in = null; - BufferedOutputStream out = null; - try { - in = getInputStream(feed.getDownload_url()); - if (in == null) { - return false; - } - out = new BufferedOutputStream(new FileOutputStream( - feed.getFile_url())); - byte[] buffer = new byte[8 * 1024]; - int count = 0; - while ((count = in.read(buffer)) != -1) { - out.write(buffer, 0, count); - } - out.flush(); - successful = true; - return true; - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (in != null) { - in.close(); - } - if (out != null) { - out.close(); - } - if (successful) { - break; - } - } - } - if (!successful) { - Log.e(TAG, "Download failed after " + num_retries + " retries"); - throw new IOException(); - } else { - return true; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + file.delete(); + file = null; + + outputStream.close(); + outputStream = null; + } + + private Feed runFeedTest(Feed feed, FeedGenerator g, String encoding, long flags) throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { + g.writeFeed(feed, outputStream, encoding, flags); + FeedHandler handler = new FeedHandler(); + Feed parsedFeed = new Feed(feed.getDownload_url(), feed.getLastUpdate()); + parsedFeed.setFile_url(file.getAbsolutePath()); + parsedFeed.setDownloaded(true); + handler.parseFeed(parsedFeed); + return parsedFeed; + } + + private void feedValid(Feed feed, Feed parsedFeed, String feedType) { + assertEquals(feed.getTitle(), parsedFeed.getTitle()); + if (feedType.equals(Feed.TYPE_ATOM1)) { + assertEquals(feed.getFeedIdentifier(), parsedFeed.getFeedIdentifier()); + assertEquals(feed.getAuthor(), parsedFeed.getAuthor()); } - } + assertEquals(feed.getLink(), parsedFeed.getLink()); + assertEquals(feed.getDescription(), parsedFeed.getDescription()); + assertEquals(feed.getLanguage(), parsedFeed.getLanguage()); + assertEquals(feed.getPaymentLink(), parsedFeed.getPaymentLink()); - private boolean isFeedValid(Feed feed) { - Log.i(TAG, "Checking if " + feed.getDownload_url() + " is valid"); - boolean result = false; - if (feed.getTitle() == null) { - Log.e(TAG, "Feed has no title"); - return false; - } - if (!hasValidFeedItems(feed)) { - Log.e(TAG, "Feed has invalid items"); - return false; - } - if (feed.getLink() == null) { - Log.e(TAG, "Feed has no link"); - return false; - } - if (feed.getLink() != null && feed.getLink().length() == 0) { - Log.e(TAG, "Feed has empty link"); - return false; - } - if (feed.getIdentifyingValue() == null) { - Log.e(TAG, "Feed has no identifying value"); - return false; - } - if (feed.getIdentifyingValue() != null - && feed.getIdentifyingValue().length() == 0) { - Log.e(TAG, "Feed has empty identifying value"); - return false; - } - return true; - } + if (feed.getImage() != null) { + FeedImage image = feed.getImage(); + FeedImage parsedImage = parsedFeed.getImage(); + assertNotNull(parsedImage); - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItems()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - } - return true; - } + assertEquals(image.getTitle(), parsedImage.getTitle()); + assertEquals(image.getDownload_url(), parsedImage.getDownload_url()); + } - public void testParseFeeds() { - Log.i(TAG, "Testing RSS feeds"); - while (!feeds.isEmpty()) { - Feed feed = feeds.get(0); - parseFeed(feed); - feeds.remove(0); - } + if (feed.getItems() != null) { + assertNotNull(parsedFeed.getItems()); + assertEquals(feed.getItems().size(), parsedFeed.getItems().size()); - Log.i(TAG, "RSS Test completed"); - } + for (int i = 0; i < feed.getItems().size(); i++) { + FeedItem item = feed.getItems().get(i); + FeedItem parsedItem = parsedFeed.getItems().get(i); - private void parseFeed(Feed feed) { - try { - Log.i(TAG, "Testing feed with url " + feed.getDownload_url()); - FeedHandler handler = new FeedHandler(); - if (downloadFeed(feed)) { - handler.parseFeed(feed); - assertTrue(isFeedValid(feed)); + if (item.getItemIdentifier() != null) + assertEquals(item.getItemIdentifier(), parsedItem.getItemIdentifier()); + assertEquals(item.getTitle(), parsedItem.getTitle()); + assertEquals(item.getDescription(), parsedItem.getDescription()); + assertEquals(item.getContentEncoded(), parsedItem.getContentEncoded()); + assertEquals(item.getLink(), parsedItem.getLink()); + assertEquals(item.getPubDate().getTime(), parsedItem.getPubDate().getTime()); + assertEquals(item.getPaymentLink(), parsedItem.getPaymentLink()); + + if (item.hasMedia()) { + assertTrue(parsedItem.hasMedia()); + FeedMedia media = item.getMedia(); + FeedMedia parsedMedia = parsedItem.getMedia(); + + assertEquals(media.getDownload_url(), parsedMedia.getDownload_url()); + assertEquals(media.getSize(), parsedMedia.getSize()); + assertEquals(media.getMime_type(), parsedMedia.getMime_type()); + } + + if (item.hasItemImage()) { + assertTrue(parsedItem.hasItemImage()); + FeedImage image = item.getImage(); + FeedImage parsedImage = parsedItem.getImage(); + + assertEquals(image.getTitle(), parsedImage.getTitle()); + assertEquals(image.getDownload_url(), parsedImage.getDownload_url()); + } + + if (item.getChapters() != null) { + assertNotNull(parsedItem.getChapters()); + assertEquals(item.getChapters().size(), parsedItem.getChapters().size()); + List chapters = item.getChapters(); + List parsedChapters = parsedItem.getChapters(); + for (int j = 0; j < chapters.size(); j++) { + Chapter chapter = chapters.get(j); + Chapter parsedChapter = parsedChapters.get(j); + + assertEquals(chapter.getTitle(), parsedChapter.getTitle()); + assertEquals(chapter.getLink(), parsedChapter.getLink()); + } + } } - } catch (Exception e) { - Log.e(TAG, "Error when trying to test " + feed.getDownload_url()); - e.printStackTrace(); - } - } + } + } - @Override - protected void tearDown() throws Exception { - super.tearDown(); - for (Feed feed : feeds) { - File f = new File(feed.getFile_url()); - f.delete(); - } - } + public void testRSS2Basic() throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { + Feed f1 = createTestFeed(10, false, true, true); + Feed f2 = runFeedTest(f1, new RSS2Generator(), "UTF-8", RSS2Generator.FEATURE_WRITE_GUID); + feedValid(f1, f2, Feed.TYPE_RSS2); + } + + private Feed createTestFeed(int numItems, boolean withImage, boolean withFeedMedia, boolean withChapters) { + FeedImage image = null; + if (withImage) { + image = new FeedImage(0, "image", null, "http://example.com/picture", false); + } + Feed feed = new Feed(0, new Date(), "title", "http://example.com", "This is the description", + "http://example.com/payment", "Daniel", "en", null, "http://example.com/feed", image, file.getAbsolutePath(), + "http://example.com/feed", true); + feed.setItems(new ArrayList()); + + for (int i = 0; i < numItems; i++) { + FeedItem item = new FeedItem(0, "item-" + i, "http://example.com/item-" + i, + "http://example.com/items/" + i, new Date(i), false, feed); + feed.getItems().add(item); + if (withFeedMedia) { + item.setMedia(new FeedMedia(0, item, 4711, 0, 100, "audio/mp3", null, "http://example.com/media-" + i, + false, null, 0)); + } + } + + return feed; + } } diff --git a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java new file mode 100644 index 000000000..a47321427 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java @@ -0,0 +1,106 @@ +package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator; + +import android.util.Xml; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.syndication.util.SyndDateUtils; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Creates Atom feeds. See FeedGenerator for more information. + */ +public class AtomGenerator implements FeedGenerator{ + + private static final String NS_ATOM = "http://www.w3.org/2005/Atom"; + + public static final long FEATURE_USE_RFC3339LOCAL = 1; + + @Override + public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException { + if (feed == null) throw new IllegalArgumentException("feed = null"); + if (outputStream == null) throw new IllegalArgumentException("outputStream = null"); + if (encoding == null) throw new IllegalArgumentException("encoding = null"); + + XmlSerializer xml = Xml.newSerializer(); + xml.setOutput(outputStream, encoding); + xml.startDocument(encoding, null); + + xml.startTag(null, "feed"); + xml.attribute(null, "xmlns", NS_ATOM); + + // Write Feed data + if (feed.getIdentifyingValue() != null) { + xml.startTag(null, "id"); + xml.text(feed.getIdentifyingValue()); + xml.endTag(null, "id"); + } + if (feed.getTitle() != null) { + xml.startTag(null, "title"); + xml.text(feed.getTitle()); + xml.endTag(null, "title"); + } + if (feed.getLink() != null) { + xml.startTag(null, "link"); + xml.attribute(null, "rel", "alternate"); + xml.attribute(null, "href", feed.getLink()); + xml.endTag(null, "link"); + } + if (feed.getDescription() != null) { + xml.startTag(null, "subtitle"); + xml.text(feed.getDescription()); + xml.endTag(null, "subtitle"); + } + + // Write FeedItem data + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + + if (item.getIdentifyingValue() != null) { + xml.startTag(null, "id"); + xml.text(item.getIdentifyingValue()); + xml.endTag(null, "id"); + } + if (item.getTitle() != null) { + xml.startTag(null, "title"); + xml.text(item.getTitle()); + xml.endTag(null, "title"); + } + if (item.getLink() != null) { + xml.startTag(null, "link"); + xml.attribute(null, "rel", "alternate"); + xml.attribute(null, "href", item.getLink()); + xml.endTag(null, "link"); + } + if (item.getPubDate() != null) { + xml.startTag(null, "published"); + if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) { + xml.text(SyndDateUtils.formatRFC3339Local(item.getPubDate())); + } else { + xml.text(SyndDateUtils.formatRFC3339UTC(item.getPubDate())); + } + } + if (item.getDescription() != null) { + xml.startTag(null, "content"); + xml.text(item.getDescription()); + xml.endTag(null, "content"); + } + if (item.getMedia() != null) { + FeedMedia media = item.getMedia(); + xml.startTag(null, "link"); + xml.attribute(null, "rel", "enclosure"); + xml.attribute(null, "href", media.getDownload_url()); + xml.attribute(null, "type", media.getMime_type()); + xml.attribute(null, "length", String.valueOf(media.getSize())); + xml.endTag(null, "link"); + } + } + } + + xml.endTag(null, "feed"); + xml.endDocument(); + } +} diff --git a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java new file mode 100644 index 000000000..e6b0f0697 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/FeedGenerator.java @@ -0,0 +1,28 @@ +package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator; + +import de.danoeh.antennapod.feed.Feed; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Generates a machine-readable, platform-independent representation of a Feed object. + */ +public interface FeedGenerator { + + /** + * Creates a machine-readable, platform-independent representation of a given + * Feed object and writes it to the given OutputStream. + *

+ * The representation might not be compliant with its specification if the feed + * is missing certain attribute values. This is intentional because the FeedGenerator is + * used for creating test data. + * + * @param feed The feed that should be written. Must not be null. + * @param outputStream The output target that the feed will be written to. The outputStream is not closed after + * the method's execution Must not be null. + * @param encoding The encoding to use. Must not be null. + * @param flags Optional argument for enabling implementation-dependent features. + */ + public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException; +} diff --git a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java new file mode 100644 index 000000000..0742f8d78 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/GeneratorUtil.java @@ -0,0 +1,20 @@ +package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; + +/** + * Utility methods for FeedGenerator + */ +public class GeneratorUtil { + + public static void addPaymentLink(XmlSerializer xml, String paymentLink) throws IOException { + xml.startTag("http://www.w3.org/2005/Atom", "link"); + xml.attribute(null, "rel", "payment"); + xml.attribute(null, "title", "Flattr this!"); + xml.attribute(null, "href", paymentLink); + xml.attribute(null, "type", "text/html"); + xml.endTag("http://www.w3.org/2005/Atom", "link"); + } +} diff --git a/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java new file mode 100644 index 000000000..aea995e0f --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/util/syndication/feedgenerator/RSS2Generator.java @@ -0,0 +1,110 @@ +package instrumentationTest.de.test.antennapod.util.syndication.feedgenerator; + +import android.util.Xml; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.syndication.util.SyndDateUtils; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Creates RSS 2.0 feeds. See FeedGenerator for more information. + */ +public class RSS2Generator implements FeedGenerator{ + + public static final long FEATURE_WRITE_GUID = 1; + + @Override + public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException { + if (feed == null) throw new IllegalArgumentException("feed = null"); + if (outputStream == null) throw new IllegalArgumentException("outputStream = null"); + if (encoding == null) throw new IllegalArgumentException("encoding = null"); + + XmlSerializer xml = Xml.newSerializer(); + xml.setOutput(outputStream, encoding); + xml.startDocument(encoding, null); + + xml.setPrefix("atom", "http://www.w3.org/2005/Atom"); + xml.startTag(null, "rss"); + xml.attribute(null, "version", "2.0"); + xml.startTag(null, "channel"); + + // Write Feed data + if (feed.getTitle() != null) { + xml.startTag(null, "title"); + xml.text(feed.getTitle()); + xml.endTag(null, "title"); + } + if (feed.getDescription() != null) { + xml.startTag(null, "description"); + xml.text(feed.getDescription()); + xml.endTag(null, "description"); + } + if (feed.getLink() != null) { + xml.startTag(null, "link"); + xml.text(feed.getLink()); + xml.endTag(null, "link"); + } + if (feed.getLanguage() != null) { + xml.startTag(null, "language"); + xml.text(feed.getLanguage()); + xml.endTag(null, "language"); + } + + if (feed.getPaymentLink() != null) { + GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink()); + } + + // Write FeedItem data + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + xml.startTag(null, "item"); + + if (item.getTitle() != null) { + xml.startTag(null, "title"); + xml.text(item.getTitle()); + xml.endTag(null, "title"); + } + if (item.getDescription() != null) { + xml.startTag(null, "description"); + xml.text(item.getDescription()); + xml.endTag(null, "description"); + } + if (item.getLink() != null) { + xml.startTag(null, "link"); + xml.text(item.getLink()); + xml.endTag(null, "link"); + } + if (item.getPubDate() != null) { + xml.startTag(null, "pubDate"); + xml.text(SyndDateUtils.formatRFC822Date(item.getPubDate())); + xml.endTag(null, "pubDate"); + } + if ((flags & FEATURE_WRITE_GUID) != 0) { + xml.startTag(null, "guid"); + xml.text(item.getItemIdentifier()); + xml.endTag(null, "guid"); + } + if (item.getMedia() != null) { + xml.startTag(null, "enclosure"); + xml.attribute(null, "url", item.getMedia().getDownload_url()); + xml.attribute(null, "length", String.valueOf(item.getMedia().getSize())); + xml.attribute(null, "type", item.getMedia().getMime_type()); + xml.endTag(null, "enclosure"); + } + if (item.getPaymentLink() != null) { + GeneratorUtil.addPaymentLink(xml, item.getPaymentLink()); + } + + xml.endTag(null, "item"); + } + } + + xml.endTag(null, "channel"); + xml.endTag(null, "rss"); + + xml.endDocument(); + } +}