mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-22 20:48:59 +00:00
(search) Vendor rssreader and modify it to be able to consume the nlnet atom feed
Also dial down the logging a bit for the rssreader package.
This commit is contained in:
parent
b5469bd8a1
commit
2315fdc731
@ -27,7 +27,7 @@ dependencies {
|
||||
implementation project(':code:processes:crawling-process:ft-content-type')
|
||||
|
||||
implementation libs.jsoup
|
||||
implementation libs.rssreader
|
||||
implementation project(':third-party:rssreader')
|
||||
implementation libs.opencsv
|
||||
implementation libs.sqlite
|
||||
implementation libs.bundles.slf4j
|
||||
|
27
code/functions/live-capture/test-resources/nlnet.atom
Normal file
27
code/functions/live-capture/test-resources/nlnet.atom
Normal file
@ -0,0 +1,27 @@
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://nlnet.nl">
|
||||
<title type="text">NLnet news</title>
|
||||
<updated>2025-01-01T00:00:00Z</updated>
|
||||
<id>https://nlnet.nl/feed.atom</id>
|
||||
<link rel="self" type="application/atom+xml" href="https://nlnet.nl/feed.atom"/>
|
||||
<entry>
|
||||
<id>https://nlnet.nl/news/2025/20250101-announcing-grantees-June-call.html</id>
|
||||
<author>
|
||||
<name>NLnet</name>
|
||||
</author>
|
||||
<title type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">50 Free and Open Source Projects Selected for NGI Zero grants</div>
|
||||
</title>
|
||||
<link href="/news/2025/20250101-announcing-grantees-June-call.html"/>
|
||||
<updated>2025-01-01T00:00:00Z</updated>
|
||||
<content type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p class="paralead">Happy 2025 everyone! On this first day of the fresh new year we are happy to announce 50 project teams were selected to receive NGI Zero grants. We are welcoming projects from 18 countries involving people and organisations of various types: individuals, associations, small and medium enterprises, foundations, universities, and informal collectives. The new projects are all across the different layers of the NGI technology stack: from trustworthy open hardware to services & applications which provide autonomy for end-users.</p>
|
||||
<p>The 50 free and open source projects were selected across two funds. 19 teams will receive grants from the <a href="/commonsfund/">NGI Zero Commons Fund</a>, a broadly themed fund that supports people working on reclaiming the public nature of the internet. The other 31 projects will work within <a href="/core/">NGI Zero Core</a> which focuses on strengthening the open internet architecture. Both funds offer financial and practical support. The latter consisting of <a href="/NGI0/services/">support services</a> such as accessibility and security audits, advice on license compliance, help with testing, documentation or UX design.</p>
|
||||
<h2>If you applied for a grant</h2>
|
||||
<p>This is the selection for the <a href="https://nlnet.nl/news/2024/20240401-call.html">June call</a>. We always inform <em>all</em> applicants about the outcome of the review ahead of the public announcement, if the are selected or not. If you have not heard anything, you probably applied to a later call that is still under review. You can see which call you applied to by checking the application number assigned to the project when you applied. The second number in the sequence refers to the month of the call, so 06 in the case of the June call. (It should not happen, but if you did apply to the June call and did not hear anything, do contact us.)</p>
|
||||
<h2>Meet the new projects!</h2>
|
||||
</div>
|
||||
</content>
|
||||
</entry>
|
||||
|
||||
</feed>
|
@ -1,8 +1,13 @@
|
||||
package nu.marginalia.rss.svc;
|
||||
|
||||
import com.apptasticsoftware.rssreader.Item;
|
||||
import com.apptasticsoftware.rssreader.RssReader;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class TestXmlSanitization {
|
||||
|
||||
@Test
|
||||
@ -13,6 +18,21 @@ public class TestXmlSanitization {
|
||||
Assertions.assertEquals("'", FeedFetcherService.sanitizeEntities("'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNlnetTitleTag() {
|
||||
// The NLnet atom feed puts HTML tags in the entry/title tags, which breaks the vanilla RssReader code
|
||||
|
||||
// Verify we're able to consume and strip out the HTML tags
|
||||
RssReader r = new RssReader();
|
||||
|
||||
List<Item> items = r.read(ClassLoader.getSystemResourceAsStream("nlnet.atom")).toList();
|
||||
|
||||
Assertions.assertEquals(1, items.size());
|
||||
for (var item : items) {
|
||||
Assertions.assertEquals(Optional.of("50 Free and Open Source Projects Selected for NGI Zero grants"), item.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStrayAmpersand() {
|
||||
Assertions.assertEquals("Bed & Breakfast", FeedFetcherService.sanitizeEntities("Bed & Breakfast"));
|
||||
|
@ -100,6 +100,7 @@ include 'third-party:openzim'
|
||||
include 'third-party:count-min-sketch'
|
||||
include 'third-party:commons-codec'
|
||||
include 'third-party:parquet-floor'
|
||||
include 'third-party:rssreader'
|
||||
include 'third-party:encyclopedia-marginalia-nu'
|
||||
|
||||
ext {
|
||||
@ -159,8 +160,6 @@ dependencyResolutionManagement {
|
||||
library('prometheus-server', 'io.prometheus', 'simpleclient_httpserver').version('0.16.0')
|
||||
library('prometheus-hotspot', 'io.prometheus', 'simpleclient_hotspot').version('0.16.0')
|
||||
|
||||
library('rssreader', 'com.apptasticsoftware', 'rssreader').version('3.5.0')
|
||||
|
||||
library('slf4j.api', 'org.slf4j', 'slf4j-api').version('1.7.36')
|
||||
library('slf4j.jdk14', 'org.slf4j', 'slf4j-jdk14').version('2.0.3')
|
||||
|
||||
|
1
third-party/README.md
vendored
1
third-party/README.md
vendored
@ -11,6 +11,7 @@ or lack an artifact, or to override some default that is inappropriate for the t
|
||||
* [OpenZIM](openzim/) - GPL-2.0+
|
||||
* [Commons Codec](commons-codec/) - Apache 2.0
|
||||
* [encylopedia.marginalia.nu](encyclopedia-marginalia-nu/) - GPL 2.0+
|
||||
* [rssreader](rssreader) - MIT
|
||||
|
||||
### Repackaged
|
||||
* [SymSpell](symspell/) - LGPL-3.0
|
||||
|
89
third-party/rssreader/.gitignore
vendored
Normal file
89
third-party/rssreader/.gitignore
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
.gradle
|
||||
/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# IDEA
|
||||
# ----
|
||||
.idea
|
||||
.shelf
|
||||
/*.iml
|
||||
/*.ipr
|
||||
/*.iws
|
||||
/buildSrc/.idea
|
||||
/buildSrc/.shelf
|
||||
/buildSrc/*.iml
|
||||
/buildSrc/*.ipr
|
||||
/buildSrc/*.iws
|
||||
/buildSrc/out
|
||||
/buildSrc/subprojects/*/*.iml
|
||||
/buildSrc/subprojects/*/out
|
||||
/out
|
||||
/subprojects/*/*.iml
|
||||
/subprojects/*/out
|
||||
|
||||
|
||||
# Eclipse
|
||||
# -------
|
||||
*.classpath
|
||||
*.project
|
||||
*.settings
|
||||
/bin
|
||||
/subprojects/*/bin
|
||||
atlassian-ide-plugin.xml
|
||||
.metadata/
|
||||
|
||||
|
||||
# Emacs
|
||||
# -----
|
||||
*~
|
||||
\#*\#
|
||||
.\#*
|
||||
|
||||
|
||||
# macOS
|
||||
# ----
|
||||
.DS_Store
|
||||
|
||||
|
||||
# HPROF
|
||||
# -----
|
||||
*.hprof
|
||||
|
||||
|
||||
# Compiled class file
|
||||
# -------------------
|
||||
*.class
|
||||
|
||||
|
||||
# Logs
|
||||
# ----
|
||||
/*.log
|
||||
|
||||
|
||||
# Package Files
|
||||
# -------------
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
|
||||
# generated files
|
||||
# ---------------
|
||||
bin/
|
||||
gen/
|
||||
|
||||
|
||||
# Virtual machine crash logs
|
||||
# --------------------------
|
||||
hs_err_pid*
|
21
third-party/rssreader/LICENSE
vendored
Normal file
21
third-party/rssreader/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
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.
|
31
third-party/rssreader/README.md
vendored
Normal file
31
third-party/rssreader/README.md
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
RSS Reader
|
||||
==========
|
||||
|
||||
Vendored from https://github.com/w3stling/rssreader
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024, 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.
|
||||
|
16
third-party/rssreader/build.gradle
vendored
Normal file
16
third-party/rssreader/build.gradle
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
901
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/AbstractRssReader.java
vendored
Normal file
901
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/AbstractRssReader.java
vendored
Normal file
@ -0,0 +1,901 @@
|
||||
/*
|
||||
* 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 com.apptasticsoftware.rssreader.internal.DaemonThreadFactory;
|
||||
import com.apptasticsoftware.rssreader.internal.StreamUtil;
|
||||
import com.apptasticsoftware.rssreader.internal.XMLInputFactorySecurity;
|
||||
import com.apptasticsoftware.rssreader.internal.stream.AutoCloseStream;
|
||||
import com.apptasticsoftware.rssreader.util.Default;
|
||||
import com.apptasticsoftware.rssreader.util.Mapper;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.XMLStreamReader;
|
||||
import java.io.*;
|
||||
import java.lang.ref.Cleaner;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import static com.apptasticsoftware.rssreader.util.Mapper.*;
|
||||
import static javax.xml.stream.XMLStreamConstants.*;
|
||||
|
||||
|
||||
/**
|
||||
* Abstract base class for implementing modules or extensions of RSS / Atom feeds with custom tags and attributes.
|
||||
*/
|
||||
public abstract class AbstractRssReader<C extends Channel, I extends Item> {
|
||||
private static final Logger LOGGER = Logger.getLogger("com.apptasticsoftware.rssreader");
|
||||
private static final ScheduledExecutorService EXECUTOR = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory("RssReaderWorker"));
|
||||
private static final Cleaner CLEANER = Cleaner.create();
|
||||
private final HttpClient httpClient;
|
||||
private DateTimeParser dateTimeParser = Default.getDateTimeParser();
|
||||
private String userAgent = "";
|
||||
private Duration connectionTimeout = Duration.ofSeconds(25);
|
||||
private Duration requestTimeout = Duration.ofSeconds(25);
|
||||
private Duration readTimeout = Duration.ofSeconds(25);
|
||||
private final Map<String, String> headers = new HashMap<>();
|
||||
private final HashMap<String, BiConsumer<C, String>> channelTags = new HashMap<>();
|
||||
private final HashMap<String, Map<String, BiConsumer<C, String>>> channelAttributes = new HashMap<>();
|
||||
private final HashMap<String, Consumer<I>> onItemTags = new HashMap<>();
|
||||
private final HashMap<String, BiConsumer<I, String>> itemTags = new HashMap<>();
|
||||
private final HashMap<String, Map<String, BiConsumer<I, String>>> itemAttributes = new HashMap<>();
|
||||
private final Set<String> collectChildNodesForTag = Set.of("content", "summary", "title");
|
||||
private boolean isInitialized;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
protected AbstractRssReader() {
|
||||
httpClient = createHttpClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param httpClient http client
|
||||
*/
|
||||
protected AbstractRssReader(HttpClient httpClient) {
|
||||
Objects.requireNonNull(httpClient, "Http client must not be null");
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object of a Channel implementation.
|
||||
*
|
||||
* @deprecated
|
||||
* Use {@link AbstractRssReader#createChannel(DateTimeParser)} instead.
|
||||
*
|
||||
* @return channel
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.5.0", forRemoval=true)
|
||||
protected C createChannel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object of a Channel implementation.
|
||||
*
|
||||
* @param dateTimeParser dateTimeParser
|
||||
* @return channel
|
||||
*/
|
||||
protected abstract C createChannel(DateTimeParser dateTimeParser);
|
||||
|
||||
/**
|
||||
* Returns an object of an Item implementation.
|
||||
*
|
||||
* @deprecated
|
||||
* Use {@link AbstractRssReader#createItem(DateTimeParser)} instead.
|
||||
*
|
||||
* @return item
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.5.0", forRemoval=true)
|
||||
protected I createItem() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object of an Item implementation.
|
||||
*
|
||||
* @param dateTimeParser dateTimeParser
|
||||
* @return item
|
||||
*/
|
||||
protected abstract I createItem(DateTimeParser dateTimeParser);
|
||||
|
||||
/**
|
||||
* Initialize channel and items tags and attributes
|
||||
*/
|
||||
protected void initialize() {
|
||||
registerChannelTags();
|
||||
registerChannelAttributes();
|
||||
registerItemTags();
|
||||
registerItemAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register channel tags for mapping to channel object fields
|
||||
*/
|
||||
@SuppressWarnings("java:S1192")
|
||||
protected void registerChannelTags() {
|
||||
channelTags.putIfAbsent("title", (channel, value) -> Mapper.mapIfEmpty(value, channel::getTitle, channel::setTitle));
|
||||
channelTags.putIfAbsent("description", (channel, value) -> Mapper.mapIfEmpty(value, channel::getDescription, channel::setDescription));
|
||||
channelTags.putIfAbsent("/feed/title", Channel::setTitle);
|
||||
channelTags.putIfAbsent("/rss/channel/title", Channel::setTitle);
|
||||
channelTags.putIfAbsent("/rss/channel/description", Channel::setDescription);
|
||||
channelTags.putIfAbsent("subtitle", Channel::setDescription);
|
||||
channelTags.putIfAbsent("link", Channel::setLink);
|
||||
channelTags.putIfAbsent("category", Channel::addCategory);
|
||||
channelTags.putIfAbsent("language", Channel::setLanguage);
|
||||
channelTags.putIfAbsent("copyright", Channel::setCopyright);
|
||||
channelTags.putIfAbsent("rights", Channel::setCopyright);
|
||||
channelTags.putIfAbsent("generator", Channel::setGenerator);
|
||||
channelTags.putIfAbsent("ttl", Channel::setTtl);
|
||||
channelTags.putIfAbsent("pubDate", Channel::setPubDate);
|
||||
channelTags.putIfAbsent("lastBuildDate", Channel::setLastBuildDate);
|
||||
channelTags.putIfAbsent("updated", Channel::setLastBuildDate);
|
||||
channelTags.putIfAbsent("managingEditor", Channel::setManagingEditor);
|
||||
channelTags.putIfAbsent("webMaster", Channel::setWebMaster);
|
||||
channelTags.putIfAbsent("docs", Channel::setDocs);
|
||||
channelTags.putIfAbsent("rating", Channel::setRating);
|
||||
channelTags.putIfAbsent("/rss/channel/image/link", (channel, value) -> createIfNull(channel::getImage, channel::setImage, Image::new).setLink(value));
|
||||
channelTags.putIfAbsent("/rss/channel/image/title", (channel, value) -> createIfNull(channel::getImage, channel::setImage, Image::new).setTitle(value));
|
||||
channelTags.putIfAbsent("/rss/channel/image/url", (channel, value) -> createIfNull(channel::getImage, channel::setImage, Image::new).setUrl(value));
|
||||
channelTags.putIfAbsent("/rss/channel/image/description", (channel, value) -> createIfNullOptional(channel::getImage, channel::setImage, Image::new).ifPresent(i -> i.setDescription(value)));
|
||||
channelTags.putIfAbsent("/rss/channel/image/height", (channel, value) -> createIfNullOptional(channel::getImage, channel::setImage, Image::new).ifPresent(i -> mapInteger(value, i::setHeight)));
|
||||
channelTags.putIfAbsent("/rss/channel/image/width", (channel, value) -> createIfNullOptional(channel::getImage, channel::setImage, Image::new).ifPresent(i -> mapInteger(value, i::setWidth)));
|
||||
channelTags.putIfAbsent("dc:language", (channel, value) -> Mapper.mapIfEmpty(value, channel::getLanguage, channel::setLanguage));
|
||||
channelTags.putIfAbsent("dc:rights", (channel, value) -> Mapper.mapIfEmpty(value, channel::getCopyright, channel::setCopyright));
|
||||
channelTags.putIfAbsent("dc:title", (channel, value) -> Mapper.mapIfEmpty(value, channel::getTitle, channel::setTitle));
|
||||
channelTags.putIfAbsent("sy:updatePeriod", (channel, value) -> channel.syUpdatePeriod = value);
|
||||
channelTags.putIfAbsent("sy:updateFrequency", (channel, value) -> mapInteger(value, number -> channel.syUpdateFrequency = number));
|
||||
channelTags.putIfAbsent("/feed/icon", (channel, value) -> createIfNull(channel::getImage, channel::setImage, Image::new).setUrl(value));
|
||||
channelTags.putIfAbsent("/feed/logo", (channel, value) -> createIfNull(channel::getImage, channel::setImage, Image::new).setUrl(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register channel attributes for mapping to channel object fields
|
||||
*/
|
||||
protected void registerChannelAttributes() {
|
||||
channelAttributes.computeIfAbsent("link", k -> new HashMap<>()).put("href", Channel::setLink);
|
||||
channelAttributes.computeIfAbsent("category", k -> new HashMap<>()).putIfAbsent("term", Channel::addCategory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register item tags for mapping to item object fields
|
||||
*/
|
||||
@SuppressWarnings("java:S1192")
|
||||
protected void registerItemTags() {
|
||||
itemTags.putIfAbsent("guid", Item::setGuid);
|
||||
itemTags.putIfAbsent("id", Item::setGuid);
|
||||
itemTags.putIfAbsent("title", (item, value) -> Mapper.mapIfEmpty(value, item::getTitle, item::setTitle));
|
||||
itemTags.putIfAbsent("/feed/entry/title", Item::setTitle);
|
||||
itemTags.putIfAbsent("/rss/channel/item/title", Item::setTitle);
|
||||
itemTags.putIfAbsent("description", Item::setDescription);
|
||||
itemTags.putIfAbsent("summary", Item::setDescription);
|
||||
itemTags.putIfAbsent("content", Item::setContent);
|
||||
itemTags.putIfAbsent("content:encoded", (item, value) -> Mapper.mapIfEmpty(value, item::getContent, item::setContent));
|
||||
itemTags.putIfAbsent("link", Item::setLink);
|
||||
itemTags.putIfAbsent("author", Item::setAuthor);
|
||||
itemTags.putIfAbsent("/feed/entry/author/name", Item::setAuthor);
|
||||
itemTags.putIfAbsent("category", Item::addCategory);
|
||||
itemTags.putIfAbsent("pubDate", Item::setPubDate);
|
||||
itemTags.putIfAbsent("published", Item::setPubDate);
|
||||
itemTags.putIfAbsent("updated", (item, value) -> {
|
||||
item.setUpdated(value);
|
||||
Mapper.mapIfEmpty(value, item::getPubDate, item::setPubDate);
|
||||
});
|
||||
itemTags.putIfAbsent("comments", Item::setComments);
|
||||
itemTags.putIfAbsent("dc:creator", (item, value) -> Mapper.mapIfEmpty(value, item::getAuthor, item::setAuthor));
|
||||
itemTags.putIfAbsent("dc:date", (item, value) -> Mapper.mapIfEmpty(value, item::getPubDate, item::setPubDate));
|
||||
itemTags.putIfAbsent("dc:identifier", (item, value) -> Mapper.mapIfEmpty(value, item::getGuid, item::setGuid));
|
||||
itemTags.putIfAbsent("dc:title", (item, value) -> Mapper.mapIfEmpty(value, item::getTitle, item::setTitle));
|
||||
itemTags.putIfAbsent("dc:description", (item, value) -> Mapper.mapIfEmpty(value, item::getDescription, item::setDescription));
|
||||
|
||||
onItemTags.put("enclosure", item -> item.addEnclosure(new Enclosure()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register item attributes for mapping to item object fields
|
||||
*/
|
||||
protected void registerItemAttributes() {
|
||||
itemAttributes.computeIfAbsent("link", k -> new HashMap<>()).putIfAbsent("href", Item::setLink);
|
||||
itemAttributes.computeIfAbsent("guid", k -> new HashMap<>()).putIfAbsent("isPermaLink", (item, value) -> item.setIsPermaLink(Boolean.parseBoolean(value)) );
|
||||
itemAttributes.computeIfAbsent("category", k -> new HashMap<>()).putIfAbsent("term", Item::addCategory);
|
||||
|
||||
var enclosureAttributes = itemAttributes.computeIfAbsent("enclosure", k -> new HashMap<>());
|
||||
enclosureAttributes.putIfAbsent("url", (item, value) -> item.getEnclosure().ifPresent(a -> a.setUrl(value)));
|
||||
enclosureAttributes.putIfAbsent("type", (item, value) -> item.getEnclosure().ifPresent(a -> a.setType(value)));
|
||||
enclosureAttributes.putIfAbsent("length", (item, value) -> item.getEnclosure().ifPresent(e -> mapLong(value, e::setLength)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user-agent of the http client.
|
||||
* Optional parameter if not set the default value for {@code java.net.http.HttpClient} will be used.
|
||||
* @param userAgent the user-agent to use.
|
||||
* @return updated RSSReader.
|
||||
*/
|
||||
public AbstractRssReader<C, I> setUserAgent(String userAgent) {
|
||||
Objects.requireNonNull(userAgent, "User-agent must not be null");
|
||||
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a http header to the http client.
|
||||
* @param key the key name of the header.
|
||||
* @param value the value of the header.
|
||||
* @return updated RSSReader.
|
||||
*/
|
||||
public AbstractRssReader<C, I> addHeader(String key, String value) {
|
||||
Objects.requireNonNull(key, "Key must not be null");
|
||||
Objects.requireNonNull(value, "Value must not be null");
|
||||
|
||||
this.headers.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the connection timeout for the http client.
|
||||
* The connection timeout is the time it takes to establish a connection to the server.
|
||||
* If set to zero the default value for {@link java.net.http.HttpClient.Builder#connectTimeout(Duration)} will be used.
|
||||
* Default: 25 seconds.
|
||||
*
|
||||
* @param connectionTimeout the timeout duration.
|
||||
* @return updated RSSReader.
|
||||
*/
|
||||
public AbstractRssReader<C, I> setConnectionTimeout(Duration connectionTimeout) {
|
||||
validate(connectionTimeout, "Connection timeout");
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request timeout for the http client.
|
||||
* The request timeout is the time between the request is sent and the first byte of the response is received.
|
||||
* If set to zero the default value for {@link java.net.http.HttpRequest.Builder#timeout(Duration)} will be used.
|
||||
* Default: 25 seconds.
|
||||
*
|
||||
* @param requestTimeout the timeout duration.
|
||||
* @return updated RSSReader.
|
||||
*/
|
||||
public AbstractRssReader<C, I> setRequestTimeout(Duration requestTimeout) {
|
||||
validate(requestTimeout, "Request timeout");
|
||||
this.requestTimeout = requestTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read timeout.
|
||||
* The read timeout it the time for reading all data in the response body.
|
||||
* The effect of setting the timeout to zero is the same as setting an infinite Duration, ie. block forever.
|
||||
* Default: 25 seconds.
|
||||
*
|
||||
* @param readTimeout the timeout duration.
|
||||
* @return updated RSSReader.
|
||||
*/
|
||||
public AbstractRssReader<C, I> setReadTimeout(Duration readTimeout) {
|
||||
validate(readTimeout, "Read timeout");
|
||||
this.readTimeout = readTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void validate(Duration duration, String name) {
|
||||
Objects.requireNonNull(duration, name + " must not be null");
|
||||
|
||||
if (duration.isNegative()) {
|
||||
throw new IllegalArgumentException(name + " must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item extension for tags
|
||||
* @param tag - tag name
|
||||
* @param consumer - setter method in Item class to use for mapping
|
||||
* @return this instance
|
||||
*/
|
||||
public AbstractRssReader<C, I> addItemExtension(String tag, BiConsumer<I, String> consumer) {
|
||||
Objects.requireNonNull(tag, "Item tag must not be null");
|
||||
Objects.requireNonNull(consumer, "Item consumer must not be null");
|
||||
|
||||
itemTags.put(tag, consumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item extension for attributes
|
||||
* @param tag - tag name
|
||||
* @param attribute - attribute name
|
||||
* @param consumer - setter method in Item class to use for mapping
|
||||
* @return this instance
|
||||
*/
|
||||
public AbstractRssReader<C, I> addItemExtension(String tag, String attribute, BiConsumer<I, String> consumer) {
|
||||
Objects.requireNonNull(tag, "Item tag must not be null");
|
||||
Objects.requireNonNull(attribute, "Item attribute must not be null");
|
||||
Objects.requireNonNull(consumer, "Item consumer must not be null");
|
||||
|
||||
itemAttributes.computeIfAbsent(tag, k -> new HashMap<>())
|
||||
.put(attribute, consumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add channel extension for tags
|
||||
* @param tag - tag name
|
||||
* @param consumer - setter method in Channel class to use for mapping
|
||||
* @return this instance
|
||||
*/
|
||||
public AbstractRssReader<C, I> addChannelExtension(String tag, BiConsumer<C, String> consumer) {
|
||||
Objects.requireNonNull(tag, "Channel tag must not be null");
|
||||
Objects.requireNonNull(consumer, "Channel consumer must not be null");
|
||||
|
||||
channelTags.put(tag, consumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add channel extension for attributes
|
||||
* @param tag - tag name
|
||||
* @param attribute - attribute name
|
||||
* @param consumer - setter method in Channel class to use for mapping
|
||||
* @return this instance
|
||||
*/
|
||||
public AbstractRssReader<C, I> addChannelExtension(String tag, String attribute, BiConsumer<C, String> consumer) {
|
||||
Objects.requireNonNull(tag, "Channel tag must not be null");
|
||||
Objects.requireNonNull(attribute, "Channel attribute must not be null");
|
||||
Objects.requireNonNull(consumer, "Channel consumer must not be null");
|
||||
|
||||
channelAttributes.computeIfAbsent(tag, k -> new HashMap<>())
|
||||
.put(attribute, consumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read RSS feed with the given URL or file URI.
|
||||
* @param url URL to RSS feed or file URI.
|
||||
* @return Stream of items
|
||||
* @throws IOException Fail to read url or its content
|
||||
*/
|
||||
@SuppressWarnings("squid:S1181")
|
||||
public Stream<I> read(String url) throws IOException {
|
||||
Objects.requireNonNull(url, "URL must not be null");
|
||||
|
||||
try {
|
||||
return readAsync(url).get(1, TimeUnit.MINUTES);
|
||||
} catch (CompletionException e) {
|
||||
try {
|
||||
throw e.getCause();
|
||||
} catch (IOException e2) {
|
||||
throw e2;
|
||||
} catch(Throwable e2) {
|
||||
throw new AssertionError(e2);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException(e);
|
||||
} catch (ExecutionException | TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from a collections of RSS feed.
|
||||
* @param urls collections of URLs or file URIs
|
||||
* @return Stream of items
|
||||
*/
|
||||
public Stream<Item> read(Collection<String> urls) {
|
||||
Objects.requireNonNull(urls, "URLs collection must not be null");
|
||||
urls.forEach(url -> Objects.requireNonNull(url, "URL must not be null. Url: " + url));
|
||||
|
||||
if (!isInitialized) {
|
||||
initialize();
|
||||
isInitialized = true;
|
||||
}
|
||||
return AutoCloseStream.of(urls.stream()
|
||||
.parallel()
|
||||
.map(url -> {
|
||||
try {
|
||||
return Map.entry(url, readAsync(url));
|
||||
} catch (Exception e) {
|
||||
if (LOGGER.isLoggable(Level.WARNING)) {
|
||||
LOGGER.log(Level.WARNING, () -> String.format("Failed read URL %s. Message: %s", url, e.getMessage()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(f -> {
|
||||
try {
|
||||
return f.getValue().join();
|
||||
} catch (Exception e) {
|
||||
if (LOGGER.isLoggable(Level.WARNING)) {
|
||||
LOGGER.log(Level.WARNING, () -> String.format("Failed to read URL %s. Message: %s", f.getKey(), e.getMessage()));
|
||||
}
|
||||
return Stream.empty();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read RSS feed from input stream.
|
||||
* @param inputStream inputStream containing the RSS feed.
|
||||
* @return Stream of items
|
||||
*/
|
||||
public Stream<I> read(InputStream inputStream) {
|
||||
Objects.requireNonNull(inputStream, "Input stream must not be null");
|
||||
|
||||
if (!isInitialized) {
|
||||
initialize();
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
inputStream = new BufferedInputStream(inputStream);
|
||||
removeBadData(inputStream);
|
||||
var itemIterator = new RssItemIterator(inputStream);
|
||||
return AutoCloseStream.of(StreamUtil.asStream(itemIterator).onClose(itemIterator::close));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read RSS feed asynchronous with the given URL.
|
||||
* @param url URL to RSS feed.
|
||||
* @return Stream of items
|
||||
*/
|
||||
public CompletableFuture<Stream<I>> readAsync(String url) {
|
||||
Objects.requireNonNull(url, "URL must not be null");
|
||||
|
||||
if (!isInitialized) {
|
||||
initialize();
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
try {
|
||||
var uri = URI.create(url);
|
||||
if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
// Read from file
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return read(new FileInputStream(uri.getPath()));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Read from http or https
|
||||
return sendAsyncRequest(url).thenApply(processResponse());
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Read feed data provided as a string
|
||||
var inputStream = new ByteArrayInputStream(url.getBytes(StandardCharsets.UTF_8));
|
||||
return read(inputStream);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends request
|
||||
* @param url url
|
||||
* @return response
|
||||
*/
|
||||
protected CompletableFuture<HttpResponse<InputStream>> sendAsyncRequest(String url) {
|
||||
var builder = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Accept-Encoding", "gzip");
|
||||
if (requestTimeout.toMillis() > 0) {
|
||||
builder.timeout(requestTimeout);
|
||||
}
|
||||
|
||||
if (!userAgent.isBlank()) {
|
||||
builder.header("User-Agent", userAgent);
|
||||
}
|
||||
|
||||
headers.forEach(builder::header);
|
||||
return httpClient.sendAsync(builder.GET().build(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
}
|
||||
|
||||
private Function<HttpResponse<InputStream>, Stream<I>> processResponse() {
|
||||
return response -> {
|
||||
try {
|
||||
if (response.statusCode() >= 400 && response.statusCode() < 600) {
|
||||
throw new IOException(String.format("Response HTTP status code: %d", response.statusCode()));
|
||||
}
|
||||
|
||||
var inputStream = response.body();
|
||||
if ("gzip".equals(response.headers().firstValue("Content-Encoding").orElse(null))) {
|
||||
inputStream = new GZIPInputStream(inputStream);
|
||||
}
|
||||
|
||||
inputStream = new BufferedInputStream(inputStream);
|
||||
removeBadData(inputStream);
|
||||
var itemIterator = new RssItemIterator(inputStream);
|
||||
return AutoCloseStream.of(StreamUtil.asStream(itemIterator).onClose(itemIterator::close));
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings({"java:S108", "java:S2674"})
|
||||
private void removeBadData(InputStream inputStream) {
|
||||
try {
|
||||
inputStream.mark(128);
|
||||
long count = 0;
|
||||
int data = inputStream.read();
|
||||
while (Character.isWhitespace(data)) {
|
||||
data = inputStream.read();
|
||||
++count;
|
||||
}
|
||||
inputStream.reset();
|
||||
inputStream.skip(count);
|
||||
} catch (IOException ignore) { }
|
||||
}
|
||||
|
||||
private static class CleaningAction implements Runnable {
|
||||
private final XMLStreamReader xmlStreamReader;
|
||||
private final List<AutoCloseable> resources;
|
||||
|
||||
public CleaningAction(XMLStreamReader xmlStreamReader, AutoCloseable... resources) {
|
||||
this.xmlStreamReader = xmlStreamReader;
|
||||
this.resources = List.of(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (xmlStreamReader != null) {
|
||||
xmlStreamReader.close();
|
||||
}
|
||||
} catch (XMLStreamException e) {
|
||||
LOGGER.log(Level.WARNING, "Failed to close XML stream. ", e);
|
||||
}
|
||||
|
||||
for (AutoCloseable resource : resources) {
|
||||
try {
|
||||
if (resource != null) {
|
||||
resource.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.WARNING, "Failed to close resource. ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RssItemIterator implements Iterator<I>, AutoCloseable {
|
||||
private final StringBuilder textBuilder;
|
||||
private final Map<String, StringBuilder> childNodeTextBuilder;
|
||||
private final Deque<String> elementStack;
|
||||
private XMLStreamReader reader;
|
||||
private C channel;
|
||||
private I item = null;
|
||||
private I nextItem;
|
||||
private boolean isChannelPart = false;
|
||||
private boolean isItemPart = false;
|
||||
private ScheduledFuture<?> parseWatchdog;
|
||||
private final AtomicBoolean isClosed;
|
||||
private Cleaner.Cleanable cleanable;
|
||||
|
||||
public RssItemIterator(InputStream is) {
|
||||
nextItem = null;
|
||||
textBuilder = new StringBuilder();
|
||||
childNodeTextBuilder = new HashMap<>();
|
||||
elementStack = new ArrayDeque<>();
|
||||
isClosed = new AtomicBoolean(false);
|
||||
|
||||
try {
|
||||
// disable XML external entity (XXE) processing
|
||||
var xmlInputFactory = XMLInputFactorySecurity.hardenFactory(XMLInputFactory.newInstance());
|
||||
xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, true);
|
||||
xmlInputFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, false);
|
||||
|
||||
reader = xmlInputFactory.createXMLStreamReader(is);
|
||||
cleanable = CLEANER.register(this, new CleaningAction(reader, is));
|
||||
if (!readTimeout.isZero()) {
|
||||
parseWatchdog = EXECUTOR.schedule(this::close, readTimeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
catch (XMLStreamException e) {
|
||||
LOGGER.log(Level.FINE, "Failed to process XML.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (isClosed.compareAndSet(false, true)) {
|
||||
cleanable.clean();
|
||||
if (parseWatchdog != null) {
|
||||
parseWatchdog.cancel(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void peekNext() {
|
||||
if (nextItem == null) {
|
||||
try {
|
||||
nextItem = next();
|
||||
} catch (NoSuchElementException e) {
|
||||
nextItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
peekNext();
|
||||
return nextItem != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S3776")
|
||||
public I next() {
|
||||
if (nextItem != null) {
|
||||
var next = nextItem;
|
||||
nextItem = null;
|
||||
return next;
|
||||
}
|
||||
|
||||
try {
|
||||
while (reader.hasNext()) {
|
||||
var type = reader.next();
|
||||
collectChildNodes(type);
|
||||
|
||||
if (type == CHARACTERS || type == CDATA) {
|
||||
parseCharacters();
|
||||
} else if (type == START_ELEMENT) {
|
||||
parseStartElement();
|
||||
parseAttributes();
|
||||
} else if (type == END_ELEMENT) {
|
||||
var itemParsed = parseEndElement();
|
||||
if (itemParsed) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (XMLStreamException e) {
|
||||
LOGGER.log(Level.WARNING, "Failed to parse XML.", e);
|
||||
}
|
||||
|
||||
close();
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
private void collectChildNodes(int type) {
|
||||
if (type == START_ELEMENT) {
|
||||
var nsTagName = toNsName(reader.getPrefix(), reader.getLocalName());
|
||||
|
||||
if (!childNodeTextBuilder.isEmpty()) {
|
||||
StringBuilder startTagBuilder = new StringBuilder("<").append(nsTagName);
|
||||
// Add namespaces to start tag
|
||||
for (int i = 0; i < reader.getNamespaceCount(); ++i) {
|
||||
startTagBuilder.append(" ")
|
||||
.append(toNamespacePrefix(reader.getNamespacePrefix(i)))
|
||||
.append("=")
|
||||
.append(reader.getNamespaceURI(i));
|
||||
}
|
||||
// Add attributes to start tag
|
||||
for (int i = 0; i < reader.getAttributeCount(); ++i) {
|
||||
startTagBuilder.append(" ")
|
||||
.append(toNsName(reader.getAttributePrefix(i), reader.getAttributeLocalName(i)))
|
||||
.append("=")
|
||||
.append(reader.getAttributeValue(i));
|
||||
}
|
||||
startTagBuilder.append(">");
|
||||
var startTag = startTagBuilder.toString();
|
||||
|
||||
childNodeTextBuilder.entrySet()
|
||||
.stream()
|
||||
.filter(e -> !e.getKey().equals(nsTagName))
|
||||
.forEach(e -> e.getValue().append(startTag));
|
||||
}
|
||||
|
||||
// Collect child notes for tag names in this set
|
||||
if (collectChildNodesForTag.contains(nsTagName)) {
|
||||
childNodeTextBuilder.put(nsTagName, new StringBuilder());
|
||||
}
|
||||
} else if (type == CHARACTERS || type == CDATA) {
|
||||
childNodeTextBuilder.forEach((k, builder) -> builder.append(reader.getText()));
|
||||
} else if (type == END_ELEMENT) {
|
||||
var nsTagName = toNsName(reader.getPrefix(), reader.getLocalName());
|
||||
var endTag = "</" + nsTagName + ">";
|
||||
childNodeTextBuilder.entrySet()
|
||||
.stream()
|
||||
.filter(e -> !e.getKey().equals(nsTagName))
|
||||
.forEach(e -> e.getValue().append(endTag));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S5738")
|
||||
private void parseStartElement() {
|
||||
textBuilder.setLength(0);
|
||||
var nsTagName = toNsName(reader.getPrefix(), reader.getLocalName());
|
||||
elementStack.addLast(nsTagName);
|
||||
|
||||
if (isChannel(nsTagName)) {
|
||||
channel = Objects.requireNonNullElse(createChannel(dateTimeParser), createChannel());
|
||||
channel.setTitle("");
|
||||
channel.setDescription("");
|
||||
channel.setLink("");
|
||||
isChannelPart = true;
|
||||
} else if (isItem(nsTagName)) {
|
||||
item = Objects.requireNonNullElse(createItem(dateTimeParser), createItem());
|
||||
item.setChannel(channel);
|
||||
isChannelPart = false;
|
||||
isItemPart = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isChannel(String tagName) {
|
||||
return "channel".equals(tagName) || "feed".equals(tagName);
|
||||
}
|
||||
|
||||
protected boolean isItem(String tagName) {
|
||||
return "item".equals(tagName) || "entry".equals(tagName);
|
||||
}
|
||||
|
||||
private void parseAttributes() {
|
||||
var nsTagName = toNsName(reader.getPrefix(), reader.getLocalName());
|
||||
var elementFullPath = getElementFullPath();
|
||||
|
||||
if (isChannelPart) {
|
||||
// Map channel attributes
|
||||
mapChannelAttributes(nsTagName);
|
||||
mapChannelAttributes(elementFullPath);
|
||||
} else if (isItemPart) {
|
||||
onItemTags.computeIfPresent(nsTagName, (k, f) -> { f.accept(item); return f; });
|
||||
onItemTags.computeIfPresent(elementFullPath, (k, f) -> { f.accept(item); return f; });
|
||||
// Map item attributes
|
||||
mapItemAttributes(nsTagName);
|
||||
mapItemAttributes(elementFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void mapChannelAttributes(String key) {
|
||||
var consumers = channelAttributes.get(key);
|
||||
if (consumers != null && channel != null) {
|
||||
consumers.forEach((attributeName, consumer) -> {
|
||||
var attributeValue = Optional.ofNullable(reader.getAttributeValue(null, attributeName));
|
||||
attributeValue.ifPresent(v -> consumer.accept(channel, v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void mapItemAttributes(String key) {
|
||||
var consumers = itemAttributes.get(key);
|
||||
if (consumers != null && item != null) {
|
||||
consumers.forEach((attributeName, consumer) -> {
|
||||
var attributeValue = Optional.ofNullable(reader.getAttributeValue(null, attributeName));
|
||||
attributeValue.ifPresent(v -> consumer.accept(item, v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseEndElement() {
|
||||
var nsTagName = toNsName(reader.getPrefix(), reader.getLocalName());
|
||||
var text = textBuilder.toString().trim();
|
||||
var elementFullPath = getElementFullPath();
|
||||
elementStack.removeLast();
|
||||
|
||||
if (isChannelPart) {
|
||||
parseChannelCharacters(channel, nsTagName, elementFullPath, text);
|
||||
} else {
|
||||
parseItemCharacters(item, nsTagName, elementFullPath, text);
|
||||
}
|
||||
|
||||
textBuilder.setLength(0);
|
||||
return isItem(nsTagName);
|
||||
}
|
||||
|
||||
private void parseCharacters() {
|
||||
var text = reader.getText();
|
||||
if (text.isBlank()) {
|
||||
return;
|
||||
}
|
||||
textBuilder.append(text);
|
||||
}
|
||||
|
||||
private void parseChannelCharacters(C channel, String nsTagName, String elementFullPath, String text) {
|
||||
if (channel == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
channelTags.computeIfPresent(nsTagName, (k, f) -> { f.accept(channel, text); return f; });
|
||||
channelTags.computeIfPresent(elementFullPath, (k, f) -> { f.accept(channel, text); return f; });
|
||||
}
|
||||
|
||||
private void parseItemCharacters(final I item, String nsTagName, String elementFullPath, final String text) {
|
||||
var builder = childNodeTextBuilder.remove(nsTagName);
|
||||
if (item == null || (text.isEmpty() && builder == null)) {
|
||||
return;
|
||||
}
|
||||
var textValue = (builder != null) ? builder.toString().trim() : text;
|
||||
itemTags.computeIfPresent(nsTagName, (k, f) -> { f.accept(item, textValue); return f; });
|
||||
|
||||
itemTags.computeIfPresent(elementFullPath, (k, f) -> { f.accept(item, textValue); return f; });
|
||||
}
|
||||
|
||||
private String toNsName(String prefix, String name) {
|
||||
return prefix.isEmpty() ? name : prefix + ":" + name;
|
||||
}
|
||||
|
||||
private String toNamespacePrefix(String prefix) {
|
||||
return prefix == null || prefix.isEmpty() ? "xmlns" : "xmlns" + ":" + prefix;
|
||||
}
|
||||
|
||||
private String getElementFullPath() {
|
||||
return "/" + String.join("/", elementStack);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient() {
|
||||
HttpClient client;
|
||||
try {
|
||||
var context = SSLContext.getInstance("TLSv1.3");
|
||||
context.init(null, null, null);
|
||||
|
||||
var builder = HttpClient.newBuilder()
|
||||
.sslContext(context)
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS);
|
||||
if (connectionTimeout.toMillis() > 0) {
|
||||
builder.connectTimeout(connectionTimeout);
|
||||
}
|
||||
client = builder.build();
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
var builder = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS);
|
||||
if (connectionTimeout.toMillis() > 0) {
|
||||
builder.connectTimeout(connectionTimeout);
|
||||
}
|
||||
client = builder.build();
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
396
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Channel.java
vendored
Normal file
396
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Channel.java
vendored
Normal file
@ -0,0 +1,396 @@
|
||||
/*
|
||||
* 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 com.apptasticsoftware.rssreader.util.Default;
|
||||
import com.apptasticsoftware.rssreader.util.Util;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class representing the RSS channel.
|
||||
*/
|
||||
public class Channel {
|
||||
private String title;
|
||||
private String description;
|
||||
private String category;
|
||||
private final List<String> categories = new ArrayList<>();
|
||||
private String language;
|
||||
private String link;
|
||||
private String copyright;
|
||||
private String generator;
|
||||
private String ttl;
|
||||
private String pubDate;
|
||||
private String lastBuildDate;
|
||||
private String managingEditor;
|
||||
private String webMaster;
|
||||
private String docs;
|
||||
private String rating;
|
||||
private Image image;
|
||||
protected String syUpdatePeriod;
|
||||
protected int syUpdateFrequency = 1;
|
||||
private final DateTimeParser dateTimeParser;
|
||||
|
||||
/**
|
||||
* Constructor for Channel
|
||||
* @deprecated
|
||||
* Use {@link Channel#Channel(DateTimeParser)} instead.
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.5.0", forRemoval=true)
|
||||
public Channel() {
|
||||
dateTimeParser = Default.getDateTimeParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for Channel
|
||||
* @param dateTimeParser dateTimeParser
|
||||
*/
|
||||
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.
|
||||
* @return title
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 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.
|
||||
* @param title title
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get phrase or sentence describing the channel.
|
||||
* @return description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set phrase or sentence describing the channel.
|
||||
* @param description channel description
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category for the channel.
|
||||
*
|
||||
* @deprecated
|
||||
* This method be removed in a future version.
|
||||
* <p> Use {@link Channel#getCategories()} instead.
|
||||
*
|
||||
* @return category
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.3.0", forRemoval=true)
|
||||
public Optional<String> getCategory() {
|
||||
return Optional.ofNullable(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set category for the channel.
|
||||
*
|
||||
* @deprecated
|
||||
* This method be removed in a future version.
|
||||
* <p> Use {@link Channel#addCategory(String category)} instead.
|
||||
*
|
||||
* @param category channel category
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.3.0", forRemoval=true)
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories for the channel.
|
||||
* @return list of categories
|
||||
*/
|
||||
public List<String> getCategories() {
|
||||
return Collections.unmodifiableList(categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add category for the channel.
|
||||
* @param category channel category
|
||||
*/
|
||||
public void addCategory(String category) {
|
||||
this.category = category;
|
||||
categories.add(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the language the channel is written in.
|
||||
* @return language
|
||||
*/
|
||||
public Optional<String> getLanguage() {
|
||||
return Optional.ofNullable(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the language the channel is written in.
|
||||
* @param language language
|
||||
*/
|
||||
public void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the HTML website corresponding to the channel.
|
||||
* @return link
|
||||
*/
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL to the HTML website corresponding to the channel.
|
||||
* @param link URL
|
||||
*/
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get copyright notice for content in the channel.
|
||||
* @return URL
|
||||
*/
|
||||
public Optional<String> getCopyright() {
|
||||
return Optional.ofNullable(copyright);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set copyright notice for content in the channel.
|
||||
* @param copyright copyright
|
||||
*/
|
||||
public void setCopyright(String copyright) {
|
||||
this.copyright = copyright;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string indicating the program used to generate the channel.
|
||||
* @return generator
|
||||
*/
|
||||
public Optional<String> getGenerator() {
|
||||
return Optional.ofNullable(generator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a string indicating the program used to generate the channel.
|
||||
* @param generator generator
|
||||
*/
|
||||
public void setGenerator(String generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ttl (time to live). It's a number of minutes that indicates how long a channel can be cached before
|
||||
* refreshing from the source.
|
||||
* @return time to live
|
||||
*/
|
||||
public Optional<String> getTtl() {
|
||||
return Optional.ofNullable(ttl)
|
||||
.or(() -> Optional.ofNullable(syUpdatePeriod)
|
||||
.map(Util::toMinutes)
|
||||
.map(minutes -> minutes / Math.max(syUpdateFrequency, 1))
|
||||
.map(String::valueOf));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ttl (time to live). It's a number of minutes that indicates how long a channel can be cached before
|
||||
* refreshing from the source.
|
||||
* @param ttl time to live
|
||||
*/
|
||||
public void setTtl(String ttl) {
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the publication date for the content in the channel.
|
||||
* @return publication date
|
||||
*/
|
||||
public Optional<String> getPubDate() {
|
||||
return Optional.ofNullable(pubDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the publication date for the content in the channel.
|
||||
* @return publication date
|
||||
*/
|
||||
public Optional<ZonedDateTime> getPubDateZonedDateTime() {
|
||||
return getPubDate().map(dateTimeParser::parse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the publication date for the content in the channel.
|
||||
* @param pubDate publication date
|
||||
*/
|
||||
public void setPubDate(String pubDate) {
|
||||
this.pubDate = pubDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last time the content of the channel changed.
|
||||
* @return last build date
|
||||
*/
|
||||
public Optional<String> getLastBuildDate() {
|
||||
return Optional.ofNullable(lastBuildDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last time the content of the channel changed.
|
||||
* @return last build date
|
||||
*/
|
||||
public Optional<ZonedDateTime> getLastBuildDateZonedDateTime() {
|
||||
return getLastBuildDate().map(dateTimeParser::parse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last time the content of the channel changed.
|
||||
* @param lastBuildDate last build date
|
||||
*/
|
||||
public void setLastBuildDate(String lastBuildDate) {
|
||||
this.lastBuildDate = lastBuildDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email address for person responsible for editorial content.
|
||||
* @return managing editor
|
||||
*/
|
||||
public Optional<String> getManagingEditor() {
|
||||
return Optional.ofNullable(managingEditor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set email address for person responsible for editorial content.
|
||||
* @param managingEditor managing editor
|
||||
*/
|
||||
public void setManagingEditor(String managingEditor) {
|
||||
this.managingEditor = managingEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email address for person responsible for technical issues relating to channel.
|
||||
* @return web master
|
||||
*/
|
||||
public Optional<String> getWebMaster() {
|
||||
return Optional.ofNullable(webMaster);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set email address for person responsible for technical issues relating to channel.
|
||||
* @param webMaster web master
|
||||
*/
|
||||
public void setWebMaster(String webMaster) {
|
||||
this.webMaster = webMaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the documentation for the format used in the RSS file.
|
||||
* @return documentation
|
||||
*/
|
||||
public String getDocs() {
|
||||
return docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the documentation for the format used in the RSS file.
|
||||
* @param docs documentation
|
||||
*/
|
||||
public void setDocs(String docs) {
|
||||
this.docs = docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PICS rating for the channel.
|
||||
* @return rating
|
||||
*/
|
||||
public String getRating() {
|
||||
return rating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the PICS rating for the channel.
|
||||
* @param rating rating
|
||||
*/
|
||||
public void setRating(String rating) {
|
||||
this.rating = rating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a GIF, JPEG or PNG image that can be displayed with the channel.
|
||||
* @return image
|
||||
*/
|
||||
public Optional<Image> getImage() {
|
||||
return Optional.ofNullable(image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a GIF, JPEG or PNG image that can be displayed with the channel.
|
||||
* @param image image
|
||||
*/
|
||||
public void setImage(Image image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Channel channel = (Channel) o;
|
||||
return Objects.equals(getTitle(), channel.getTitle()) &&
|
||||
Objects.equals(getDescription(), channel.getDescription()) &&
|
||||
getCategories().equals(channel.getCategories()) &&
|
||||
Objects.equals(getLanguage(), channel.getLanguage()) &&
|
||||
Objects.equals(getLink(), channel.getLink()) &&
|
||||
Objects.equals(getCopyright(), channel.getCopyright()) &&
|
||||
Objects.equals(getGenerator(), channel.getGenerator()) &&
|
||||
Objects.equals(getTtl(), channel.getTtl()) &&
|
||||
Objects.equals(getPubDate(), channel.getPubDate()) &&
|
||||
Objects.equals(getLastBuildDate(), channel.getLastBuildDate()) &&
|
||||
Objects.equals(getManagingEditor(), channel.getManagingEditor()) &&
|
||||
Objects.equals(getWebMaster(), channel.getWebMaster()) &&
|
||||
Objects.equals(getDocs(), channel.getDocs()) &&
|
||||
Objects.equals(getRating(), channel.getRating()) &&
|
||||
Objects.equals(getImage(), channel.getImage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getTitle(), getDescription(), getCategories(), getLanguage(), getLink(),
|
||||
getCopyright(), getGenerator(), getTtl(), getPubDate(), getLastBuildDate(),
|
||||
getManagingEditor(), getWebMaster(), getDocs(), getRating(), getImage());
|
||||
}
|
||||
}
|
495
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/DateTime.java
vendored
Normal file
495
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/DateTime.java
vendored
Normal file
@ -0,0 +1,495 @@
|
||||
/*
|
||||
* 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 com.apptasticsoftware.rssreader.util.Default;
|
||||
import com.apptasticsoftware.rssreader.util.ItemComparator;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.*;
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
|
||||
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
|
||||
|
||||
/**
|
||||
* Class for converting date time strings.
|
||||
* This class is only used when sorting or when calling getters that returns ZonedDateTime.
|
||||
*/
|
||||
@SuppressWarnings("javaarchitecture:S7091")
|
||||
public class DateTime implements DateTimeParser {
|
||||
private final ZoneId defaultZone;
|
||||
|
||||
private static final DateTimeFormatter BASIC_ISO_DATE;
|
||||
private static final DateTimeFormatter ISO_LOCAL_DATE;
|
||||
private static final DateTimeFormatter ISO_OFFSET_DATE_TIME;
|
||||
private static final DateTimeFormatter ISO_OFFSET_DATE_TIME_SPECIAL;
|
||||
private static final DateTimeFormatter ISO_LOCAL_DATE_TIME;
|
||||
private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_SPECIAL;
|
||||
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_TIMEZONE;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_TIMEZONE2;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_NO_TIMEZONE;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_GMT_OFFSET;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_GMT_AND_OFFSET;
|
||||
private static final DateTimeFormatter RFC_822_DATE_TIME;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EST;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EDT;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CST;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CDT;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MST;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MDT;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PST;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PDT;
|
||||
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_GMT_OFFSET_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_822_DATE_TIME_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EST_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EDT_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CST_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CDT_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MST_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MDT_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PST_FULL_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PDT_FULL_DOW;
|
||||
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_GMT_OFFSET_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_822_DATE_TIME_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EST_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EDT_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CST_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CDT_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MST_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MDT_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PST_FULL_DOW_MONTH;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PDT_FULL_DOW_MONTH;
|
||||
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_GMT_OFFSET_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_822_DATE_TIME_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EST_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_EDT_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CST_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_CDT_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MST_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_MDT_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PST_NO_DOW;
|
||||
private static final DateTimeFormatter RFC_1123_DATE_TIME_SPECIAL_PDT_NO_DOW;
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_SPECIAL_1;
|
||||
|
||||
static {
|
||||
BASIC_ISO_DATE = DateTimeFormatter.BASIC_ISO_DATE.withLocale(Locale.ENGLISH);
|
||||
ISO_LOCAL_DATE = DateTimeFormatter.ISO_LOCAL_DATE.withLocale(Locale.ENGLISH);
|
||||
ISO_OFFSET_DATE_TIME = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(Locale.ENGLISH);
|
||||
ISO_LOCAL_DATE_TIME = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(Locale.ENGLISH);
|
||||
ISO_LOCAL_DATE_TIME_SPECIAL = new DateTimeFormatterBuilder().parseCaseInsensitive().append(ISO_LOCAL_DATE).appendLiteral(' ').append(ISO_LOCAL_TIME).toFormatter().withLocale(Locale.ENGLISH);
|
||||
ISO_OFFSET_DATE_TIME_SPECIAL = new DateTimeFormatterBuilder().parseCaseInsensitive().append(DateTimeFormatter.ISO_LOCAL_DATE).appendLiteral('T').append(ISO_LOCAL_TIME).appendOffset("+HHMM", "0000").toFormatter(Locale.ENGLISH);
|
||||
|
||||
RFC_1123_DATE_TIME = DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_TIMEZONE = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s zzz", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_TIMEZONE2 = new DateTimeFormatterBuilder().appendPattern("E, d LLL yyyy H:m:s").appendOffset("+H:mm", "+00").toFormatter().withLocale(Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_NO_TIMEZONE = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s", Locale.ENGLISH).withZone(ZoneId.of("UTC"));
|
||||
RFC_1123_DATE_TIME_SPECIAL = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s z", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_GMT_OFFSET = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s O", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_GMT_AND_OFFSET = DateTimeFormatter.ofPattern("E, dd MMM yyyy H:m:s 'GMT'Z", Locale.ENGLISH);
|
||||
RFC_822_DATE_TIME = DateTimeFormatter.ofPattern("E, d LLL yy H:m:s X", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_SPECIAL_EDT = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'EDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-4));
|
||||
RFC_1123_DATE_TIME_SPECIAL_EST = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'EST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CDT = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'CDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CST = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'CST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MDT = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'MDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MST = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'MST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PDT = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'PDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PST = DateTimeFormatter.ofPattern("E, d LLL yyyy H:m:s 'PST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-8));
|
||||
|
||||
RFC_1123_DATE_TIME_SPECIAL_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s z", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_GMT_OFFSET_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s O", Locale.ENGLISH);
|
||||
RFC_822_DATE_TIME_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s X", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_SPECIAL_EDT_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'EDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-4));
|
||||
RFC_1123_DATE_TIME_SPECIAL_EST_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'EST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CDT_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'CDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CST_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'CST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MDT_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'MDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MST_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'MST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PDT_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'PDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PST_FULL_DOW = DateTimeFormatter.ofPattern("EEEE, d LLL yyyy H:m:s 'PST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-8));
|
||||
|
||||
RFC_1123_DATE_TIME_SPECIAL_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s z", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_GMT_OFFSET_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s O", Locale.ENGLISH);
|
||||
RFC_822_DATE_TIME_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s X", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_SPECIAL_EDT_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'EDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-4));
|
||||
RFC_1123_DATE_TIME_SPECIAL_EST_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'EST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CDT_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'CDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CST_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'CST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MDT_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'MDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MST_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'MST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PDT_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'PDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PST_FULL_DOW_MONTH = DateTimeFormatter.ofPattern("EEEE, d LLLL yyyy H:m:s 'PST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-8));
|
||||
|
||||
|
||||
RFC_1123_DATE_TIME_SPECIAL_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s z", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_GMT_OFFSET_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s O", Locale.ENGLISH);
|
||||
RFC_822_DATE_TIME_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s X", Locale.ENGLISH);
|
||||
RFC_1123_DATE_TIME_SPECIAL_EDT_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'EDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-4));
|
||||
RFC_1123_DATE_TIME_SPECIAL_EST_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'EST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CDT_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'CDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-5));
|
||||
RFC_1123_DATE_TIME_SPECIAL_CST_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'CST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MDT_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'MDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-6));
|
||||
RFC_1123_DATE_TIME_SPECIAL_MST_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'MST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PDT_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'PDT'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-7));
|
||||
RFC_1123_DATE_TIME_SPECIAL_PST_NO_DOW = DateTimeFormatter.ofPattern("d LLL yyyy H:m:s 'PST'", Locale.ENGLISH).withZone(ZoneOffset.ofHours(-8));
|
||||
|
||||
DATE_TIME_SPECIAL_1 = DateTimeFormatter.ofPattern("d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates DateTime object for converting timestamps.
|
||||
* Using default timezone UTC if no timezone information if found in timestamp.
|
||||
*/
|
||||
public DateTime() {
|
||||
defaultZone = ZoneId.of("UTC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates DateTime object for converting timestamps.
|
||||
* @param defaultZone time zone to use if no timezone information if found in timestamp
|
||||
*/
|
||||
public DateTime(ZoneId defaultZone) {
|
||||
this.defaultZone = defaultZone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts date time string to LocalDateTime object. Note any time zone information in date time string is ignored.
|
||||
* @param dateTime date time string
|
||||
* @return local date time object
|
||||
*/
|
||||
public LocalDateTime toLocalDateTime(String dateTime) {
|
||||
var zonedDateTime = toZonedDateTime(dateTime);
|
||||
if (zonedDateTime == null) {
|
||||
return null;
|
||||
}
|
||||
return zonedDateTime.toLocalDateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts date time string to ZonedDateTime object. Use if time date string contains time zone information.
|
||||
* @param dateTime date time string
|
||||
* @return zoned date time object
|
||||
*/
|
||||
@SuppressWarnings("java:S3776")
|
||||
public ZonedDateTime toZonedDateTime(String dateTime) {
|
||||
if (dateTime == null)
|
||||
return null;
|
||||
|
||||
DateTimeFormatter formatter = getDateTimeFormatter(dateTime, false);
|
||||
|
||||
if (formatter == null) {
|
||||
throw new IllegalArgumentException("Unknown date time format " + dateTime);
|
||||
}
|
||||
|
||||
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 {
|
||||
if (isDateOnly(formatter)) {
|
||||
LocalDate date = LocalDate.parse(dateTime, formatter);
|
||||
return ZonedDateTime.of(date, LocalTime.MIDNIGHT, defaultZone);
|
||||
} else {
|
||||
return ZonedDateTime.parse(dateTime, formatter);
|
||||
}
|
||||
} catch (DateTimeParseException e) {
|
||||
int index = dateTime.indexOf(',');
|
||||
if (index != -1 && e.getMessage().contains("Conflict found: Field DayOfWeek")) {
|
||||
// Handel date time with incorrect day of week
|
||||
String newDateTime = dateTime.substring(index+1).trim();
|
||||
try {
|
||||
DateTimeFormatter newFormatter = getDateTimeFormatter(newDateTime, true);
|
||||
return ZonedDateTime.parse(newDateTime, newFormatter);
|
||||
} catch (DateTimeParseException e2) {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDateOnly(DateTimeFormatter formatter) {
|
||||
return formatter == ISO_LOCAL_DATE || formatter == BASIC_ISO_DATE;
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3776")
|
||||
private static DateTimeFormatter getDateTimeFormatter(String dateTime, boolean skipEndOfWeekPart) {
|
||||
if (skipEndOfWeekPart) {
|
||||
return parseRfcDateTimeNoDayOfWeek(dateTime);
|
||||
}
|
||||
|
||||
int index = dateTime.indexOf(',');
|
||||
|
||||
if (index == -1) {
|
||||
index = dateTime.indexOf(' ');
|
||||
if (Character.isDigit(dateTime.charAt(0)) && (index == 1 || index == 2)) {
|
||||
return DATE_TIME_SPECIAL_1;
|
||||
}
|
||||
return parseIsoDateTime(dateTime);
|
||||
} else if (index <= 3) {
|
||||
return parseRfcDateTime(dateTime);
|
||||
} else {
|
||||
if (isLongMonthText(dateTime)) {
|
||||
return parseRfcDateTimeLongDayOfWeekLongMonth(dateTime);
|
||||
} else {
|
||||
return parseRfcDateTimeLongDayOfWeek(dateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLongMonthText(String dateTime) {
|
||||
int index1 = dateTime.indexOf(' ');
|
||||
int index2 = dateTime.indexOf(' ', index1 + 1);
|
||||
int index3 = dateTime.indexOf(' ', index2 + 1);
|
||||
|
||||
return (index3 - index2 - 1) > 3;
|
||||
}
|
||||
|
||||
private static DateTimeFormatter parseIsoDateTime(String dateTime) {
|
||||
if (dateTime.length() == 24 && dateTime.charAt(4) == '-' && dateTime.charAt(10) == 'T' && (dateTime.charAt(dateTime.length() - 5) == '-' || dateTime.charAt(dateTime.length() - 5) == '+'))
|
||||
return ISO_OFFSET_DATE_TIME_SPECIAL;
|
||||
else if (dateTime.length() >= 20 && dateTime.length() <= 35 && dateTime.charAt(4) == '-' && dateTime.charAt(10) == 'T') // && dateTime.charAt(dateTime.length() - 3) == ':')
|
||||
return ISO_OFFSET_DATE_TIME;
|
||||
else if (dateTime.length() == 19 && dateTime.charAt(10) == 'T')
|
||||
return ISO_LOCAL_DATE_TIME;
|
||||
else if (dateTime.length() == 19 && dateTime.charAt(10) == ' ')
|
||||
return ISO_LOCAL_DATE_TIME_SPECIAL;
|
||||
else if (dateTime.length() == 10 && dateTime.charAt(4) == '-' && dateTime.charAt(7) == '-')
|
||||
return ISO_LOCAL_DATE;
|
||||
else if (dateTime.length() == 8)
|
||||
return BASIC_ISO_DATE;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3776")
|
||||
private static DateTimeFormatter parseRfcDateTime(String dateTime) {
|
||||
if ((dateTime.length() == 28 || dateTime.length() == 29) && dateTime.charAt(3) == ',' && dateTime.endsWith(" UTC"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" EDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EDT;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" EST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EST;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" CDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CDT;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" CST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CST;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" MDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MDT;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" MST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MST;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" PDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PDT;
|
||||
else if (dateTime.length() <= 29 && dateTime.charAt(3) == ',' && dateTime.endsWith(" PST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PST;
|
||||
else if (dateTime.length() > 31 && dateTime.contains("GMT") && !dateTime.endsWith("GMT") && dateTime.lastIndexOf(':') < 29)
|
||||
return RFC_1123_DATE_TIME_GMT_AND_OFFSET;
|
||||
else if (dateTime.length() >= 28 && dateTime.length() <= 35 && dateTime.charAt(3) == ',' && dateTime.contains("GMT"))
|
||||
return RFC_1123_DATE_TIME_GMT_OFFSET;
|
||||
else if ((dateTime.length() == 28 || dateTime.length() == 29) && dateTime.charAt(3) == ',' && (dateTime.charAt(13) == ' ' || dateTime.charAt(14) == ' '))
|
||||
return RFC_822_DATE_TIME;
|
||||
else if (dateTime.length() >= 28 && dateTime.length() <= 32) {
|
||||
if ((dateTime.contains(" +") || dateTime.contains(" -")) && dateTime.charAt(dateTime.length()-3) == ':')
|
||||
return RFC_1123_DATE_TIME_TIMEZONE;
|
||||
if (dateTime.contains(" +") || dateTime.contains(" -"))
|
||||
return RFC_1123_DATE_TIME;
|
||||
else if (dateTime.contains("+") || dateTime.contains("-"))
|
||||
return RFC_1123_DATE_TIME_TIMEZONE2;
|
||||
else
|
||||
return RFC_1123_DATE_TIME_TIMEZONE;
|
||||
}
|
||||
else if ((dateTime.length() == 26 || dateTime.length() == 27) && dateTime.charAt(3) == ',' && dateTime.endsWith(" Z"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL;
|
||||
else if ((dateTime.length() == 24 || dateTime.length() == 25) && dateTime.charAt(3) == ',')
|
||||
return RFC_1123_DATE_TIME_NO_TIMEZONE;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3776")
|
||||
private static DateTimeFormatter parseRfcDateTimeLongDayOfWeek(String dateTime) {
|
||||
if (dateTime.endsWith(" UTC"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_FULL_DOW;
|
||||
else if (dateTime.endsWith(" EDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EDT_FULL_DOW;
|
||||
else if (dateTime.endsWith(" EST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EST_FULL_DOW;
|
||||
else if (dateTime.endsWith(" CDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CDT_FULL_DOW;
|
||||
else if (dateTime.endsWith(" CST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CST_FULL_DOW;
|
||||
else if (dateTime.endsWith(" MDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MDT_FULL_DOW;
|
||||
else if (dateTime.endsWith(" MST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MST_FULL_DOW;
|
||||
else if (dateTime.endsWith(" PDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PDT_FULL_DOW;
|
||||
else if (dateTime.endsWith(" PST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PST_FULL_DOW;
|
||||
else if (dateTime.contains("GMT"))
|
||||
return RFC_1123_DATE_TIME_GMT_OFFSET_FULL_DOW;
|
||||
else if (dateTime.endsWith(" Z"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_FULL_DOW;
|
||||
else if (dateTime.charAt(3) != ',' && dateTime.charAt(dateTime.length() - 3) == ':' && dateTime.charAt(dateTime.length() - 7) == ' ')
|
||||
return RFC_1123_DATE_TIME_SPECIAL_FULL_DOW;
|
||||
else if (dateTime.contains("-") ||dateTime.contains("+"))
|
||||
return RFC_822_DATE_TIME_FULL_DOW;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3776")
|
||||
private static DateTimeFormatter parseRfcDateTimeLongDayOfWeekLongMonth(String dateTime) {
|
||||
if (dateTime.endsWith(" UTC"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" EDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EDT_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" EST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EST_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" CDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CDT_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" CST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CST_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" MDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MDT_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" MST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MST_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" PDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PDT_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" PST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PST_FULL_DOW_MONTH;
|
||||
else if (dateTime.contains("GMT"))
|
||||
return RFC_1123_DATE_TIME_GMT_OFFSET_FULL_DOW_MONTH;
|
||||
else if (dateTime.endsWith(" Z"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_FULL_DOW_MONTH;
|
||||
else if (dateTime.charAt(3) != ',' && dateTime.charAt(dateTime.length() - 3) == ':' && dateTime.charAt(dateTime.length() - 7) == ' ')
|
||||
return RFC_1123_DATE_TIME_SPECIAL_FULL_DOW_MONTH;
|
||||
else if (dateTime.contains("-") ||dateTime.contains("+"))
|
||||
return RFC_822_DATE_TIME_FULL_DOW_MONTH;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3776")
|
||||
private static DateTimeFormatter parseRfcDateTimeNoDayOfWeek(String dateTime) {
|
||||
if (dateTime.endsWith(" UTC"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_NO_DOW;
|
||||
else if (dateTime.endsWith(" EDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EDT_NO_DOW;
|
||||
else if (dateTime.endsWith(" EST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_EST_NO_DOW;
|
||||
else if (dateTime.endsWith(" CDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CDT_NO_DOW;
|
||||
else if (dateTime.endsWith(" CST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_CST_NO_DOW;
|
||||
else if (dateTime.endsWith(" MDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MDT_NO_DOW;
|
||||
else if (dateTime.endsWith(" MST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_MST_NO_DOW;
|
||||
else if (dateTime.endsWith(" PDT"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PDT_NO_DOW;
|
||||
else if (dateTime.endsWith(" PST"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_PST_NO_DOW;
|
||||
else if (dateTime.contains("GMT"))
|
||||
return RFC_1123_DATE_TIME_GMT_OFFSET_NO_DOW;
|
||||
else if (dateTime.endsWith(" Z"))
|
||||
return RFC_1123_DATE_TIME_SPECIAL_NO_DOW;
|
||||
else if (dateTime.contains("-") ||dateTime.contains("+"))
|
||||
return RFC_822_DATE_TIME_NO_DOW;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert date time string to time in milliseconds
|
||||
* @param dateTime date time string
|
||||
* @return time in milliseconds
|
||||
*/
|
||||
public Long toEpochMilli(String dateTime) {
|
||||
ZonedDateTime zonedDateTime = toZonedDateTime(dateTime);
|
||||
|
||||
if (zonedDateTime == null)
|
||||
return null;
|
||||
|
||||
return zonedDateTime.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts timestamp string to Instant
|
||||
* @param dateTime timestamp
|
||||
* @return instant
|
||||
*/
|
||||
public Instant toInstant(String dateTime) {
|
||||
ZonedDateTime zonedDateTime = toZonedDateTime(dateTime);
|
||||
|
||||
if (zonedDateTime == null)
|
||||
return null;
|
||||
|
||||
return zonedDateTime.toInstant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator comparing publication date of Item class. Sorted in ascending order (oldest first)
|
||||
*
|
||||
* @deprecated
|
||||
* This method be removed in a future version.
|
||||
* <p> Use {@link ItemComparator#oldestItemFirst()} instead.
|
||||
*
|
||||
* @return comparator
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.3.0", forRemoval=true)
|
||||
public static Comparator<Item> pubDateComparator() {
|
||||
var dateTime = Default.getDateTimeParser();
|
||||
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 toZonedDateTime(timestamp);
|
||||
}
|
||||
}
|
47
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/DateTimeParser.java
vendored
Normal file
47
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/DateTimeParser.java
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.Instant;
|
||||
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);
|
||||
|
||||
/**
|
||||
* Converts a timestamp in String format to an Instant
|
||||
* @param dateTime timestamp
|
||||
* @return Instant
|
||||
*/
|
||||
Instant toInstant(String dateTime);
|
||||
}
|
98
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Enclosure.java
vendored
Normal file
98
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Enclosure.java
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class representing the Enclosure.
|
||||
*/
|
||||
public class Enclosure {
|
||||
private String url;
|
||||
private String type;
|
||||
private Long length;
|
||||
|
||||
/**
|
||||
* Get the URL of enclosure.
|
||||
* @return url
|
||||
*/
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL of the enclosure.
|
||||
* @param url URL
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of enclosure.
|
||||
* @return type
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type of the enclosure.
|
||||
* @param type type
|
||||
*/
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of enclosure.
|
||||
* @return length
|
||||
*/
|
||||
public Optional<Long> getLength() {
|
||||
return Optional.ofNullable(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of the enclosure.
|
||||
* @param length length
|
||||
*/
|
||||
public void setLength(Long length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Enclosure enclosure = (Enclosure) o;
|
||||
return Objects.equals(getUrl(), enclosure.getUrl()) && Objects.equals(getType(), enclosure.getType()) && Objects.equals(getLength(), enclosure.getLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getUrl(), getType(), getLength());
|
||||
}
|
||||
|
||||
}
|
150
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Image.java
vendored
Normal file
150
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Image.java
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class representing a image in channel.
|
||||
*/
|
||||
public class Image {
|
||||
private String title;
|
||||
private String link;
|
||||
private String url;
|
||||
private String description;
|
||||
private Integer height;
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* Get title that describes the image.
|
||||
* @return title
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set title that describes the image.
|
||||
* @param title title
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the site.
|
||||
* @return link
|
||||
*/
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL of the site.
|
||||
* @param link link
|
||||
*/
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of a GIF, JPEG or PNG image that represents the channel.
|
||||
* @return url to image
|
||||
*/
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL of a GIF, JPEG or PNG image that represents the channel.
|
||||
* @param url url to image
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description.
|
||||
* @return description
|
||||
*/
|
||||
public Optional<String> getDescription() {
|
||||
return Optional.ofNullable(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the description.
|
||||
* @param description description
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of the image.
|
||||
* @return image height
|
||||
*/
|
||||
public Optional<Integer> getHeight() {
|
||||
return Optional.ofNullable(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the height of the image.
|
||||
* @param height image height
|
||||
*/
|
||||
public void setHeight(Integer height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of the image.
|
||||
* @return image width
|
||||
*/
|
||||
public Optional<Integer> getWidth() {
|
||||
return Optional.ofNullable(width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the image.
|
||||
* @param width image width
|
||||
*/
|
||||
public void setWidth(Integer width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Image image = (Image) o;
|
||||
return Objects.equals(getTitle(), image.getTitle()) && Objects.equals(getLink(), image.getLink()) &&
|
||||
Objects.equals(getUrl(), image.getUrl()) && Objects.equals(getDescription(), image.getDescription()) &&
|
||||
Objects.equals(getHeight(), image.getHeight()) && Objects.equals(getWidth(), image.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getTitle(), getLink(), getUrl(), getDescription(), getHeight(), getWidth());
|
||||
}
|
||||
}
|
434
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Item.java
vendored
Normal file
434
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/Item.java
vendored
Normal file
@ -0,0 +1,434 @@
|
||||
/*
|
||||
* 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 com.apptasticsoftware.rssreader.util.Default;
|
||||
import com.apptasticsoftware.rssreader.util.ItemComparator;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class representing a RSS item. A channel may contain any number of items. An item may represent a "story" -- much
|
||||
* like a story in a newspaper or magazine; if so its description is a synopsis of the story, and the link points
|
||||
* to the full story.
|
||||
*/
|
||||
@SuppressWarnings("javaarchitecture:S7091")
|
||||
public class Item implements Comparable<Item> {
|
||||
private final Comparator<Item> defaultComparator;
|
||||
private String title;
|
||||
private String description;
|
||||
private String content;
|
||||
private String link;
|
||||
private String author;
|
||||
private String category;
|
||||
private final List<String> categories = new ArrayList<>();
|
||||
private String guid;
|
||||
private Boolean isPermaLink;
|
||||
private String pubDate;
|
||||
private String updated;
|
||||
private String comments;
|
||||
private Enclosure enclosure;
|
||||
private final List<Enclosure> enclosures = new ArrayList<>();
|
||||
private Channel channel;
|
||||
private final DateTimeParser dateTimeParser;
|
||||
|
||||
/**
|
||||
* Constructor for Item
|
||||
* @deprecated
|
||||
* Use {@link Item#Item(DateTimeParser)} instead.
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.5.0", forRemoval=true)
|
||||
public Item() {
|
||||
dateTimeParser = Default.getDateTimeParser();
|
||||
defaultComparator = ItemComparator.newestPublishedItemFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for Item
|
||||
* @param dateTimeParser dateTimeParser
|
||||
*/
|
||||
public Item(DateTimeParser dateTimeParser) {
|
||||
this.dateTimeParser = dateTimeParser;
|
||||
defaultComparator = ItemComparator.newestPublishedItemFirst(dateTimeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title of the item.
|
||||
*
|
||||
* @return title
|
||||
*/
|
||||
public Optional<String> getTitle() {
|
||||
return Optional.ofNullable(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the item.
|
||||
*
|
||||
* @param title title
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
// Speculative XML sanitization; Marginalia modification in case we get HTML in the title
|
||||
|
||||
if (title != null && title.startsWith("<")) {
|
||||
StringBuilder sanitized = new StringBuilder();
|
||||
for (int i = 0; i < title.length(); i++) {
|
||||
char c = title.charAt(i);
|
||||
if (c == '<') {
|
||||
// find matching bracket
|
||||
for (i=i+1; i < title.length() && title.charAt(i) != '>'; i++);
|
||||
}
|
||||
else {
|
||||
sanitized.append(c);
|
||||
}
|
||||
}
|
||||
title = sanitized.toString();
|
||||
}
|
||||
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item synopsis.
|
||||
*
|
||||
* @return description
|
||||
*/
|
||||
public Optional<String> getDescription() {
|
||||
return Optional.ofNullable(description).or(this::getContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the item synopsis.
|
||||
*
|
||||
* @param description description
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item content.
|
||||
*
|
||||
* @return content
|
||||
*/
|
||||
public Optional<String> getContent() {
|
||||
return Optional.ofNullable(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the item content.
|
||||
*
|
||||
* @param content content
|
||||
*/
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the item.
|
||||
*
|
||||
* @return link
|
||||
*/
|
||||
public Optional<String> getLink() {
|
||||
return Optional.ofNullable(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL of the item.
|
||||
*
|
||||
* @param link link
|
||||
*/
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email address of the author of the item.
|
||||
*
|
||||
* @return author
|
||||
*/
|
||||
public Optional<String> getAuthor() {
|
||||
return Optional.ofNullable(author);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set email address of the author of the item.
|
||||
*
|
||||
* @param author author
|
||||
*/
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category for item.
|
||||
*
|
||||
* @deprecated
|
||||
* This method be removed in a future version.
|
||||
* <p> Use {@link Item#getCategories()} instead.
|
||||
*
|
||||
* @return category
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.3.0", forRemoval=true)
|
||||
public Optional<String> getCategory() {
|
||||
return Optional.ofNullable(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set category for item.
|
||||
*
|
||||
* @deprecated
|
||||
* This method be removed in a future version.
|
||||
* <p> Use {@link Item#addCategory(String category)} instead.
|
||||
*
|
||||
* @param category category
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
@Deprecated(since="3.3.0", forRemoval=true)
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories for item.
|
||||
* @return list of categories
|
||||
*/
|
||||
public List<String> getCategories() {
|
||||
return Collections.unmodifiableList(categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add category for item.
|
||||
* @param category category
|
||||
*/
|
||||
public void addCategory(String category) {
|
||||
this.category = category;
|
||||
categories.add(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string that uniquely identifies the item.
|
||||
*
|
||||
* @return guid
|
||||
*/
|
||||
public Optional<String> getGuid() {
|
||||
return Optional.ofNullable(guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a string that uniquely identifies the item.
|
||||
*
|
||||
* @param guid guid
|
||||
*/
|
||||
public void setGuid(String guid) {
|
||||
this.guid = guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the guid element has an attribute named "isPermaLink" with a value of true, the reader may assume that
|
||||
* it is a permalink to the item, that is, an url that can be opened in a Web browser, that points to the full
|
||||
* item described by the item element.
|
||||
*
|
||||
* @return permanent link
|
||||
*/
|
||||
public Optional<Boolean> getIsPermaLink() {
|
||||
return Optional.ofNullable(isPermaLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the guid element has an attribute named "isPermaLink" with a value of true, the reader may assume that
|
||||
* it is a permalink to the item, that is, an url that can be opened in a Web browser, that points to the full
|
||||
* item described by the item element.
|
||||
*
|
||||
* @param isPermaLink is perma link
|
||||
*/
|
||||
public void setIsPermaLink(boolean isPermaLink) {
|
||||
this.isPermaLink = isPermaLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string that indicates when the item was published.
|
||||
*
|
||||
* @return publication date
|
||||
*/
|
||||
public Optional<String> getPubDate() {
|
||||
return Optional.ofNullable(pubDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a string that indicates when the item was published.
|
||||
*
|
||||
* @param pubDate publication date
|
||||
*/
|
||||
public void setPubDate(String pubDate) {
|
||||
this.pubDate = pubDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a ZonedDateTime that indicates when the item was published.
|
||||
*
|
||||
* @return publication date
|
||||
*/
|
||||
public Optional<ZonedDateTime> getPubDateZonedDateTime() {
|
||||
return getPubDate().map(dateTimeParser::parse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string that indicates when the item was updated.
|
||||
*
|
||||
* @return updated date
|
||||
*/
|
||||
public Optional<String> getUpdated() {
|
||||
return Optional.ofNullable(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a string that indicates when the item was updated.
|
||||
*
|
||||
* @param updated updated date
|
||||
*/
|
||||
public void setUpdated(String updated) {
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a ZonedDateTime that indicates when the item was updated.
|
||||
*
|
||||
* @return publication date
|
||||
*/
|
||||
public Optional<ZonedDateTime> getUpdatedZonedDateTime() {
|
||||
return getUpdated().map(dateTimeParser::parse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comments relating to the item.
|
||||
* @return comments
|
||||
*/
|
||||
public Optional<String> getComments() {
|
||||
return Optional.ofNullable(comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comments relating to the item.
|
||||
* @param comments comments
|
||||
*/
|
||||
public void setComments(String comments) {
|
||||
this.comments = comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enclosure of the item.
|
||||
*
|
||||
* @return enclosure
|
||||
*/
|
||||
public Optional<Enclosure> getEnclosure() {
|
||||
return Optional.ofNullable(enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the enclosure of the item.
|
||||
*
|
||||
* @param enclosure enclosure
|
||||
*/
|
||||
public void setEnclosure(Enclosure enclosure) {
|
||||
addEnclosure(enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enclosures for item.
|
||||
* Use this method if multiple enclosures exist per item.
|
||||
* @return list of enclosures
|
||||
*/
|
||||
public List<Enclosure> getEnclosures() {
|
||||
return Collections.unmodifiableList(enclosures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add enclosure for item.
|
||||
* @param enclosure enclosure
|
||||
*/
|
||||
public void addEnclosure(Enclosure enclosure) {
|
||||
this.enclosure = enclosure;
|
||||
enclosures.add(enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel that this item was published in.
|
||||
*
|
||||
* @return channel
|
||||
*/
|
||||
public Channel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the channel that this item was published in.
|
||||
*
|
||||
* @param channel channel
|
||||
*/
|
||||
public void setChannel(Channel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Item item = (Item) o;
|
||||
return Objects.equals(getTitle(), item.getTitle()) &&
|
||||
Objects.equals(getDescription(), item.getDescription()) &&
|
||||
Objects.equals(getContent(), item.getContent()) &&
|
||||
Objects.equals(getLink(), item.getLink()) &&
|
||||
Objects.equals(getAuthor(), item.getAuthor()) &&
|
||||
getCategories().equals(item.getCategories()) &&
|
||||
Objects.equals(getGuid(), item.getGuid()) &&
|
||||
Objects.equals(getIsPermaLink(), item.getIsPermaLink()) &&
|
||||
Objects.equals(getPubDate(), item.getPubDate()) &&
|
||||
Objects.equals(getUpdated(), item.getUpdated()) &&
|
||||
Objects.equals(getComments(), item.getComments()) &&
|
||||
getEnclosures().equals(item.getEnclosures()) &&
|
||||
Objects.equals(getChannel(), item.getChannel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getTitle(), getDescription(), getContent(), getLink(), getAuthor(), getCategories(),
|
||||
getGuid(), getIsPermaLink(), getPubDate(), getUpdated(), getComments(), getEnclosures(), getChannel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares publication time of two {@code Item} objects.
|
||||
*
|
||||
* @param o item to compare
|
||||
* @return value
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Item o) {
|
||||
return defaultComparator.compare(this, o);
|
||||
}
|
||||
}
|
58
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/RssReader.java
vendored
Normal file
58
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/RssReader.java
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.net.http.HttpClient;
|
||||
|
||||
/**
|
||||
* Class for reading RSS (Rich Site Summary) and Atom types of web feeds.
|
||||
*/
|
||||
public class RssReader extends AbstractRssReader<Channel, Item> {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public RssReader() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param httpClient http client
|
||||
*/
|
||||
public RssReader(HttpClient httpClient) {
|
||||
super(httpClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Channel createChannel(DateTimeParser dateTimeParser) {
|
||||
return new Channel(dateTimeParser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Item createItem(DateTimeParser dateTimeParser) {
|
||||
return new Item(dateTimeParser);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.apptasticsoftware.rssreader.internal;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
/**
|
||||
* Thread factory that creates daemon threads
|
||||
*/
|
||||
public class DaemonThreadFactory implements ThreadFactory {
|
||||
private final String name;
|
||||
private int counter;
|
||||
|
||||
public DaemonThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r, name + "-" + counter++);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
|
||||
}
|
52
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/internal/StreamUtil.java
vendored
Normal file
52
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/internal/StreamUtil.java
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* Internal utility class for working with streams.
|
||||
*/
|
||||
public class StreamUtil {
|
||||
|
||||
private StreamUtil() { }
|
||||
|
||||
/**
|
||||
* Creates a Stream from an Iterator.
|
||||
*
|
||||
* @param iterator The Iterator to create the Stream from.
|
||||
* @param <T> The type of the elements in the Stream.
|
||||
* @return A Stream created from the Iterator.
|
||||
*/
|
||||
public static <T> Stream<T> asStream(Iterator<T> iterator) {
|
||||
requireNonNull(iterator);
|
||||
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal;
|
||||
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* This type exposes helper methods that will help defend against XXE attacks in {@link
|
||||
* XMLInputFactory}.
|
||||
*
|
||||
* <p>For more on XXE:
|
||||
*
|
||||
* <p><a
|
||||
* href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">XXE
|
||||
* OWASP CheatSheet</a>
|
||||
*/
|
||||
public class XMLInputFactorySecurity {
|
||||
|
||||
private XMLInputFactorySecurity() {}
|
||||
|
||||
public static XMLInputFactory hardenFactory(final XMLInputFactory factory) {
|
||||
Objects.requireNonNull(factory);
|
||||
// disable XML external entity (XXE) processing
|
||||
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
|
||||
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
|
||||
return factory;
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal.stream;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
@SuppressWarnings("javaarchitecture:S7027")
|
||||
public class AbstractAutoCloseStream<T, S extends BaseStream<T, S>> implements AutoCloseable {
|
||||
private final S stream;
|
||||
private final AtomicBoolean isClosed;
|
||||
|
||||
AbstractAutoCloseStream(S stream) {
|
||||
this.stream = Objects.requireNonNull(stream);
|
||||
this.isClosed = new AtomicBoolean();
|
||||
}
|
||||
|
||||
protected S stream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (isClosed.compareAndSet(false,true)) {
|
||||
stream().close();
|
||||
}
|
||||
}
|
||||
|
||||
<R> R autoClose(Function<S, R> function) {
|
||||
try (S s = stream()) {
|
||||
return function.apply(s);
|
||||
}
|
||||
}
|
||||
|
||||
<U> Stream<U> asAutoCloseStream(Stream<U> stream) {
|
||||
return asAutoCloseStream(stream, AutoCloseStream::new);
|
||||
}
|
||||
|
||||
IntStream asAutoCloseStream(IntStream stream) {
|
||||
return asAutoCloseStream(stream, AutoCloseIntStream::new);
|
||||
}
|
||||
|
||||
LongStream asAutoCloseStream(LongStream stream) {
|
||||
return asAutoCloseStream(stream, AutoCloseLongStream::new);
|
||||
}
|
||||
|
||||
DoubleStream asAutoCloseStream(DoubleStream stream) {
|
||||
return asAutoCloseStream(stream, AutoCloseDoubleStream::new);
|
||||
}
|
||||
|
||||
private <U> U asAutoCloseStream(U stream, UnaryOperator<U> wrapper) {
|
||||
if (stream instanceof AbstractAutoCloseStream) {
|
||||
return stream;
|
||||
}
|
||||
return wrapper.apply(stream);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal.stream;
|
||||
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.PrimitiveIterator;
|
||||
import java.util.Spliterator;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AutoCloseDoubleStream extends AbstractAutoCloseStream<Double, DoubleStream> implements DoubleStream {
|
||||
|
||||
AutoCloseDoubleStream(DoubleStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream filter(DoublePredicate predicate) {
|
||||
return asAutoCloseStream(stream().filter(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream map(DoubleUnaryOperator mapper) {
|
||||
return asAutoCloseStream(stream().map(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper) {
|
||||
return asAutoCloseStream(stream().mapToObj(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream mapToInt(DoubleToIntFunction mapper) {
|
||||
return asAutoCloseStream(stream().mapToInt(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream mapToLong(DoubleToLongFunction mapper) {
|
||||
return asAutoCloseStream(stream().mapToLong(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream flatMap(DoubleFunction<? extends DoubleStream> mapper) {
|
||||
return asAutoCloseStream(stream().flatMap(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream distinct() {
|
||||
return asAutoCloseStream(stream().distinct());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream sorted() {
|
||||
return asAutoCloseStream(stream().sorted());
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3864")
|
||||
@Override
|
||||
public DoubleStream peek(DoubleConsumer action) {
|
||||
return asAutoCloseStream(stream().peek(action));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream limit(long maxSize) {
|
||||
return asAutoCloseStream(stream().limit(maxSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream skip(long n) {
|
||||
return asAutoCloseStream(stream().skip(n));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(DoubleConsumer action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEach(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEachOrdered(DoubleConsumer action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEachOrdered(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] toArray() {
|
||||
return autoClose(DoubleStream::toArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double reduce(double identity, DoubleBinaryOperator op) {
|
||||
return autoClose(stream -> stream.reduce(identity, op));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble reduce(DoubleBinaryOperator op) {
|
||||
return autoClose(stream -> stream.reduce(op));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R collect(Supplier<R> supplier, ObjDoubleConsumer<R> accumulator, BiConsumer<R, R> combiner) {
|
||||
return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sum() {
|
||||
return autoClose(DoubleStream::sum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble min() {
|
||||
return autoClose(DoubleStream::min);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble max() {
|
||||
return autoClose(DoubleStream::max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return autoClose(DoubleStream::count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble average() {
|
||||
return autoClose(DoubleStream::average);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleSummaryStatistics summaryStatistics() {
|
||||
return autoClose(DoubleStream::summaryStatistics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean anyMatch(DoublePredicate predicate) {
|
||||
return autoClose(stream -> stream.anyMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allMatch(DoublePredicate predicate) {
|
||||
return autoClose(stream -> stream.allMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean noneMatch(DoublePredicate predicate) {
|
||||
return autoClose(stream -> stream.noneMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble findFirst() {
|
||||
return autoClose(DoubleStream::findFirst);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble findAny() {
|
||||
return autoClose(DoubleStream::findAny);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Double> boxed() {
|
||||
return asAutoCloseStream(stream().boxed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream sequential() {
|
||||
return asAutoCloseStream(stream().sequential());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream parallel() {
|
||||
return asAutoCloseStream(stream().parallel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrimitiveIterator.OfDouble iterator() {
|
||||
return stream().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator.OfDouble spliterator() {
|
||||
return stream().spliterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel() {
|
||||
return stream().isParallel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream unordered() {
|
||||
return asAutoCloseStream(stream().unordered());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream onClose(Runnable closeHandler) {
|
||||
return asAutoCloseStream(stream().onClose(closeHandler));
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal.stream;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AutoCloseIntStream extends AbstractAutoCloseStream<Integer, IntStream> implements IntStream {
|
||||
|
||||
AutoCloseIntStream(IntStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream filter(IntPredicate predicate) {
|
||||
return asAutoCloseStream(stream().filter(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream map(IntUnaryOperator mapper) {
|
||||
return asAutoCloseStream(stream().map(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Stream<U> mapToObj(IntFunction<? extends U> mapper) {
|
||||
return asAutoCloseStream(stream().mapToObj(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream mapToLong(IntToLongFunction mapper) {
|
||||
return asAutoCloseStream(stream().mapToLong(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream mapToDouble(IntToDoubleFunction mapper) {
|
||||
return asAutoCloseStream(stream().mapToDouble(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream flatMap(IntFunction<? extends IntStream> mapper) {
|
||||
return asAutoCloseStream(stream().flatMap(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream distinct() {
|
||||
return asAutoCloseStream(stream().distinct());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream sorted() {
|
||||
return asAutoCloseStream(stream().sorted());
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3864")
|
||||
@Override
|
||||
public IntStream peek(IntConsumer action) {
|
||||
return asAutoCloseStream(stream().peek(action));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream limit(long maxSize) {
|
||||
return asAutoCloseStream(stream().limit(maxSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream skip(long n) {
|
||||
return asAutoCloseStream(stream().skip(n));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(IntConsumer action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEach(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEachOrdered(IntConsumer action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEachOrdered(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] toArray() {
|
||||
return autoClose(IntStream::toArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int reduce(int identity, IntBinaryOperator op) {
|
||||
return autoClose(stream -> stream.reduce(identity, op));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt reduce(IntBinaryOperator op) {
|
||||
return autoClose(stream -> stream.reduce(op));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R, R> combiner) {
|
||||
return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sum() {
|
||||
return autoClose(IntStream::sum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt min() {
|
||||
return autoClose(IntStream::min);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt max() {
|
||||
return autoClose(IntStream::max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return autoClose(IntStream::count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble average() {
|
||||
return autoClose(IntStream::average);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntSummaryStatistics summaryStatistics() {
|
||||
return autoClose(IntStream::summaryStatistics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean anyMatch(IntPredicate predicate) {
|
||||
return autoClose(stream -> stream.anyMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allMatch(IntPredicate predicate) {
|
||||
return autoClose(stream -> stream.allMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean noneMatch(IntPredicate predicate) {
|
||||
return autoClose(stream -> stream.noneMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt findFirst() {
|
||||
return autoClose(IntStream::findFirst);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt findAny() {
|
||||
return autoClose(IntStream::findAny);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream asLongStream() {
|
||||
return asAutoCloseStream(stream().asLongStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream asDoubleStream() {
|
||||
return asAutoCloseStream(stream().asDoubleStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Integer> boxed() {
|
||||
return asAutoCloseStream(stream().boxed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream sequential() {
|
||||
return asAutoCloseStream(stream().sequential());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream parallel() {
|
||||
return asAutoCloseStream(stream().parallel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrimitiveIterator.OfInt iterator() {
|
||||
return stream().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator.OfInt spliterator() {
|
||||
return stream().spliterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel() {
|
||||
return stream().isParallel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream unordered() {
|
||||
return asAutoCloseStream(stream().unordered());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream onClose(Runnable closeHandler) {
|
||||
return asAutoCloseStream(stream().onClose(closeHandler));
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal.stream;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AutoCloseLongStream extends AbstractAutoCloseStream<Long, LongStream> implements LongStream {
|
||||
|
||||
AutoCloseLongStream(LongStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream filter(LongPredicate predicate) {
|
||||
return asAutoCloseStream(stream().filter(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream map(LongUnaryOperator mapper) {
|
||||
return asAutoCloseStream(stream().map(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Stream<U> mapToObj(LongFunction<? extends U> mapper) {
|
||||
return asAutoCloseStream(stream().mapToObj(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream mapToInt(LongToIntFunction mapper) {
|
||||
return asAutoCloseStream(stream().mapToInt(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream mapToDouble(LongToDoubleFunction mapper) {
|
||||
return asAutoCloseStream(stream().mapToDouble(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream flatMap(LongFunction<? extends LongStream> mapper) {
|
||||
return asAutoCloseStream(stream().flatMap(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream distinct() {
|
||||
return asAutoCloseStream(stream().distinct());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream sorted() {
|
||||
return asAutoCloseStream(stream().sorted());
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3864")
|
||||
@Override
|
||||
public LongStream peek(LongConsumer action) {
|
||||
return asAutoCloseStream(stream().peek(action));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream limit(long maxSize) {
|
||||
return asAutoCloseStream(stream().limit(maxSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream skip(long n) {
|
||||
return asAutoCloseStream(stream().skip(n));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(LongConsumer action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEach(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEachOrdered(LongConsumer action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEachOrdered(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public long[] toArray() {
|
||||
return autoClose(LongStream::toArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long reduce(long identity, LongBinaryOperator op) {
|
||||
return autoClose(stream -> stream.reduce(identity, op));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong reduce(LongBinaryOperator op) {
|
||||
return autoClose(stream -> stream.reduce(op));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R collect(Supplier<R> supplier, ObjLongConsumer<R> accumulator, BiConsumer<R, R> combiner) {
|
||||
return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long sum() {
|
||||
return autoClose(LongStream::sum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong min() {
|
||||
return autoClose(LongStream::min);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong max() {
|
||||
return autoClose(LongStream::max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return autoClose(LongStream::count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble average() {
|
||||
return autoClose(LongStream::average);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongSummaryStatistics summaryStatistics() {
|
||||
return autoClose(LongStream::summaryStatistics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean anyMatch(LongPredicate predicate) {
|
||||
return autoClose(stream -> stream.anyMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allMatch(LongPredicate predicate) {
|
||||
return autoClose(stream -> stream.allMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean noneMatch(LongPredicate predicate) {
|
||||
return autoClose(stream -> stream.noneMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong findFirst() {
|
||||
return autoClose(LongStream::findFirst);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong findAny() {
|
||||
return autoClose(LongStream::findAny);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream asDoubleStream() {
|
||||
return asAutoCloseStream(stream().asDoubleStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Long> boxed() {
|
||||
return asAutoCloseStream(stream().boxed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream sequential() {
|
||||
return asAutoCloseStream(stream().sequential());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream parallel() {
|
||||
return asAutoCloseStream(stream().parallel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrimitiveIterator.OfLong iterator() {
|
||||
return stream().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator.OfLong spliterator() {
|
||||
return stream().spliterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel() {
|
||||
return stream().isParallel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream unordered() {
|
||||
return asAutoCloseStream(stream().unordered());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream onClose(Runnable closeHandler) {
|
||||
return asAutoCloseStream(stream().onClose(closeHandler));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024, 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.internal.stream;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
/**
|
||||
* A Stream that automatically calls its {@link #close()} method after a terminating operation, such as limit(), forEach(), or collect(), has been executed.
|
||||
* <p>
|
||||
* This class is useful for working with streams that have resources that need to be closed, such as file streams or network connections.
|
||||
* <p>
|
||||
* If the {@link #close()} method is called manually, the stream will be closed and any subsequent operations will throw an {@link IllegalStateException}.
|
||||
* <p>
|
||||
* The {@link #iterator()} and {@link #spliterator()} methods are not supported by this class.
|
||||
*/
|
||||
@SuppressWarnings("javaarchitecture:S7027")
|
||||
public class AutoCloseStream<T> extends AbstractAutoCloseStream<T, Stream<T>> implements Stream<T> {
|
||||
|
||||
AutoCloseStream(Stream<T> stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AutoCloseStream from the given stream.
|
||||
* @param stream the stream to wrap
|
||||
* @return a new AutoCloseStream
|
||||
*/
|
||||
public static <T> AutoCloseStream<T> of(Stream<T> stream) {
|
||||
Objects.requireNonNull(stream);
|
||||
return new AutoCloseStream<>(stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> filter(Predicate<? super T> predicate) {
|
||||
return asAutoCloseStream(stream().filter(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> Stream<R> map(Function<? super T, ? extends R> mapper) {
|
||||
return asAutoCloseStream(stream().map(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream mapToInt(ToIntFunction<? super T> mapper) {
|
||||
return asAutoCloseStream(stream().mapToInt(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream mapToLong(ToLongFunction<? super T> mapper) {
|
||||
return asAutoCloseStream(stream().mapToLong(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) {
|
||||
return asAutoCloseStream(stream().mapToDouble(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) {
|
||||
return asAutoCloseStream(stream().flatMap(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) {
|
||||
return asAutoCloseStream(stream().flatMapToInt(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper) {
|
||||
return asAutoCloseStream(stream().flatMapToLong(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper) {
|
||||
return asAutoCloseStream(stream().flatMapToDouble(mapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> distinct() {
|
||||
return asAutoCloseStream(stream().distinct());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> sorted() {
|
||||
return asAutoCloseStream(stream().sorted());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> sorted(Comparator<? super T> comparator) {
|
||||
return asAutoCloseStream(stream().sorted(comparator));
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3864")
|
||||
@Override
|
||||
public Stream<T> peek(Consumer<? super T> action) {
|
||||
return asAutoCloseStream(stream().peek(action));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> limit(long maxSize) {
|
||||
return asAutoCloseStream(stream().limit(maxSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> skip(long n) {
|
||||
return asAutoCloseStream(stream().skip(n));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(Consumer<? super T> action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEach(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEachOrdered(Consumer<? super T> action) {
|
||||
autoClose(stream -> {
|
||||
stream.forEachOrdered(action);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
return autoClose(Stream::toArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A> A[] toArray(IntFunction<A[]> generator) {
|
||||
return autoClose(stream -> stream.toArray(generator));
|
||||
}
|
||||
|
||||
@Override
|
||||
public T reduce(T identity, BinaryOperator<T> accumulator) {
|
||||
return autoClose(stream -> stream.reduce(identity, accumulator));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> reduce(BinaryOperator<T> accumulator) {
|
||||
return autoClose(stream -> stream.reduce(accumulator));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) {
|
||||
return autoClose(stream -> stream.reduce(identity, accumulator, combiner));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) {
|
||||
return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R, A> R collect(Collector<? super T, A, R> collector) {
|
||||
return autoClose(stream -> stream.collect(collector));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> min(Comparator<? super T> comparator) {
|
||||
return autoClose(stream -> stream.min(comparator));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> max(Comparator<? super T> comparator) {
|
||||
return autoClose(stream -> stream.max(comparator));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return autoClose(Stream::count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean anyMatch(Predicate<? super T> predicate) {
|
||||
return autoClose(stream -> stream.anyMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allMatch(Predicate<? super T> predicate) {
|
||||
return autoClose(stream -> stream.allMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean noneMatch(Predicate<? super T> predicate) {
|
||||
return autoClose(stream -> stream.noneMatch(predicate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> findFirst() {
|
||||
return autoClose(Stream::findFirst);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> findAny() {
|
||||
return autoClose(Stream::findAny);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return stream().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator<T> spliterator() {
|
||||
return stream().spliterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel() {
|
||||
return stream().isParallel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> sequential() {
|
||||
return asAutoCloseStream(stream().sequential());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> parallel() {
|
||||
return asAutoCloseStream(stream().parallel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> unordered() {
|
||||
return asAutoCloseStream(stream().unordered());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> onClose(Runnable closeHandler) {
|
||||
return asAutoCloseStream(stream().onClose(closeHandler));
|
||||
}
|
||||
}
|
303
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/module/itunes/ItunesChannel.java
vendored
Normal file
303
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/module/itunes/ItunesChannel.java
vendored
Normal file
@ -0,0 +1,303 @@
|
||||
/*
|
||||
* 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.module.itunes;
|
||||
|
||||
import com.apptasticsoftware.rssreader.Channel;
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class representing the Itunes channel.
|
||||
*/
|
||||
public class ItunesChannel extends Channel {
|
||||
|
||||
private String itunesImage;
|
||||
private final List<String> itunesCategories = new ArrayList<>();
|
||||
private boolean itunesExplicit;
|
||||
private String itunesAuthor;
|
||||
private ItunesOwner itunesOwner;
|
||||
private String itunesTitle;
|
||||
private String itunesSubtitle;
|
||||
private String itunesSummary;
|
||||
private String itunesType;
|
||||
private String itunesNewFeedUrl;
|
||||
private boolean itunesBlock;
|
||||
private boolean itunesComplete;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param dateTimeParser timestamp parser
|
||||
*/
|
||||
public ItunesChannel(DateTimeParser dateTimeParser) {
|
||||
super(dateTimeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the artwork for the show.
|
||||
* Specify your show artwork by providing a URL linking to it.
|
||||
* @return image
|
||||
*/
|
||||
public String getItunesImage() {
|
||||
return itunesImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the artwork for the show.
|
||||
* Specify your show artwork by providing a URL linking to it.
|
||||
* @param image image
|
||||
*/
|
||||
public void setItunesImage(String image) {
|
||||
this.itunesImage = image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the show category information. For a complete list of categories and subcategories
|
||||
* @return list of categories
|
||||
*/
|
||||
public List<String> getItunesCategories() {
|
||||
return Collections.unmodifiableList(itunesCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the show category information. For a complete list of categories and subcategories
|
||||
* @param itunesCategory category
|
||||
*/
|
||||
public void addItunesCategory(String itunesCategory) {
|
||||
if (itunesCategory != null) {
|
||||
itunesCategories.add(itunesCategory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the podcast parental advisory information.
|
||||
* The explicit value can be one of the following:
|
||||
* <p>
|
||||
* True. If you specify true, indicating the presence of explicit content,
|
||||
* Apple Podcasts displays an Explicit parental advisory graphic for your podcast.
|
||||
* Podcasts containing explicit material aren’t available in some Apple Podcasts territories.
|
||||
* <p>
|
||||
* False. If you specify false, indicating that your podcast doesn’t contain
|
||||
* explicit language or adult content, Apple Podcasts displays a Clean parental
|
||||
* advisory graphic for your podcast.
|
||||
* @return explicit
|
||||
*/
|
||||
public Boolean getItunesExplicit() {
|
||||
return itunesExplicit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the podcast parental advisory information.
|
||||
* The explicit value can be one of the following:
|
||||
* <p>
|
||||
* True. If you specify true, indicating the presence of explicit content,
|
||||
* Apple Podcasts displays an Explicit parental advisory graphic for your podcast.
|
||||
* Podcasts containing explicit material aren’t available in some Apple Podcasts territories.
|
||||
* <p>
|
||||
* False. If you specify false, indicating that your podcast doesn’t contain
|
||||
* explicit language or adult content, Apple Podcasts displays a Clean parental
|
||||
* advisory graphic for your podcast.
|
||||
* @param itunesExplicit explicit
|
||||
*/
|
||||
public void setItunesExplicit(Boolean itunesExplicit) {
|
||||
this.itunesExplicit = itunesExplicit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the group responsible for creating the show.
|
||||
* @return author
|
||||
*/
|
||||
public Optional<String> getItunesAuthor() {
|
||||
return Optional.ofNullable(itunesAuthor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the group responsible for creating the show.
|
||||
* @param itunesAuthor author
|
||||
*/
|
||||
public void setItunesAuthor(String itunesAuthor) {
|
||||
this.itunesAuthor = itunesAuthor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the podcast owner contact information.
|
||||
* @param itunesOwner owner
|
||||
*/
|
||||
public void setItunesOwner(ItunesOwner itunesOwner) {
|
||||
this.itunesOwner = itunesOwner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the podcast owner contact information.
|
||||
* @return owner
|
||||
*/
|
||||
public Optional<ItunesOwner> getItunesOwner() {
|
||||
return Optional.ofNullable(itunesOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title specific for Apple Podcasts.
|
||||
* @return title
|
||||
*/
|
||||
public Optional<String> getItunesTitle() {
|
||||
return Optional.ofNullable(itunesTitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title specific for Apple Podcasts.
|
||||
* @param itunesTitle title
|
||||
*/
|
||||
public void setItunesTitle(String itunesTitle) {
|
||||
this.itunesTitle = itunesTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subtitle specific for Apple Podcasts.
|
||||
* @return subtitle
|
||||
*/
|
||||
public Optional<String> getItunesSubtitle() {
|
||||
return Optional.ofNullable(itunesSubtitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the subtitle specific for Apple Podcasts.
|
||||
* @param itunesSubtitle subtitle
|
||||
*/
|
||||
public void setItunesSubtitle(String itunesSubtitle) {
|
||||
this.itunesSubtitle = itunesSubtitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary.
|
||||
* @return summary
|
||||
*/
|
||||
public String getItunesSummary() {
|
||||
return itunesSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the summary.
|
||||
* @param itunesSummary summary
|
||||
*/
|
||||
public void setItunesSummary(String itunesSummary) {
|
||||
this.itunesSummary = itunesSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of show.
|
||||
* @return type
|
||||
*/
|
||||
public Optional<String> getItunesType() {
|
||||
return Optional.ofNullable(itunesType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type of show.
|
||||
* @param itunesType type
|
||||
*/
|
||||
public void setItunesType(String itunesType) {
|
||||
this.itunesType = itunesType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the new podcast RSS Feed URL.
|
||||
* If you change the URL of your podcast feed, you should use this tag in your new feed.
|
||||
* @return new feed url
|
||||
*/
|
||||
public Optional<String> getItunesNewFeedUrl() {
|
||||
return Optional.ofNullable(itunesNewFeedUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the new podcast RSS Feed URL.
|
||||
* If you change the URL of your podcast feed, you should use this tag in your new feed.
|
||||
* @param itunesNewFeedUrl new feed url
|
||||
*/
|
||||
public void setItunesNewFeedUrl(String itunesNewFeedUrl) {
|
||||
this.itunesNewFeedUrl = itunesNewFeedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the podcast show or hide status.
|
||||
* If you want your show removed from the Apple directory, use this tag.
|
||||
* @return block
|
||||
*/
|
||||
public boolean isItunesBlock() {
|
||||
return itunesBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the podcast show or hide status.
|
||||
* If you want your show removed from the Apple directory, use this tag.
|
||||
* @param itunesBlock block
|
||||
*/
|
||||
public void setItunesBlock(boolean itunesBlock) {
|
||||
this.itunesBlock = itunesBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the podcast update status.
|
||||
* If you will never publish another episode to your show, use this tag.
|
||||
* @return complete
|
||||
*/
|
||||
public boolean isItunesComplete() {
|
||||
return itunesComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the podcast update status.
|
||||
* If you will never publish another episode to your show, use this tag.
|
||||
* @param itunesComplete complete
|
||||
*/
|
||||
public void setItunesComplete(boolean itunesComplete) {
|
||||
this.itunesComplete = itunesComplete;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
ItunesChannel that = (ItunesChannel) o;
|
||||
return getItunesExplicit() == that.getItunesExplicit() &&
|
||||
isItunesBlock() == that.isItunesBlock() &&
|
||||
isItunesComplete() == that.isItunesComplete() &&
|
||||
Objects.equals(getItunesImage(), that.getItunesImage()) &&
|
||||
getItunesCategories().equals(that.getItunesCategories()) &&
|
||||
Objects.equals(getItunesAuthor(), that.getItunesAuthor()) &&
|
||||
Objects.equals(getItunesOwner(), that.getItunesOwner()) &&
|
||||
Objects.equals(getItunesTitle(), that.getItunesTitle()) &&
|
||||
Objects.equals(getItunesSubtitle(), that.getItunesSubtitle()) &&
|
||||
Objects.equals(getItunesSummary(), that.getItunesSummary()) &&
|
||||
Objects.equals(getItunesType(), that.getItunesType()) &&
|
||||
Objects.equals(getItunesNewFeedUrl(), that.getItunesNewFeedUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), getItunesImage(), getItunesCategories(), getItunesExplicit(),
|
||||
getItunesAuthor(), getItunesOwner(), getItunesTitle(), getItunesSubtitle(), getItunesSummary(),
|
||||
getItunesType(), getItunesNewFeedUrl(), isItunesBlock(), isItunesComplete());
|
||||
}
|
||||
}
|
285
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/module/itunes/ItunesItem.java
vendored
Normal file
285
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/module/itunes/ItunesItem.java
vendored
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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.module.itunes;
|
||||
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
import com.apptasticsoftware.rssreader.Item;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class representing the Itunes item.
|
||||
*/
|
||||
public class ItunesItem extends Item {
|
||||
private String itunesDuration;
|
||||
private String itunesImage;
|
||||
private boolean itunesExplicit;
|
||||
private String itunesTitle;
|
||||
private String itunesSubtitle;
|
||||
private String itunesSummary;
|
||||
private String itunesKeywords;
|
||||
private Integer itunesEpisode;
|
||||
private Integer itunesSeason;
|
||||
private String itunesEpisodeType;
|
||||
private boolean itunesBlock;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param dateTimeParser timestamp parser
|
||||
*/
|
||||
public ItunesItem(DateTimeParser dateTimeParser) {
|
||||
super(dateTimeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration of an episode.
|
||||
* @return duration
|
||||
*/
|
||||
public Optional<String> getItunesDuration() {
|
||||
return Optional.ofNullable(itunesDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the duration of an episode.
|
||||
* @param itunesDuration duration
|
||||
*/
|
||||
public void setItunesDuration(String itunesDuration) {
|
||||
this.itunesDuration = itunesDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration
|
||||
* @return duration
|
||||
*/
|
||||
@SuppressWarnings("java:S108")
|
||||
public Optional<Duration> getItunesDurationAsDuration() {
|
||||
if (itunesDuration != null && !itunesDuration.isBlank()) {
|
||||
try {
|
||||
String[] parts = itunesDuration.split(":");
|
||||
if (parts.length == 1) {
|
||||
return Optional.of(Duration.ofSeconds(Long.parseLong(parts[0])));
|
||||
} else if (parts.length == 2) {
|
||||
return Optional.of(Duration.ofMinutes(Long.parseLong(parts[0]))
|
||||
.plusSeconds(Long.parseLong(parts[1])));
|
||||
} else if (parts.length == 3) {
|
||||
return Optional.of(Duration.ofHours(Long.parseLong(parts[0]))
|
||||
.plusMinutes(Long.parseLong(parts[1]))
|
||||
.plusSeconds(Long.parseLong(parts[2])));
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode artwork.
|
||||
* @return image
|
||||
*/
|
||||
public Optional<String> getItunesImage() {
|
||||
return Optional.ofNullable(itunesImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode artwork.
|
||||
* @param itunesImage image
|
||||
*/
|
||||
public void setItunesImage(String itunesImage) {
|
||||
this.itunesImage = itunesImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode parental advisory information.
|
||||
* Where the explicit value can be one of the following:
|
||||
* <p>
|
||||
* true. If you specify true, indicating the presence of explicit content, Apple Podcasts displays an Explicit parental advisory graphic for your episode.
|
||||
* Episodes containing explicit material aren’t available in some Apple Podcasts territories.
|
||||
* <p>
|
||||
* false. If you specify false, indicating that the episode does not contain explicit language or adult content, Apple Podcasts displays a Clean parental advisory graphic for your episode.
|
||||
* @return explicit
|
||||
*/
|
||||
public boolean isItunesExplicit() {
|
||||
return itunesExplicit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode parental advisory information.
|
||||
* Where the explicit value can be one of the following:
|
||||
* <p>
|
||||
* true. If you specify true, indicating the presence of explicit content, Apple Podcasts displays an Explicit parental advisory graphic for your episode.
|
||||
* Episodes containing explicit material aren’t available in some Apple Podcasts territories.
|
||||
* <p>
|
||||
* false. If you specify false, indicating that the episode does not contain explicit language or adult content, Apple Podcasts displays a Clean parental advisory graphic for your episode.
|
||||
* @param itunesExplicit explicit
|
||||
*/
|
||||
public void setItunesExplicit(boolean itunesExplicit) {
|
||||
this.itunesExplicit = itunesExplicit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode title specific for Apple Podcasts.
|
||||
* @return title
|
||||
*/
|
||||
public Optional<String> getItunesTitle() {
|
||||
return Optional.ofNullable(itunesTitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode title specific for Apple Podcasts.
|
||||
* @param itunesTitle title
|
||||
*/
|
||||
public void setItunesTitle(String itunesTitle) {
|
||||
this.itunesTitle = itunesTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode subtitle specific for Apple Podcasts.
|
||||
* @return subtitle
|
||||
*/
|
||||
public Optional<String> getItunesSubtitle() {
|
||||
return Optional.ofNullable(itunesSubtitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode subtitle specific for Apple Podcasts.
|
||||
* @param itunesSubtitle subtitle
|
||||
*/
|
||||
public void setItunesSubtitle(String itunesSubtitle) {
|
||||
this.itunesSubtitle = itunesSubtitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode summary.
|
||||
* @return summary
|
||||
*/
|
||||
public Optional<String> getItunesSummary() {
|
||||
return Optional.ofNullable(itunesSummary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode summary.
|
||||
* @param itunesSummary summary
|
||||
*/
|
||||
public void setItunesSummary(String itunesSummary) {
|
||||
this.itunesSummary = itunesSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the keywords.
|
||||
* @return keywords
|
||||
*/
|
||||
public Optional<String> getItunesKeywords() {
|
||||
return Optional.ofNullable(itunesKeywords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the keywords.
|
||||
* @param itunesKeywords keywords
|
||||
*/
|
||||
public void setItunesKeywords(String itunesKeywords) {
|
||||
this.itunesKeywords = itunesKeywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode number.
|
||||
* @return episode
|
||||
*/
|
||||
public Optional<Integer> getItunesEpisode() {
|
||||
return Optional.ofNullable(itunesEpisode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode number.
|
||||
* @param itunesEpisode episode
|
||||
*/
|
||||
public void setItunesEpisode(Integer itunesEpisode) {
|
||||
this.itunesEpisode = itunesEpisode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode season number.
|
||||
* @return season
|
||||
*/
|
||||
public Optional<Integer> getItunesSeason() {
|
||||
return Optional.ofNullable(itunesSeason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode season number.
|
||||
* @param itunesSeason season
|
||||
*/
|
||||
public void setItunesSeason(Integer itunesSeason) {
|
||||
this.itunesSeason = itunesSeason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode type.
|
||||
* @return type
|
||||
*/
|
||||
public Optional<String> getItunesEpisodeType() {
|
||||
return Optional.ofNullable(itunesEpisodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode type.
|
||||
* @param itunesEpisodeType type
|
||||
*/
|
||||
public void setItunesEpisodeType(String itunesEpisodeType) {
|
||||
this.itunesEpisodeType = itunesEpisodeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode show or hide status.
|
||||
* If you want an episode removed from the Apple directory, use this tag.
|
||||
* @return block
|
||||
*/
|
||||
public boolean isItunesBlock() {
|
||||
return itunesBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the episode show or hide status.
|
||||
* If you want an episode removed from the Apple directory, use this tag.
|
||||
* @param itunesBlock block
|
||||
*/
|
||||
public void setItunesBlock(boolean itunesBlock) {
|
||||
this.itunesBlock = itunesBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
ItunesItem that = (ItunesItem) o;
|
||||
return isItunesExplicit() == that.isItunesExplicit() && isItunesBlock() == that.isItunesBlock() && Objects.equals(getItunesDuration(), that.getItunesDuration()) && Objects.equals(getItunesImage(), that.getItunesImage()) && Objects.equals(getItunesTitle(), that.getItunesTitle()) && Objects.equals(getItunesSubtitle(), that.getItunesSubtitle()) && Objects.equals(getItunesSummary(), that.getItunesSummary()) && Objects.equals(getItunesKeywords(), that.getItunesKeywords()) && Objects.equals(getItunesEpisode(), that.getItunesEpisode()) && Objects.equals(getItunesSeason(), that.getItunesSeason()) && Objects.equals(getItunesEpisodeType(), that.getItunesEpisodeType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), getItunesDuration(), getItunesImage(), isItunesExplicit(), getItunesTitle(), getItunesSubtitle(), getItunesSummary(), getItunesKeywords(), getItunesEpisode(), getItunesSeason(), getItunesEpisodeType(), isItunesBlock());
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.module.itunes;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class representing the Itunes owner.
|
||||
*/
|
||||
public class ItunesOwner {
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
|
||||
/**
|
||||
* Get the name
|
||||
* @return name
|
||||
*/
|
||||
public Optional<String> getName() {
|
||||
return Optional.ofNullable(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name
|
||||
* @param name name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email
|
||||
* @return email
|
||||
*/
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the email
|
||||
* @param email email
|
||||
*/
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ItunesOwner that = (ItunesOwner) o;
|
||||
return Objects.equals(getName(), that.getName()) && Objects.equals(getEmail(), that.getEmail());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getName(), getEmail());
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.module.itunes;
|
||||
|
||||
import com.apptasticsoftware.rssreader.AbstractRssReader;
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
|
||||
import static com.apptasticsoftware.rssreader.util.Mapper.mapBoolean;
|
||||
import static com.apptasticsoftware.rssreader.util.Mapper.mapInteger;
|
||||
|
||||
/**
|
||||
* Class for reading podcast (itunes) feeds.
|
||||
*/
|
||||
public class ItunesRssReader extends AbstractRssReader<ItunesChannel, ItunesItem> {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public ItunesRssReader() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param httpClient http client
|
||||
*/
|
||||
public ItunesRssReader(HttpClient httpClient) {
|
||||
super(httpClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerChannelTags() {
|
||||
super.registerChannelTags();
|
||||
addChannelExtension("itunes:explicit", (i, v) -> mapBoolean(v, i::setItunesExplicit));
|
||||
addChannelExtension("itunes:author", ItunesChannel::setItunesAuthor);
|
||||
|
||||
addChannelExtension("itunes:name", (i, v) -> {
|
||||
if (i.getItunesOwner().isEmpty())
|
||||
i.setItunesOwner(new ItunesOwner());
|
||||
i.getItunesOwner().ifPresent(a -> a.setName(v));
|
||||
});
|
||||
|
||||
addChannelExtension("itunes:email", (i, v) -> {
|
||||
if (i.getItunesOwner().isEmpty())
|
||||
i.setItunesOwner(new ItunesOwner());
|
||||
i.getItunesOwner().ifPresent(a -> a.setEmail(v));
|
||||
});
|
||||
|
||||
addChannelExtension("itunes:title", ItunesChannel::setItunesTitle);
|
||||
addChannelExtension("itunes:subtitle", ItunesChannel::setItunesSubtitle);
|
||||
addChannelExtension("itunes:summary", ItunesChannel::setItunesSummary);
|
||||
addChannelExtension("itunes:type", ItunesChannel::setItunesType);
|
||||
addChannelExtension("itunes:new-feed-url", ItunesChannel::setItunesNewFeedUrl);
|
||||
addChannelExtension("itunes:block", (i, v) -> mapBoolean(v, i::setItunesBlock));
|
||||
addChannelExtension("itunes:complete", (i, v) -> mapBoolean(v, i::setItunesComplete));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerChannelAttributes() {
|
||||
super.registerChannelAttributes();
|
||||
addChannelExtension("itunes:image", "href", ItunesChannel::setItunesImage);
|
||||
addChannelExtension("itunes:category", "text", ItunesChannel::addItunesCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerItemTags() {
|
||||
super.registerItemTags();
|
||||
addItemExtension("itunes:duration", ItunesItem::setItunesDuration);
|
||||
addItemExtension("itunes:explicit", (i, v) -> mapBoolean(v, i::setItunesExplicit));
|
||||
addItemExtension("itunes:title", ItunesItem::setItunesTitle);
|
||||
addItemExtension("itunes:subtitle", ItunesItem::setItunesSubtitle);
|
||||
addItemExtension("itunes:summary", ItunesItem::setItunesSummary);
|
||||
addItemExtension("itunes:keywords", ItunesItem::setItunesKeywords);
|
||||
addItemExtension("itunes:episode", (i, v) -> mapInteger(v, i::setItunesEpisode));
|
||||
addItemExtension("itunes:season", (i, v) -> mapInteger(v, i::setItunesSeason));
|
||||
addItemExtension("itunes:episodeType", ItunesItem::setItunesEpisodeType);
|
||||
addItemExtension("itunes:block", (i, v) -> mapBoolean(v, i::setItunesBlock));
|
||||
addItemExtension("itunes:image", "href", ItunesItem::setItunesImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItunesChannel createChannel(DateTimeParser dateTimeParser) {
|
||||
return new ItunesChannel(dateTimeParser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItunesItem createItem(DateTimeParser dateTimeParser) {
|
||||
return new ItunesItem(dateTimeParser);
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.module.mediarss;
|
||||
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
import com.apptasticsoftware.rssreader.Item;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class representing the media rss item.
|
||||
*/
|
||||
public class MediaRssItem extends Item {
|
||||
private MediaThumbnail mediaThumbnail;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param dateTimeParser timestamp parser
|
||||
*/
|
||||
public MediaRssItem(DateTimeParser dateTimeParser) {
|
||||
super(dateTimeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media thumbnail
|
||||
*
|
||||
* @return media thumbnail
|
||||
*/
|
||||
public Optional<MediaThumbnail> getMediaThumbnail() {
|
||||
return Optional.ofNullable(mediaThumbnail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media thumbnail
|
||||
*
|
||||
* @param mediaThumbnail media thumbnail
|
||||
*/
|
||||
public void setMediaThumbnail(MediaThumbnail mediaThumbnail) {
|
||||
this.mediaThumbnail = mediaThumbnail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
MediaRssItem that = (MediaRssItem) o;
|
||||
return Objects.equals(getMediaThumbnail(), that.getMediaThumbnail());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), getMediaThumbnail());
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.module.mediarss;
|
||||
|
||||
import com.apptasticsoftware.rssreader.AbstractRssReader;
|
||||
import com.apptasticsoftware.rssreader.Channel;
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Class for reading media rss feeds.
|
||||
*/
|
||||
public class MediaRssReader extends AbstractRssReader<Channel, MediaRssItem> {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public MediaRssReader() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param httpClient http client
|
||||
*/
|
||||
public MediaRssReader(HttpClient httpClient) {
|
||||
super(httpClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Channel createChannel(DateTimeParser dateTimeParser) {
|
||||
return new Channel(dateTimeParser);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaRssItem createItem(DateTimeParser dateTimeParser) {
|
||||
return new MediaRssItem(dateTimeParser);
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S1192")
|
||||
@Override
|
||||
protected void registerItemAttributes() {
|
||||
super.registerItemAttributes();
|
||||
super.addItemExtension("media:thumbnail", "url", mediaThumbnailSetterTemplateBuilder(MediaThumbnail::setUrl));
|
||||
super.addItemExtension("media:thumbnail", "height", mediaThumbnailSetterTemplateBuilder(
|
||||
(mediaThumbnail, height) -> mediaThumbnail.setHeight(Integer.parseInt(height))
|
||||
));
|
||||
super.addItemExtension("media:thumbnail", "width", mediaThumbnailSetterTemplateBuilder(
|
||||
(mediaThumbnail, width) -> mediaThumbnail.setWidth(Integer.parseInt(width))
|
||||
));
|
||||
}
|
||||
|
||||
private BiConsumer<MediaRssItem, String> mediaThumbnailSetterTemplateBuilder(BiConsumer<MediaThumbnail, String> setter) {
|
||||
return (mediaRssItem, value) -> {
|
||||
var mediaThumbnail = mediaRssItem.getMediaThumbnail().orElse(new MediaThumbnail());
|
||||
setter.accept(mediaThumbnail, value);
|
||||
mediaRssItem.setMediaThumbnail(mediaThumbnail);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package com.apptasticsoftware.rssreader.module.mediarss;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Class representing the media thumbnail from the media rss spec.
|
||||
* See <a href="https://www.rssboard.org/media-rss#media-thumbnails">for details</a>.
|
||||
*/
|
||||
public class MediaThumbnail {
|
||||
private String url;
|
||||
private Integer width;
|
||||
private Integer height;
|
||||
private String time;
|
||||
|
||||
/**
|
||||
* Get the url of the thumbnail
|
||||
*
|
||||
* @return url
|
||||
*/
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the url of the thumbnail
|
||||
*
|
||||
* @param url url
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of the thumbnail
|
||||
*
|
||||
* @return width
|
||||
*/
|
||||
public Optional<Integer> getWidth() {
|
||||
return Optional.ofNullable(width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width of the thumbnail
|
||||
*
|
||||
* @param width width
|
||||
*/
|
||||
public void setWidth(Integer width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of the thumbnail
|
||||
*
|
||||
* @return height
|
||||
*/
|
||||
public Optional<Integer> getHeight() {
|
||||
return Optional.ofNullable(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the height of the thumbnail
|
||||
*
|
||||
* @param height height
|
||||
*/
|
||||
public void setHeight(Integer height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the thumbnail
|
||||
*
|
||||
* @return time
|
||||
*/
|
||||
public Optional<String> getTime() {
|
||||
return Optional.ofNullable(time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time of the thumbnail
|
||||
*
|
||||
* @param time time
|
||||
*/
|
||||
public void setTime(String time) {
|
||||
this.time = time;
|
||||
}
|
||||
}
|
28
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/package-info.java
vendored
Normal file
28
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/package-info.java
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This package is intended for RSS reader.
|
||||
*/
|
||||
package com.apptasticsoftware.rssreader;
|
23
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/Default.java
vendored
Normal file
23
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/Default.java
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
package com.apptasticsoftware.rssreader.util;
|
||||
|
||||
import com.apptasticsoftware.rssreader.DateTime;
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
|
||||
/**
|
||||
* Provides default implementations for various components.
|
||||
*/
|
||||
@SuppressWarnings("javaarchitecture:S7091")
|
||||
public class Default {
|
||||
|
||||
private Default() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default date time parser.
|
||||
* @return date time parser
|
||||
*/
|
||||
public static DateTimeParser getDateTimeParser() {
|
||||
return new DateTime();
|
||||
}
|
||||
}
|
202
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/ItemComparator.java
vendored
Normal file
202
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/ItemComparator.java
vendored
Normal file
@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.apptasticsoftware.rssreader.Channel;
|
||||
import com.apptasticsoftware.rssreader.DateTimeParser;
|
||||
import com.apptasticsoftware.rssreader.Item;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Provides different comparators for sorting item objects.
|
||||
*/
|
||||
@SuppressWarnings("java:S1133")
|
||||
public final class ItemComparator {
|
||||
private static final String MUST_NOT_BE_NULL_MESSAGE = "Date time parser must not be null";
|
||||
|
||||
private ItemComparator() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*
|
||||
* @deprecated As of release 3.9.0, replaced by {@link #oldestPublishedItemFirst()}
|
||||
*/
|
||||
@Deprecated(since = "3.9.0", forRemoval = true)
|
||||
public static <I extends Item> Comparator<I> oldestItemFirst() {
|
||||
return oldestPublishedItemFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> oldestPublishedItemFirst() {
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getPubDateZonedDateTime().orElse(null),
|
||||
Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on updated date if exist otherwise on publication date in ascending order (oldest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> oldestUpdatedItemFirst() {
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getUpdatedZonedDateTime().orElse(i.getPubDateZonedDateTime().orElse(null)),
|
||||
Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @param dateTimeParser date time parser
|
||||
* @return comparator
|
||||
*
|
||||
* @deprecated As of release 3.9.0, replaced by {@link #oldestPublishedItemFirst(DateTimeParser)}
|
||||
*/
|
||||
@Deprecated(since = "3.9.0", forRemoval = true)
|
||||
public static <I extends Item> Comparator<I> oldestItemFirst(DateTimeParser dateTimeParser) {
|
||||
return oldestPublishedItemFirst(dateTimeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @param dateTimeParser date time parser
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> oldestPublishedItemFirst(DateTimeParser dateTimeParser) {
|
||||
Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getPubDate().map(dateTimeParser::parse).orElse(null),
|
||||
Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on updated date if exist otherwise on publication date in ascending order (oldest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @param dateTimeParser date time parser
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> oldestUpdatedItemFirst(DateTimeParser dateTimeParser) {
|
||||
Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getUpdated().or(i::getPubDate).map(dateTimeParser::parse).orElse(null),
|
||||
Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*
|
||||
* @deprecated As of release 3.9.0, replaced by {@link #newestPublishedItemFirst()}
|
||||
*/
|
||||
@Deprecated(since = "3.9.0", forRemoval = true)
|
||||
public static <I extends Item> Comparator<I> newestItemFirst() {
|
||||
return newestPublishedItemFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> newestPublishedItemFirst() {
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getPubDateZonedDateTime().orElse(null),
|
||||
Comparator.nullsLast(Comparator.naturalOrder())).reversed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on updated date if exist otherwise on publication date in descending order (newest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> newestUpdatedItemFirst() {
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getUpdatedZonedDateTime().orElse(i.getPubDateZonedDateTime().orElse(null)),
|
||||
Comparator.nullsLast(Comparator.naturalOrder())).reversed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @param dateTimeParser date time parser
|
||||
* @return comparator
|
||||
*
|
||||
* @deprecated As of release 3.9.0, replaced by {@link #newestPublishedItemFirst(DateTimeParser)}
|
||||
*/
|
||||
@Deprecated(since = "3.9.0", forRemoval = true)
|
||||
public static <I extends Item> Comparator<I> newestItemFirst(DateTimeParser dateTimeParser) {
|
||||
return newestPublishedItemFirst(dateTimeParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @param dateTimeParser date time parser
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> newestPublishedItemFirst(DateTimeParser dateTimeParser) {
|
||||
Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getPubDate().map(dateTimeParser::parse).orElse(null),
|
||||
Comparator.nullsLast(Comparator.naturalOrder())).reversed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on updated date if exist otherwise on publication date in descending order (newest first)
|
||||
* @param <I> any class that extends Item
|
||||
* @param dateTimeParser date time parser
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> newestUpdatedItemFirst(DateTimeParser dateTimeParser) {
|
||||
Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
|
||||
return Comparator.comparing((I i) ->
|
||||
i.getUpdated().or(i::getPubDate).map(dateTimeParser::parse).orElse(null),
|
||||
Comparator.nullsLast(Comparator.naturalOrder())).reversed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for sorting Items on channel title
|
||||
* @param <I> any class that extends Item
|
||||
* @return comparator
|
||||
*/
|
||||
public static <I extends Item> Comparator<I> channelTitle() {
|
||||
return Comparator.comparing(
|
||||
Item::getChannel,
|
||||
Comparator.nullsFirst(Comparator.comparing(
|
||||
Channel::getTitle, Comparator.nullsFirst(Comparator.naturalOrder()))));
|
||||
}
|
||||
|
||||
}
|
120
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/Mapper.java
vendored
Normal file
120
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/Mapper.java
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
package com.apptasticsoftware.rssreader.util;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Provides methods for mapping field
|
||||
*/
|
||||
public final class Mapper {
|
||||
private static final Logger LOGGER = Logger.getLogger("com.apptasticsoftware.rssreader.util");
|
||||
|
||||
private Mapper() { }
|
||||
|
||||
/**
|
||||
* Maps a boolean text value (true, false, no or yes) to a boolean field. Text value can be in any casing.
|
||||
* @param text text value
|
||||
* @param func boolean setter method
|
||||
*/
|
||||
public static void mapBoolean(String text, Consumer<Boolean> func) {
|
||||
text = text.toLowerCase();
|
||||
if ("true".equals(text) || "yes".equals(text)) {
|
||||
func.accept(Boolean.TRUE);
|
||||
} else if ("false".equals(text) || "no".equals(text)) {
|
||||
func.accept(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a integer text value to a integer field.
|
||||
* @param text text value
|
||||
* @param func integer setter method
|
||||
*/
|
||||
public static void mapInteger(String text, Consumer<Integer> func) {
|
||||
mapNumber(text, func, Integer::valueOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a long text value to a long field.
|
||||
* @param text text value
|
||||
* @param func long setter method
|
||||
*/
|
||||
public static void mapLong(String text, Consumer<Long> func) {
|
||||
mapNumber(text, func, Long::valueOf);
|
||||
}
|
||||
|
||||
private static <T> void mapNumber(String text, Consumer<T> func, Function<String, T> convert) {
|
||||
if (!isNullOrEmpty(text)) {
|
||||
try {
|
||||
func.accept(convert.apply(text));
|
||||
} catch (NumberFormatException e) {
|
||||
if (LOGGER.isLoggable(Level.WARNING)) {
|
||||
LOGGER.log(Level.WARNING, () -> String.format("Failed to convert %s. Message: %s", text, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map value if field has not been mapped before
|
||||
* @param text value to map
|
||||
* @param getter getter to check if field is empty
|
||||
* @param setter setter to set value
|
||||
* @param <T> type
|
||||
*/
|
||||
public static <T> void mapIfEmpty(String text, Supplier<T> getter, Consumer<String> setter) {
|
||||
if (isNullOrEmpty(getter) && !isNullOrEmpty(text)) {
|
||||
setter.accept(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance if a getter returns optional empty and assigns the field the new instance.
|
||||
* @param getter getter method
|
||||
* @param setter setter method
|
||||
* @param factory factory for creating a new instance if field is not set before
|
||||
* @return existing or new instance
|
||||
* @param <T> any class
|
||||
*/
|
||||
public static <T> T createIfNull(Supplier<Optional<T>> getter, Consumer<T> setter, Supplier<T> factory) {
|
||||
return createIfNullOptional(getter, setter, factory).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance if a getter returns optional empty and assigns the field the new instance.
|
||||
* @param getter getter method
|
||||
* @param setter setter method
|
||||
* @param factory factory for creating a new instance if field is not set before
|
||||
* @return existing or new instance
|
||||
* @param <T> any class
|
||||
*/
|
||||
public static <T> Optional<T> createIfNullOptional(Supplier<Optional<T>> getter, Consumer<T> setter, Supplier<T> factory) {
|
||||
Optional<T> instance = getter.get();
|
||||
if (instance.isEmpty()) {
|
||||
T newInstance = factory.get();
|
||||
setter.accept(newInstance);
|
||||
instance = Optional.of(newInstance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static <T> boolean isNullOrEmpty(Supplier<T> getter) {
|
||||
return getter.get() == null ||
|
||||
"".equals(getter.get()) ||
|
||||
getter.get() == Optional.empty() ||
|
||||
getter.get() instanceof Optional<?> &&
|
||||
((Optional<?>) getter.get())
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.map(String::isBlank)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static boolean isNullOrEmpty(String text) {
|
||||
return text == null || text.isBlank();
|
||||
}
|
||||
}
|
30
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/Util.java
vendored
Normal file
30
third-party/rssreader/src/main/java/com/apptasticsoftware/rssreader/util/Util.java
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
package com.apptasticsoftware.rssreader.util;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Utility class for RSS reader.
|
||||
*/
|
||||
public class Util {
|
||||
|
||||
private Util() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a time period string to hours.
|
||||
*
|
||||
* @param period the time period string (e.g., "daily", "weekly", "monthly", "yearly", "hourly")
|
||||
* @return the number of hours in the given time period, or 1 if the period is not recognized
|
||||
*/
|
||||
public static int toMinutes(String period) {
|
||||
switch (period.toLowerCase(Locale.ENGLISH)) {
|
||||
case "daily": return 1440;
|
||||
case "weekly": return 10080;
|
||||
case "monthly": return 43800;
|
||||
case "yearly": return 525600;
|
||||
case "hourly":
|
||||
default: return 60;
|
||||
}
|
||||
}
|
||||
}
|
37
third-party/rssreader/src/main/java/module-info.java
vendored
Normal file
37
third-party/rssreader/src/main/java/module-info.java
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* These modules define the base APIs for RSS reader.
|
||||
*/
|
||||
module com.apptasticsoftware.rssreader {
|
||||
requires java.net.http;
|
||||
requires java.xml;
|
||||
requires java.logging;
|
||||
|
||||
exports com.apptasticsoftware.rssreader;
|
||||
exports com.apptasticsoftware.rssreader.util;
|
||||
exports com.apptasticsoftware.rssreader.module.itunes;
|
||||
exports com.apptasticsoftware.rssreader.module.mediarss;
|
||||
}
|
Loading…
Reference in New Issue
Block a user