Fallback to current date time if a RSS2 item doesn't have any date

This commit is contained in:
Shinokuni 2020-10-01 21:17:51 +02:00
parent e41ab29264
commit 10a7b99e59
3 changed files with 75 additions and 53 deletions

View File

@ -6,6 +6,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.readrops.api.utils.DateUtils
import com.readrops.api.utils.ParseException
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@ -37,7 +38,7 @@ class RSS2ItemsAdapterTest {
@Test
fun otherNamespacesTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_other_namespaces.xml")
val item = adapter.fromXml(stream)[0]
val item = adapter.fromXml(stream).first()
assertEquals(item.guid, "guid")
assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4")
@ -45,28 +46,30 @@ class RSS2ItemsAdapterTest {
assertEquals(item.content, "content:encoded")
}
@Test
fun noDateTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml")
val item = adapter.fromXml(stream).first()
assertNotNull(item.pubDate)
}
@Test
fun noTitleTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml")
Assert.assertThrows("Item title is required", ParseException::class.java) { adapter.fromXml(stream) }
Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
}
@Test
fun noLinkTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml")
Assert.assertThrows("Item link is required", ParseException::class.java) { adapter.fromXml(stream) }
}
@Test
fun noDateTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml")
Assert.assertThrows("Item date is required", ParseException::class.java) { adapter.fromXml(stream) }
Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
}
@Test
fun enclosureTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_enclosure.xml")
val item = adapter.fromXml(stream)[0]
val item = adapter.fromXml(stream).first()
assertEquals(item.imageLink, "https://image1.jpg")
}
@ -74,7 +77,7 @@ class RSS2ItemsAdapterTest {
@Test
fun mediaContentTest() {
val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_content.xml")
val item = adapter.fromXml(stream)[0]
val item = adapter.fromXml(stream).first()
assertEquals(item.imageLink, "https://image2.jpg")
}

View File

@ -5,20 +5,19 @@ import com.readrops.api.localfeed.XmlAdapter
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
import com.readrops.api.utils.*
import com.readrops.db.entities.Item
import org.joda.time.LocalDateTime
import java.io.InputStream
class RSS2ItemsAdapter : XmlAdapter<List<Item>> {
override fun fromXml(inputStream: InputStream): List<Item> {
val konsume = inputStream.konsumeXml()
val konsumer = inputStream.konsumeXml()
val items = mutableListOf<Item>()
return try {
konsume.child("rss") {
konsumer.child("rss") {
child("channel") {
allChildrenAutoIgnore("item") {
val enclosures = arrayListOf<String>()
val mediaContents = arrayListOf<String>()
val creators = arrayListOf<String?>()
val item = Item().apply {
@ -28,67 +27,71 @@ class RSS2ItemsAdapter : XmlAdapter<List<Item>> {
"link" -> link = nonNullText()
"author" -> author = nullableText()
"dc:creator" -> creators += nullableText()
"pubDate" -> pubDate = DateUtils.parse(nonNullText())
"dc:date" -> pubDate = DateUtils.parse(nonNullText())
"pubDate" -> pubDate = DateUtils.parse(nullableText())
"dc:date" -> pubDate = DateUtils.parse(nullableText())
"guid" -> guid = nullableText()
"description" -> description = nullableTextRecursively()
"content:encoded" -> content = nullableTextRecursively()
"enclosure" -> parseEnclosure(this, enclosures)
"media:content" -> parseMediaContent(this, mediaContents)
"media:group" -> parseMediaGroup(this, mediaContents)
"enclosure" -> parseEnclosure(this, item = this@apply)
"media:content" -> parseMediaContent(this, item = this@apply)
"media:group" -> parseMediaGroup(this, item = this@apply)
else -> skipContents() // for example media:description
}
}
}
validateItem(item)
if (item.guid == null) item.guid = item.link
if (item.author == null && creators.filterNotNull().isNotEmpty())
item.author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
if (enclosures.isNotEmpty()) item.imageLink = enclosures.first()
else if (mediaContents.isNotEmpty()) item.imageLink = mediaContents.first()
finalizeItem(item, creators)
items += item
}
}
}
konsume.close()
konsumer.close()
items
} catch (e: KonsumerException) {
throw ParseException(e.message)
}
}
private fun parseEnclosure(konsume: Konsumer, enclosures: MutableList<String>) {
if (konsume.attributes.getValueOpt("type") != null
&& LibUtils.isMimeImage(konsume.attributes["type"]))
enclosures += konsume.attributes["url"]
private fun parseEnclosure(konsumer: Konsumer, item: Item) {
if (konsumer.attributes.getValueOpt("type") != null
&& LibUtils.isMimeImage(konsumer.attributes["type"]) && item.imageLink == null)
item.imageLink = konsumer.attributes.getValueOpt("url")
}
private fun parseMediaContent(konsume: Konsumer, mediaContents: MutableList<String>) {
if (konsume.attributes.getValueOpt("medium") != null
&& LibUtils.isMimeImage(konsume.attributes["medium"]))
mediaContents += konsume.attributes["url"]
private fun parseMediaContent(konsumer: Konsumer, item: Item) {
if (konsumer.attributes.getValueOpt("medium") != null
&& LibUtils.isMimeImage(konsumer.attributes["medium"]) && item.imageLink == null)
item.imageLink = konsumer.attributes.getValueOpt("url")
konsume.skipContents() // ignore media content sub elements
konsumer.skipContents() // ignore media content sub elements
}
private fun parseMediaGroup(konsume: Konsumer, mediaContents: MutableList<String>) {
konsume.allChildrenAutoIgnore("content") {
private fun parseMediaGroup(konsumer: Konsumer, item: Item) {
konsumer.allChildrenAutoIgnore("content") {
when (tagName) {
"media:content" -> parseMediaContent(this, mediaContents)
"media:content" -> parseMediaContent(this, item)
else -> skipContents()
}
}
}
private fun finalizeItem(item: Item, creators: List<String?>) {
item.apply {
validateItem(this)
if (pubDate == null) pubDate = LocalDateTime.now()
if (guid == null) guid = link
if (author == null && creators.filterNotNull().isNotEmpty())
author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
}
}
private fun validateItem(item: Item) {
when {
item.title == null -> throw ParseException("Item title is required")
item.link == null -> throw ParseException("Item link is required")
item.pubDate == null -> throw ParseException("Item date is required")
}
}

View File

@ -1,5 +1,9 @@
package com.readrops.api.utils;
import android.util.Log;
import androidx.annotation.Nullable;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
@ -9,6 +13,8 @@ import java.util.Locale;
public final class DateUtils {
private static final String TAG = DateUtils.class.getSimpleName();
/**
* Base of common RSS 2 date formats.
* Examples :
@ -30,20 +36,30 @@ public final class DateUtils {
*/
private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
@Nullable
public static LocalDateTime parse(String value) {
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
.toFormatter()
.withLocale(Locale.ENGLISH)
.withOffsetParsed();
if (value == null) {
return null;
}
return formatter.parseLocalDateTime(value);
try {
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
.toFormatter()
.withLocale(Locale.ENGLISH)
.withOffsetParsed();
return formatter.parseLocalDateTime(value);
} catch (Exception e) {
Log.d(TAG, e.getMessage());
return null;
}
}
public static String formattedDateByLocal(LocalDateTime dateTime) {