Skip to content

Commit

Permalink
Merge pull request #105 from w3stling/date-time-parser
Browse files Browse the repository at this point in the history
Support for custom timestamp parser
  • Loading branch information
w3stling authored Aug 9, 2023
2 parents c83972b + 88b4c68 commit a3457d3
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
public abstract class AbstractRssReader<C extends Channel, I extends Item> {
private static final String LOG_GROUP = "com.apptasticsoftware.rssreader";
private final HttpClient httpClient;
private DateTimeParser dateTimeParser = new DateTime();
private String userAgent = "";
private final Map<String, String> headers = new HashMap<>();
private final HashMap<String, BiConsumer<C, String>> channelTags = new HashMap<>();
Expand Down Expand Up @@ -190,6 +191,22 @@ private static <T> void mapNumber(String text, Consumer<T> func, Function<String
}
}

/**
* Date and Time parser for parsing timestamps.
* @param dateTimeParser the date time parser to use.
* @return updated RSSReader.
*/
public AbstractRssReader<C, I> setDateTimeParser(DateTimeParser dateTimeParser) {
Objects.requireNonNull(dateTimeParser, "Date time parser must not be null");

this.dateTimeParser = dateTimeParser;
return this;
}

protected DateTimeParser getDateTimeParser() {
return dateTimeParser;
}

/**
* Sets the user-agent of the HttpClient.
* This is completely optional and if not set then it will not send a user-agent header.
Expand All @@ -204,7 +221,7 @@ public AbstractRssReader<C, I> setUserAgent(String userAgent) {
}

/**
* Adds a header to the HttpClient.
* Adds a http header to the HttpClient.
* This is completely optional and if no headers are set then it will not add anything.
* @param key the key name of the header.
* @param value the value of the header.
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/apptasticsoftware/rssreader/Channel.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public class Channel {
private String docs;
private String rating;
private Image image;
private final DateTimeParser dateTimeParser;

public Channel() {
dateTimeParser = new DateTime();
}

public Channel(DateTimeParser dateTimeParser) {
this.dateTimeParser = dateTimeParser;
}

/**
* Get the name of the channel. It's how people refer to your service. If you have an HTML website that contains the same information as your RSS file, the title of your channel should be the same as the title of your website.
Expand Down Expand Up @@ -221,7 +230,7 @@ public Optional<String> getPubDate() {
* @return publication date
*/
public Optional<ZonedDateTime> getPubDateZonedDateTime() {
return getPubDate().map(DateTime::toZonedDateTime);
return getPubDate().map(dateTimeParser::parse);
}

/**
Expand All @@ -245,7 +254,7 @@ public Optional<String> getLastBuildDate() {
* @return last build date
*/
public Optional<ZonedDateTime> getLastBuildDateZonedDateTime() {
return getLastBuildDate().map(DateTime::toZonedDateTime);
return getLastBuildDate().map(dateTimeParser::parse);
}

