diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/georss/GeoRssExtensions.java b/src/main/java/com/apptasticsoftware/rssreader/module/georss/GeoRssExtensions.java index 17215e69..a66f3013 100644 --- a/src/main/java/com/apptasticsoftware/rssreader/module/georss/GeoRssExtensions.java +++ b/src/main/java/com/apptasticsoftware/rssreader/module/georss/GeoRssExtensions.java @@ -65,7 +65,7 @@ private static void itemTagExtensions(FeedExtensionRegistry mapDouble(value, item::setGeoRssElevation)); - registry.addItemExtension("georss:floor", (item, value) -> mapInteger(value, item::setGeoRssFloor)); + registry.addItemExtension("georss:floor", (item, value) -> mapInteger(value, item::setGeoRssFloor)); registry.addItemExtension("georss:radius", (item, value) -> mapDouble(value, item::setGeoRssRadius)); registry.addItemExtension("georss:featuretypetag", GeoRssItem::setGeoRssFeatureTypeTag); registry.addItemExtension("georss:relationshiptag", GeoRssItem::setGeoRssRelationshipTag); diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashChannel.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashChannel.java new file mode 100644 index 00000000..3102a798 --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashChannel.java @@ -0,0 +1,13 @@ +package com.apptasticsoftware.rssreader.module.slash; + +import com.apptasticsoftware.rssreader.Channel; + +/** + * Slash RSS module channel interface extending the core Channel with Slash-specific metadata. + * Slash is a module for RSS feeds from Slash-based sites (like Slashdot). + * + * @see SlashItem for item-level Slash extensions + * @see Slash RSS Module Specification + */ +public interface SlashChannel extends Channel { +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashExtensions.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashExtensions.java new file mode 100644 index 00000000..b909e328 --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashExtensions.java @@ -0,0 +1,38 @@ +package com.apptasticsoftware.rssreader.module.slash; + +import com.apptasticsoftware.rssreader.FeedExtensionRegistry; + +import static com.apptasticsoftware.rssreader.util.Mapper.mapInteger; + +/** + * Utility class for registering Slash RSS module extensions. + * Registers handlers for Slash-specific XML elements during RSS feed parsing. + */ +public class SlashExtensions { + + private SlashExtensions() { + // Prevent instantiation + } + + /** + * Registers all Slash module extensions with the provided feed extension registry. + * + * @param registry the feed extension registry to register handlers with + */ + public static void register(FeedExtensionRegistry registry) { + itemTagExtensions(registry); + } + + /** + * Registers Slash item-level tag extensions. + * + * @param registry the feed extension registry to register handlers with + */ + private static void itemTagExtensions(FeedExtensionRegistry registry) { + registry.addItemExtension("slash:section", SlashItem::setSlashSection); + registry.addItemExtension("slash:department", SlashItem::setSlashDepartment); + registry.addItemExtension("slash:comments", (item, value) -> mapInteger(value, item::setSlashComments)); + registry.addItemExtension("slash:hit_parade", SlashItem::setSlashHitParade); + } + +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashFeedReader.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashFeedReader.java new file mode 100644 index 00000000..9dae78fa --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashFeedReader.java @@ -0,0 +1,33 @@ +package com.apptasticsoftware.rssreader.module.slash; + +import com.apptasticsoftware.rssreader.AbstractRssReader; +import com.apptasticsoftware.rssreader.DateTimeParser; +import com.apptasticsoftware.rssreader.module.slash.internal.SlashChannelImpl; +import com.apptasticsoftware.rssreader.module.slash.internal.SlashItemImpl; + +/** + * RSS feed reader with Slash module extensions. + * Provides parsing of RSS feeds that include Slash-specific metadata for items. + * + * @see SlashItem for item-level Slash properties + * @see SlashChannel for channel-level Slash properties + */ +public class SlashFeedReader extends AbstractRssReader { + + @Override + protected SlashChannel createChannel(DateTimeParser dateTimeParser) { + return new SlashChannelImpl(dateTimeParser); + } + + @Override + protected SlashItem createItem(DateTimeParser dateTimeParser) { + return new SlashItemImpl(dateTimeParser); + } + + @Override + protected void registerChannelTags() { + super.registerChannelTags(); + var registry = getFeedExtensionRegistry(); + SlashExtensions.register(registry); + } +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashItem.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashItem.java new file mode 100644 index 00000000..1e7b13db --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashItem.java @@ -0,0 +1,13 @@ +package com.apptasticsoftware.rssreader.module.slash; + +import com.apptasticsoftware.rssreader.Item; + +/** + * Slash RSS module item interface extending the core Item with Slash-specific metadata. + * Provides access to item-level Slash elements including section, department, comments count, and hit parade. + * + * @see SlashItemData for the data accessor methods + * @see Slash RSS Module Specification + */ +public interface SlashItem extends Item, SlashItemData { +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashItemData.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashItemData.java new file mode 100644 index 00000000..1d780a9d --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/SlashItemData.java @@ -0,0 +1,91 @@ +package com.apptasticsoftware.rssreader.module.slash; + +import java.util.Optional; + +/** + * Interface for accessing Slash RSS module item data. + * Provides methods to get and set Slash-specific metadata including section, department, comments, and hit parade. + * + * @see Slash RSS Module Specification + */ +public interface SlashItemData { + + /** + * Returns the underlying Slash item data object. + * + * @return the Slash item data + */ + SlashItemData getSlashItemData(); + + /** + * Returns the Slash section (category) of the item. + * + * @return an Optional containing the section, or empty if not set + */ + default Optional getSlashSection() { + return getSlashItemData().getSlashSection(); + } + + /** + * Sets the Slash section (category) of the item. + * + * @param slashSection the section to set + */ + default void setSlashSection(String slashSection) { + getSlashItemData().setSlashSection(slashSection); + } + + /** + * Returns the Slash department of the item. + * + * @return an Optional containing the department, or empty if not set + */ + default Optional getSlashDepartment() { + return getSlashItemData().getSlashDepartment(); + } + + /** + * Sets the Slash department of the item. + * + * @param slashDepartment the department to set + */ + default void setSlashDepartment(String slashDepartment) { + getSlashItemData().setSlashDepartment(slashDepartment); + } + + /** + * Returns the number of comments for the item. + * + * @return an Optional containing the comment count, or empty if not set + */ + default Optional getSlashComments() { + return getSlashItemData().getSlashComments(); + } + + /** + * Sets the number of comments for the item. + * + * @param slashComments the comment count to set + */ + default void setSlashComments(Integer slashComments) { + getSlashItemData().setSlashComments(slashComments); + } + + /** + * Returns the Slash hit parade (comma-separated list of view counts). + * + * @return an Optional containing the hit parade, or empty if not set + */ + default Optional getSlashHitParade() { + return getSlashItemData().getSlashHitParade(); + } + + /** + * Sets the Slash hit parade (comma-separated list of view counts). + * + * @param slashHitParade the hit parade to set + */ + default void setSlashHitParade(String slashHitParade) { + getSlashItemData().setSlashHitParade(slashHitParade); + } +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashChannelImpl.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashChannelImpl.java new file mode 100644 index 00000000..6d1f572e --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashChannelImpl.java @@ -0,0 +1,20 @@ +package com.apptasticsoftware.rssreader.module.slash.internal; + +import com.apptasticsoftware.rssreader.DateTimeParser; +import com.apptasticsoftware.rssreader.internal.ChannelImpl; +import com.apptasticsoftware.rssreader.module.slash.SlashChannel; + +/** + * Implementation of SlashChannel extending core channel functionality. + */ +public class SlashChannelImpl extends ChannelImpl implements SlashChannel { + + /** + * Constructs a SlashChannelImpl with the provided date-time parser. + * + * @param dateTimeParser the parser for parsing date-time values + */ + public SlashChannelImpl(DateTimeParser dateTimeParser) { + super(dateTimeParser); + } +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashItemDataImpl.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashItemDataImpl.java new file mode 100644 index 00000000..c2007275 --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashItemDataImpl.java @@ -0,0 +1,113 @@ +package com.apptasticsoftware.rssreader.module.slash.internal; + +import com.apptasticsoftware.rssreader.module.slash.SlashItemData; + +import java.util.Objects; +import java.util.Optional; + +/** + * Implementation of SlashItemData storing Slash-specific item metadata. + */ +public class SlashItemDataImpl implements SlashItemData { + private String slashSection; + private String slashDepartment; + private Integer slashComments; + private String slashHitParade; + + @Override + public SlashItemData getSlashItemData() { + return this; + } + + /** + * Returns the Slash section of the item. + * + * @return an Optional containing the section, or empty if not set + */ + @Override + public Optional getSlashSection() { + return Optional.ofNullable(slashSection); + } + + /** + * Sets the Slash section of the item. + * + * @param slashSection the section to set + */ + @Override + public void setSlashSection(String slashSection) { + this.slashSection = slashSection; + } + + /** + * Returns the Slash department of the item. + * + * @return an Optional containing the department, or empty if not set + */ + @Override + public Optional getSlashDepartment() { + return Optional.ofNullable(slashDepartment); + } + + /** + * Sets the Slash department of the item. + * + * @param slashDepartment the department to set + */ + @Override + public void setSlashDepartment(String slashDepartment) { + this.slashDepartment = slashDepartment; + } + + /** + * Returns the number of comments for the item. + * + * @return an Optional containing the comment count, or empty if not set + */ + @Override + public Optional getSlashComments() { + return Optional.ofNullable(slashComments); + } + + /** + * Sets the number of comments for the item. + * + * @param slashComments the comment count to set + */ + @Override + public void setSlashComments(Integer slashComments) { + this.slashComments = slashComments; + } + + /** + * Returns the Slash hit parade (comma-separated list of view counts). + * + * @return an Optional containing the hit parade, or empty if not set + */ + @Override + public Optional getSlashHitParade() { + return Optional.ofNullable(slashHitParade); + } + + /** + * Sets the Slash hit parade (comma-separated list of view counts). + * + * @param slashHitParade the hit parade to set + */ + @Override + public void setSlashHitParade(String slashHitParade) { + this.slashHitParade = slashHitParade; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SlashItemDataImpl that = (SlashItemDataImpl) o; + return Objects.equals(getSlashSection(), that.getSlashSection()) && Objects.equals(getSlashDepartment(), that.getSlashDepartment()) && Objects.equals(getSlashComments(), that.getSlashComments()) && Objects.equals(getSlashHitParade(), that.getSlashHitParade()); + } + + @Override + public int hashCode() { + return Objects.hash(getSlashSection(), getSlashDepartment(), getSlashComments(), getSlashHitParade()); + } +} diff --git a/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashItemImpl.java b/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashItemImpl.java new file mode 100644 index 00000000..0244fc4f --- /dev/null +++ b/src/main/java/com/apptasticsoftware/rssreader/module/slash/internal/SlashItemImpl.java @@ -0,0 +1,42 @@ +package com.apptasticsoftware.rssreader.module.slash.internal; + +import com.apptasticsoftware.rssreader.DateTimeParser; +import com.apptasticsoftware.rssreader.internal.ItemImpl; +import com.apptasticsoftware.rssreader.module.slash.SlashItem; +import com.apptasticsoftware.rssreader.module.slash.SlashItemData; + +import java.util.Objects; + +/** + * Implementation of SlashItem combining core item functionality with Slash-specific metadata. + */ +public class SlashItemImpl extends ItemImpl implements SlashItem { + private final SlashItemData slashData = new SlashItemDataImpl(); + + /** + * Constructs a SlashItemImpl with the provided date-time parser. + * + * @param dateTimeParser the parser for parsing date-time values + */ + public SlashItemImpl(DateTimeParser dateTimeParser) { + super(dateTimeParser); + } + + @Override + public SlashItemData getSlashItemData() { + return slashData; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SlashItemImpl)) return false; + if (!super.equals(o)) return false; + SlashItemImpl slashItem = (SlashItemImpl) o; + return Objects.equals(getSlashItemData(), slashItem.getSlashItemData()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), getSlashItemData()); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 170855c0..1b80bbd4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -38,6 +38,7 @@ exports com.apptasticsoftware.rssreader.module.opensearch; exports com.apptasticsoftware.rssreader.module.podcast; exports com.apptasticsoftware.rssreader.module.psc; + exports com.apptasticsoftware.rssreader.module.slash; exports com.apptasticsoftware.rssreader.module.spotify; exports com.apptasticsoftware.rssreader.module.wfw; exports com.apptasticsoftware.rssreader.module.youtube; diff --git a/src/test/java/com/apptasticsoftware/rssreader/module/slash/SlashFeedReaderTest.java b/src/test/java/com/apptasticsoftware/rssreader/module/slash/SlashFeedReaderTest.java new file mode 100644 index 00000000..8ddc2d51 --- /dev/null +++ b/src/test/java/com/apptasticsoftware/rssreader/module/slash/SlashFeedReaderTest.java @@ -0,0 +1,48 @@ +package com.apptasticsoftware.rssreader.module.slash; + +import com.apptasticsoftware.rssreader.module.slash.internal.SlashItemDataImpl; +import com.apptasticsoftware.rssreader.module.slash.internal.SlashItemImpl; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.stream.Collectors; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings("java:S5961") +class SlashFeedReaderTest { + + @Test + void example1() { + var items = new SlashFeedReader().read(fromFile("module/slash/example1.xml")).collect(Collectors.toList()); + assertEquals(1, items.size()); + + var item = items.get(0); + var channel = item.getChannel(); + assertThat(channel.getTitle(), is("Slashdot")); + assertThat(channel.getDescription(), is("News for nerds, stuff that matters")); + + assertThat(item.getTitle(), isPresentAndIs("Jupiter Moon Ganymede May Have An Ocean")); + assertThat(item.getLink(), isPresentAndIs("http://slashdot.org/article.pl?sid=00/12/17/0622203")); + assertThat(item.getDescription(), isPresentAnd(startsWith("This article talks about how Jupiter's moon, Ganymede, may have a"))); + assertThat(item.getSlashSection(), isPresentAndIs("articles")); + assertThat(item.getSlashDepartment(), isPresentAndIs("not-an-ocean-unless-there-are-lobsters")); + assertThat(item.getSlashComments(), isPresentAndIs(177)); + assertThat(item.getSlashHitParade(), isPresentAndIs("177,155,105,33,6,3,0")); + } + + @Test + void equalsContract() { + EqualsVerifier.simple().forClass(SlashItemImpl.class).withNonnullFields("slashData").withIgnoredFields("defaultComparator").withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").withIgnoredFields("enclosure").withNonnullFields("enclosures").verify(); + EqualsVerifier.simple().forClass(SlashItemDataImpl.class).verify(); + } + + private InputStream fromFile(String fileName) { + return getClass().getClassLoader().getResourceAsStream(fileName); + } +} diff --git a/src/test/resources/module/slash/example1.xml b/src/test/resources/module/slash/example1.xml new file mode 100644 index 00000000..04499de4 --- /dev/null +++ b/src/test/resources/module/slash/example1.xml @@ -0,0 +1,32 @@ + + + + + Slashdot + http://slashdot.org + News for nerds, stuff that matters + OSDN + pater@slashdot.org + Copyright © 2000 Slashdot + en-us + 2000-12-17T01:17-05:00 + + + Jupiter Moon Ganymede May Have An Ocean + http://slashdot.org/article.pl?sid=00/12/17/0622203 + + This article talks about how Jupiter's moon, Ganymede, may have a + salt water ocean on it. Kind of ... + + timothy + space + 2000-12-17T01:17 + articles + not-an-ocean-unless-there-are-lobsters + 177 + 177,155,105,33,6,3,0 + + + \ No newline at end of file