/**
Expand Down
21 changes: 16 additions & 5 deletions src/main/java/com/apptasticsoftware/rssreader/DateTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
/**
* Date Time util class for converting date time strings
*/
public final class DateTime {
private static ZoneId defaultZone = ZoneId.of("UTC");
public class DateTime implements DateTimeParser {
public static ZoneId defaultZone = ZoneId.of("UTC");

public static final DateTimeFormatter BASIC_ISO_DATE;
public static final DateTimeFormatter ISO_LOCAL_DATE;
Expand Down Expand Up @@ -136,7 +136,7 @@ public final class DateTime {
RFC_1123_DATE_TIME_SPECIAL_PST_NO_EOW = DateTimeFormatter.ofPattern("d LLL yyyy HH:mm:ss 'PST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-8));
}

private DateTime() {
public DateTime() {

}

Expand Down Expand Up @@ -177,12 +177,14 @@ public static ZonedDateTime toZonedDateTime(String dateTime) {
throw new IllegalArgumentException("Unknown date time format " + dateTime);
}

if (dateTime.length() == 19 || ((dateTime.length() == 29 || dateTime.length() == 32 || dateTime.length() == 35) && dateTime.charAt(10) == 'T') ||
((dateTime.length() == 24 || dateTime.length() == 25) && dateTime.charAt(3) == ',')) {
if (dateTime.length() == 19) {
// Missing time zone information use default time zone. If not setting any default time zone system default
// time zone is used.
LocalDateTime localDateTime = LocalDateTime.parse(dateTime, formatter);
return ZonedDateTime.of(localDateTime, defaultZone);
} else if (((dateTime.length() == 29 || dateTime.length() == 32 || dateTime.length() == 35) && dateTime.charAt(10) == 'T') ||
((dateTime.length() == 24 || dateTime.length() == 25) && dateTime.charAt(3) == ',')) {
return ZonedDateTime.parse(dateTime, formatter);
}

try {
Expand Down Expand Up @@ -387,4 +389,13 @@ public static Comparator<Item> pubDateComparator() {
return Comparator.comparing(i -> i.getPubDate().map(DateTime::toInstant).orElse(Instant.EPOCH));
}

/**
* Converts a timestamp in String format to a ZonedDateTime
* @param timestamp timestamp
* @return ZonedDateTime
*/
@Override
public ZonedDateTime parse(String timestamp) {
return DateTime.toZonedDateTime(timestamp);
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/apptasticsoftware/rssreader/DateTimeParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* MIT License
*
* Copyright (c) 2022, Apptastic Software
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.apptasticsoftware.rssreader;

import java.time.ZonedDateTime;

/**
* For parsing timestamp in channel and items.
*/
public interface DateTimeParser {

/**
* Converts a timestamp in String format to a ZonedDateTime
* @param timestamp timestamp
* @return ZonedDateTime
*/
ZonedDateTime parse(String timestamp);
}
18 changes: 14 additions & 4 deletions src/main/java/com/apptasticsoftware/rssreader/Item.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* to the full story.
*/
public class Item implements Comparable<Item> {
private static final Comparator<Item> DEFAULT_COMPARATOR = ItemComparator.newestItemFirst();
private final Comparator<Item> defaultComparator;
private String title;
private String description;
private String link;
Expand All @@ -48,6 +48,17 @@ public class Item implements Comparable<Item> {
private String comments;
private Enclosure enclosure;
private Channel channel;
private final DateTimeParser dateTimeParser;

public Item() {
dateTimeParser = new DateTime();
defaultComparator = ItemComparator.newestItemFirst();
}

public Item(DateTimeParser dateTimeParser) {
this.dateTimeParser = dateTimeParser;
defaultComparator = ItemComparator.newestItemFirst(dateTimeParser);
}

/**
* Get the title of the item.
Expand Down Expand Up @@ -232,10 +243,9 @@ public void setPubDate(String pubDate) {
* @return publication date
*/
public Optional<ZonedDateTime> getPubDateZonedDateTime() {
return getPubDate().map(DateTime::toZonedDateTime);
return getPubDate().map(dateTimeParser::parse);
}


/**
* Get comments relating to the item.
* @return comments
Expand Down Expand Up @@ -321,6 +331,6 @@ public int hashCode() {
*/
@Override
public int compareTo(Item o) {
return DEFAULT_COMPARATOR.compare(this, o);
return defaultComparator.compare(this, o);
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/apptasticsoftware/rssreader/RssReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ public RssReader(HttpClient httpClient) {

@Override
protected Channel createChannel() {
return new Channel();
return new Channel(getDateTimeParser());
}

@Override
protected Item createItem() {
return new Item();
return new Item(getDateTimeParser());
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.apptasticsoftware.rssreader.module.itunes;

import com.apptasticsoftware.rssreader.Channel;
import com.apptasticsoftware.rssreader.DateTimeParser;

import java.util.*;

Expand All @@ -22,6 +23,10 @@ public class ItunesChannel extends Channel {
private boolean itunesBlock;
private boolean itunesComplete;

public ItunesChannel(DateTimeParser dateTimeParser) {
super(dateTimeParser);
}

/**
* Get the artwork for the show.
* Specify your show artwork by providing a URL linking to it.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.apptasticsoftware.rssreader.module.itunes;

import com.apptasticsoftware.rssreader.DateTimeParser;
import com.apptasticsoftware.rssreader.Item;

import java.time.Duration;
Expand All @@ -22,6 +23,10 @@ public class ItunesItem extends Item {
private String itunesEpisodeType;
private boolean itunesBlock;

public ItunesItem(DateTimeParser dateTimeParser) {
super(dateTimeParser);
}

/**
* Get the duration of an episode.
* @return duration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ protected void registerItemTags() {

@Override
protected ItunesChannel createChannel() {
return new ItunesChannel();
return new ItunesChannel(getDateTimeParser());
}

@Override
protected ItunesItem createItem() {
return new ItunesItem();
return new ItunesItem(getDateTimeParser());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.apptasticsoftware.rssreader.util;

import com.apptasticsoftware.rssreader.DateTime;
import com.apptasticsoftware.rssreader.DateTimeParser;
import com.apptasticsoftware.rssreader.Item;

import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.Objects;

/**
* Comparator for sorting item objects.
Expand All @@ -24,6 +27,17 @@ public static <I extends Item> Comparator<I> oldestItemFirst() {
return Comparator.comparing((I i) -> i.getPubDate().map(DateTime::toInstant).orElse(Instant.EPOCH));
}

/**
* Comparator for sorting Items on publication date in ascending order (oldest first)
* @param <I> any class that extend Item
* @param dateTimeParser date time parser
* @return comparator
*/
public static <I extends Item> Comparator<I> oldestItemFirst(DateTimeParser dateTimeParser) {
Objects.requireNonNull(dateTimeParser, "Date time parser must not be null");
return Comparator.comparing((I i) -> i.getPubDate().map(dateTimeParser::parse).map(ZonedDateTime::toInstant).orElse(Instant.EPOCH));
}

/**
* Comparator for sorting Items on publication date in descending order (newest first)
* @param <I> any class that extend Item
Expand All @@ -33,6 +47,17 @@ public static <I extends Item> Comparator<I> newestItemFirst() {
return Comparator.comparing((I i) -> i.getPubDate().map(DateTime::toInstant).orElse(Instant.EPOCH)).reversed();
}

/**
* Comparator for sorting Items on publication date in descending order (newest first)
* @param <I> any class that extend Item
* @param dateTimeParser date time parser
* @return comparator
*/
public static <I extends Item> Comparator<I> newestItemFirst(DateTimeParser dateTimeParser) {
Objects.requireNonNull(dateTimeParser, "Date time parser must not be null");
return Comparator.comparing((I i) -> i.getPubDate().map(dateTimeParser::parse).map(ZonedDateTime::toInstant).orElse(Instant.EPOCH)).reversed();
}

/**
* Comparator for sorting Items on channel title
* @param <I> any class that extend Item
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ void testBadEnclosureInfo() {
void testMultipleCategories() {
var list = new RssReader().read(fromFile("multiple-categories.xml")).collect(Collectors.toList());

assertTrue(list.size() > 0);
assertFalse(list.isEmpty());
var item = list.get(0);
assertTrue(item.getChannel().getCategories().size() > 1);
assertTrue(item.getChannel().getCategory().isPresent());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ void testSortOldestFirst() throws IOException {
void testSortChannelTitle() throws IOException {

var list = Stream.concat(new RssReader().read("https://lwn.net/headlines/rss"),
new RssReader().read("https://azurecomcdn.azureedge.net/en-us/updates/feed/?updateType=retirements"))
.sorted(ItemComparator.channelTitle())
.collect(Collectors.toList());
new RssReader().read("https://azurecomcdn.azureedge.net/en-us/updates/feed/?updateType=retirements"))
.sorted(ItemComparator.channelTitle())
.collect(Collectors.toList());

var first = list.get(0);
var last = list.get(list.size() - 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,8 @@ void enclosureEqualsTest() {

@Test
void equalsContract() {
EqualsVerifier.simple().forClass(Channel.class).withIgnoredFields("category").withNonnullFields("categories").verify();
EqualsVerifier.simple().forClass(Item.class).withIgnoredFields("category").withNonnullFields("categories").verify();
EqualsVerifier.simple().forClass(Channel.class).withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").verify();
EqualsVerifier.simple().forClass(Item.class).withIgnoredFields("defaultComparator").withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").verify();
EqualsVerifier.simple().forClass(Enclosure.class).verify();
EqualsVerifier.simple().forClass(Image.class).verify();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.apptasticsoftware.rssreader.module.itunes;

import com.apptasticsoftware.rssreader.DateTime;
import com.apptasticsoftware.rssreader.util.ItemComparator;
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -31,14 +32,14 @@ void readItunesPodcastFeed2() throws IOException {

@Test
void equalsContract() {
EqualsVerifier.simple().forClass(ItunesChannel.class).withIgnoredFields("category").withNonnullFields("categories").withNonnullFields("itunesCategories").verify();
EqualsVerifier.simple().forClass(ItunesItem.class).withIgnoredFields("category").withNonnullFields("categories").verify();
EqualsVerifier.simple().forClass(ItunesChannel.class).withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").withNonnullFields("itunesCategories").verify();
EqualsVerifier.simple().forClass(ItunesItem.class).withIgnoredFields("defaultComparator").withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").verify();
EqualsVerifier.simple().forClass(ItunesOwner.class).verify();
}

@Test
void duration() {
ItunesItem item = new ItunesItem();
ItunesItem item = new ItunesItem(new DateTime());
item.setItunesDuration("1");
assertEquals(1, item.getItunesDurationAsDuration().get().getSeconds());
item.setItunesDuration("01:02");
Expand All @@ -49,7 +50,7 @@ void duration() {

@Test
void badDuration() {
ItunesItem item = new ItunesItem();
ItunesItem item = new ItunesItem(new DateTime());
item.setItunesDuration(null);
assertTrue(item.getItunesDurationAsDuration().isEmpty());
item.setItunesDuration(" ");
Expand Down

0 comments on commit a3457d3

Please sign in to comment.