diff --git a/commafeed-server/dev/EclipseCodeFormatter.xml b/commafeed-server/dev/EclipseCodeFormatter.xml
deleted file mode 100644
index 39602cac..00000000
--- a/commafeed-server/dev/EclipseCodeFormatter.xml
+++ /dev/null
@@ -1,401 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/commafeed-server/dev/checkstyle.xml b/commafeed-server/dev/checkstyle.xml
index 46f4fbdb..31f34840 100644
--- a/commafeed-server/dev/checkstyle.xml
+++ b/commafeed-server/dev/checkstyle.xml
@@ -1,8 +1,8 @@
-
+ "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
+ "https://checkstyle.org/dtds/configuration_1_3.dtd">
+
@@ -40,11 +40,6 @@
-
-
-
-
-
@@ -60,7 +55,7 @@
+ value="Empty catch block. You can use the name 'ignore' or 'ignored' for the exception variable if you really want an empty catch block, but you should strongly consider at the very least logging something." />
@@ -76,7 +71,7 @@
+ value="java.lang.Boolean, java.lang.Byte, java.lang.Character, java.lang.Double, java.lang.Float, java.lang.Integer, java.lang.Long, java.lang.Short" />
@@ -113,14 +108,6 @@
-
-
-
-
-
-
-
-
diff --git a/commafeed-server/dev/eclipse.importorder b/commafeed-server/dev/eclipse.importorder
deleted file mode 100644
index f93d1c1c..00000000
--- a/commafeed-server/dev/eclipse.importorder
+++ /dev/null
@@ -1,7 +0,0 @@
-#Organize Import Order
-#Wed Jan 29 15:15:04 CET 2025
-0=java
-1=javax
-2=jakarta
-3=org
-4=com
diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml
index b0068ce3..e4536e0d 100644
--- a/commafeed-server/pom.xml
+++ b/commafeed-server/pom.xml
@@ -285,12 +285,9 @@
UTF-8
-
- ${project.basedir}/dev/EclipseCodeFormatter.xml
-
-
- ${project.basedir}/dev/eclipse.importorder
-
+
+
+
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java
index dd690230..a0497297 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java
@@ -1,15 +1,13 @@
package com.commafeed;
-import jakarta.enterprise.event.Observes;
-import jakarta.inject.Singleton;
-
import com.commafeed.backend.feed.FeedRefreshEngine;
import com.commafeed.backend.feed.ImageProxyUrl;
import com.commafeed.backend.task.TaskScheduler;
import com.commafeed.security.password.PasswordConstraintValidator;
-
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -18,28 +16,28 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class CommaFeedApplication {
- private final FeedRefreshEngine feedRefreshEngine;
- private final TaskScheduler taskScheduler;
- private final CommaFeedConfiguration config;
+ private final FeedRefreshEngine feedRefreshEngine;
+ private final TaskScheduler taskScheduler;
+ private final CommaFeedConfiguration config;
- public void start(@Observes StartupEvent ev) {
- log.info("starting up...");
+ public void start(@Observes StartupEvent ev) {
+ log.info("starting up...");
- PasswordConstraintValidator.setMinimumPasswordLength(config.users().minimumPasswordLength());
+ PasswordConstraintValidator.setMinimumPasswordLength(
+ config.users().minimumPasswordLength());
- if (config.imageProxyEnabled()) {
- ImageProxyUrl.generateKey();
- }
+ if (config.imageProxyEnabled()) {
+ ImageProxyUrl.generateKey();
+ }
- feedRefreshEngine.start();
- taskScheduler.start();
- }
+ feedRefreshEngine.start();
+ taskScheduler.start();
+ }
- public void stop(@Observes ShutdownEvent ev) {
- log.info("shutting down...");
-
- feedRefreshEngine.stop();
- taskScheduler.stop();
- }
+ public void stop(@Observes ShutdownEvent ev) {
+ log.info("shutting down...");
+ feedRefreshEngine.stop();
+ taskScheduler.stop();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
index d4ae4cfc..0fd8da0f 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
@@ -1,406 +1,360 @@
package com.commafeed;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Optional;
-
-import jakarta.validation.constraints.Min;
-import jakarta.validation.constraints.Positive;
-
import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
-
import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.quarkus.runtime.configuration.MemorySize;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Positive;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
/**
* CommaFeed configuration
*
- * Default values are for production, they can be overridden in application.properties for other profiles
+ *
Default values are for production, they can be overridden in application.properties for other
+ * profiles
*/
@ConfigMapping(prefix = "commafeed")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface CommaFeedConfiguration {
- /**
- * Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
- */
- @WithDefault("true")
- boolean hideFromWebCrawlers();
+ /**
+ * Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
+ */
+ @WithDefault("true")
+ boolean hideFromWebCrawlers();
- /**
- * If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser.
- *
- * This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
- */
- @WithDefault("false")
- boolean imageProxyEnabled();
+ /**
+ * If enabled, images in feed entries will be proxied through the server instead of accessed
+ * directly by the browser.
+ *
+ *
This is useful if commafeed is accessed through a restricting proxy that blocks some feeds
+ * that are followed.
+ */
+ @WithDefault("false")
+ boolean imageProxyEnabled();
- /**
- * Enable password recovery via email.
- *
- * Quarkus mailer will need to be configured.
- */
- @WithDefault("false")
- boolean passwordRecoveryEnabled();
+ /**
+ * Enable password recovery via email.
+ *
+ *
Quarkus mailer will need to be configured.
+ */
+ @WithDefault("false")
+ boolean passwordRecoveryEnabled();
- /**
- * Message displayed in a notification at the bottom of the page.
- */
- Optional announcement();
+ /** Message displayed in a notification at the bottom of the page. */
+ Optional announcement();
- /**
- * Google Auth key for fetching Youtube channel favicons.
- */
- Optional googleAuthKey();
+ /** Google Auth key for fetching Youtube channel favicons. */
+ Optional googleAuthKey();
- /**
- * HTTP client configuration
- */
- @ConfigDocSection
- HttpClient httpClient();
+ /** HTTP client configuration */
+ @ConfigDocSection
+ HttpClient httpClient();
- /**
- * Feed refresh engine settings.
- */
- @ConfigDocSection
- FeedRefresh feedRefresh();
+ /** Feed refresh engine settings. */
+ @ConfigDocSection
+ FeedRefresh feedRefresh();
- /**
- * Push notification settings.
- */
- @ConfigDocSection
- PushNotifications pushNotifications();
+ /** Push notification settings. */
+ @ConfigDocSection
+ PushNotifications pushNotifications();
- /**
- * Database settings.
- */
- @ConfigDocSection
- Database database();
+ /** Database settings. */
+ @ConfigDocSection
+ Database database();
- /**
- * Users settings.
- */
- @ConfigDocSection
- Users users();
+ /** Users settings. */
+ @ConfigDocSection
+ Users users();
- /**
- * Websocket settings.
- */
- @ConfigDocSection
- Websocket websocket();
+ /** Websocket settings. */
+ @ConfigDocSection
+ Websocket websocket();
- /**
- * Duration to wait for the feed refresh engine and the task scheduler to stop when the application is shutting down.
- */
- @WithDefault("2s")
- Duration shutdownTimeout();
+ /**
+ * Duration to wait for the feed refresh engine and the task scheduler to stop when the
+ * application is shutting down.
+ */
+ @WithDefault("2s")
+ Duration shutdownTimeout();
- interface HttpClient {
- /**
- * User-Agent string that will be used by the http client, leave empty for the default one.
- */
- Optional userAgent();
+ interface HttpClient {
+ /**
+ * User-Agent string that will be used by the http client, leave empty for the default one.
+ */
+ Optional userAgent();
- /**
- * Time to wait for a connection to be established.
- */
- @WithDefault("5s")
- Duration connectTimeout();
+ /** Time to wait for a connection to be established. */
+ @WithDefault("5s")
+ Duration connectTimeout();
- /**
- * Time to wait for SSL handshake to complete.
- */
- @WithDefault("5s")
- Duration sslHandshakeTimeout();
+ /** Time to wait for SSL handshake to complete. */
+ @WithDefault("5s")
+ Duration sslHandshakeTimeout();
- /**
- * Time to wait between two packets before timeout.
- */
- @WithDefault("10s")
- Duration socketTimeout();
+ /** Time to wait between two packets before timeout. */
+ @WithDefault("10s")
+ Duration socketTimeout();
- /**
- * Time to wait for the full response to be received.
- */
- @WithDefault("10s")
- Duration responseTimeout();
+ /** Time to wait for the full response to be received. */
+ @WithDefault("10s")
+ Duration responseTimeout();
- /**
- * Time to live for a connection in the pool.
- */
- @WithDefault("30s")
- Duration connectionTimeToLive();
+ /** Time to live for a connection in the pool. */
+ @WithDefault("30s")
+ Duration connectionTimeToLive();
- /**
- * Time between eviction runs for idle connections.
- */
- @WithDefault("1m")
- Duration idleConnectionsEvictionInterval();
+ /** Time between eviction runs for idle connections. */
+ @WithDefault("1m")
+ Duration idleConnectionsEvictionInterval();
- /**
- * If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
- */
- @WithDefault("5M")
- MemorySize maxResponseSize();
+ /**
+ * If a feed is larger than this, it will be discarded to prevent memory issues while
+ * parsing the feed.
+ */
+ @WithDefault("5M")
+ MemorySize maxResponseSize();
- /**
- * Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal
- * resources.
- *
- * You may want to enable this if you host a public instance of CommaFeed with registrations open.
- */
- @WithDefault("false")
- boolean blockLocalAddresses();
+ /**
+ * Prevent access to local addresses to mitigate server-side request forgery (SSRF) attacks,
+ * which could potentially expose internal resources.
+ *
+ * You may want to enable this if you host a public instance of CommaFeed with
+ * registrations open.
+ */
+ @WithDefault("false")
+ boolean blockLocalAddresses();
- /**
- * HTTP client cache configuration
- */
- @ConfigDocSection
- HttpClientCache cache();
- }
+ /** HTTP client cache configuration */
+ @ConfigDocSection
+ HttpClientCache cache();
+ }
- interface HttpClientCache {
- /**
- * Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when subscribing to a feed for the
- * first time or when clicking "fetch all my feeds now").
- */
- @WithDefault("true")
- boolean enabled();
+ interface HttpClientCache {
+ /**
+ * Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts
+ * (e.g. when subscribing to a feed for the first time or when clicking "fetch all my feeds
+ * now").
+ */
+ @WithDefault("true")
+ boolean enabled();
- /**
- * Maximum amount of memory the cache can use.
- */
- @WithDefault("10M")
- MemorySize maximumMemorySize();
+ /** Maximum amount of memory the cache can use. */
+ @WithDefault("10M")
+ MemorySize maximumMemorySize();
- /**
- * Duration after which an entry is removed from the cache.
- */
- @WithDefault("1m")
- Duration expiration();
- }
+ /** Duration after which an entry is removed from the cache. */
+ @WithDefault("1m")
+ Duration expiration();
+ }
- interface FeedRefresh {
- /**
- * Default amount of time CommaFeed will wait before refreshing a feed.
- */
- @WithDefault("5m")
- Duration interval();
+ interface FeedRefresh {
+ /** Default amount of time CommaFeed will wait before refreshing a feed. */
+ @WithDefault("5m")
+ Duration interval();
- /**
- * Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an upper bound when:
- *
- *
- * - an error occurs while refreshing a feed and we're backing off exponentially
- * - we receive a Cache-Control header from the feed
- * - we receive a Retry-After header from the feed
- *
- */
- @WithDefault("4h")
- Duration maxInterval();
+ /**
+ * Maximum amount of time CommaFeed will wait before refreshing a feed. This is used as an
+ * upper bound when:
+ *
+ *
+ * - an error occurs while refreshing a feed and we're backing off exponentially
+ *
- we receive a Cache-Control header from the feed
+ *
- we receive a Retry-After header from the feed
+ *
+ */
+ @WithDefault("4h")
+ Duration maxInterval();
- /**
- * If enabled, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since
- * the last entry was published. The interval will be sometimes between the default refresh interval
- * (`commafeed.feed-refresh.interval`) and the maximum refresh interval (`commafeed.feed-refresh.max-interval`).
- *
- * See {@link FeedRefreshIntervalCalculator} for details.
- */
- @WithDefault("true")
- boolean intervalEmpirical();
+ /**
+ * If enabled, CommaFeed will calculate the next refresh time based on the feed's average
+ * time between entries and the time since the last entry was published. The interval will
+ * be sometimes between the default refresh interval (`commafeed.feed-refresh.interval`) and
+ * the maximum refresh interval (`commafeed.feed-refresh.max-interval`).
+ *
+ * See {@link FeedRefreshIntervalCalculator} for details.
+ */
+ @WithDefault("true")
+ boolean intervalEmpirical();
- /**
- * Feed refresh engine error handling settings.
- */
- @ConfigDocSection
- FeedRefreshErrorHandling errors();
+ /** Feed refresh engine error handling settings. */
+ @ConfigDocSection
+ FeedRefreshErrorHandling errors();
- /**
- * Amount of http threads used to fetch feeds.
- */
- @Min(1)
- @WithDefault("3")
- int httpThreads();
+ /** Amount of http threads used to fetch feeds. */
+ @Min(1)
+ @WithDefault("3")
+ int httpThreads();
- /**
- * Amount of threads used to insert new entries in the database.
- */
- @Min(1)
- @WithDefault("1")
- int databaseThreads();
+ /** Amount of threads used to insert new entries in the database. */
+ @Min(1)
+ @WithDefault("1")
+ int databaseThreads();
- /**
- * Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again.
- *
- * 0 to disable.
- */
- @WithDefault("0")
- Duration userInactivityPeriod();
+ /**
+ * Duration after which a user is considered inactive. Feeds for inactive users are not
+ * refreshed until they log in again.
+ *
+ *
0 to disable.
+ */
+ @WithDefault("0")
+ Duration userInactivityPeriod();
- /**
- * Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
- */
- @WithDefault("500ms")
- Duration filteringExpressionEvaluationTimeout();
+ /**
+ * Duration after which the evaluation of a filtering expresion to mark an entry as read is
+ * considered to have timed out.
+ */
+ @WithDefault("500ms")
+ Duration filteringExpressionEvaluationTimeout();
- /**
- * Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
- */
- @WithDefault("0")
- Duration forceRefreshCooldownDuration();
- }
+ /**
+ * Duration after which the "Fetch all my feeds now" action is available again after use to
+ * avoid spamming feeds.
+ */
+ @WithDefault("0")
+ Duration forceRefreshCooldownDuration();
+ }
- interface PushNotifications {
- /**
- * Whether to enable push notifications to notify users of new entries in their feeds.
- */
- @WithDefault("true")
- boolean enabled();
+ interface PushNotifications {
+ /** Whether to enable push notifications to notify users of new entries in their feeds. */
+ @WithDefault("true")
+ boolean enabled();
- /**
- * Amount of threads used to send external notifications about new entries.
- */
- @Min(1)
- @WithDefault("5")
- int threads();
+ /** Amount of threads used to send external notifications about new entries. */
+ @Min(1)
+ @WithDefault("5")
+ int threads();
- /**
- * Maximum amount of notifications that can be queued before new notifications are discarded.
- */
- @Min(1)
- @WithDefault("100")
- int queueCapacity();
- }
+ /**
+ * Maximum amount of notifications that can be queued before new notifications are
+ * discarded.
+ */
+ @Min(1)
+ @WithDefault("100")
+ int queueCapacity();
+ }
- interface FeedRefreshErrorHandling {
- /**
- * Number of retries before backoff is applied.
- */
- @Min(0)
- @WithDefault("3")
- int retriesBeforeBackoff();
+ interface FeedRefreshErrorHandling {
+ /** Number of retries before backoff is applied. */
+ @Min(0)
+ @WithDefault("3")
+ int retriesBeforeBackoff();
- /**
- * Duration to wait before retrying after an error. Will be multiplied by the number of errors since the last successful fetch.
- */
- @WithDefault("1h")
- Duration backoffInterval();
- }
+ /**
+ * Duration to wait before retrying after an error. Will be multiplied by the number of
+ * errors since the last successful fetch.
+ */
+ @WithDefault("1h")
+ Duration backoffInterval();
+ }
- interface Database {
- /**
- * Timeout applied to all database queries.
- *
- * 0 to disable.
- */
- @WithDefault("0")
- Duration queryTimeout();
+ interface Database {
+ /**
+ * Timeout applied to all database queries.
+ *
+ *
0 to disable.
+ */
+ @WithDefault("0")
+ Duration queryTimeout();
- /**
- * Database cleanup settings.
- */
- @ConfigDocSection
- Cleanup cleanup();
+ /** Database cleanup settings. */
+ @ConfigDocSection
+ Cleanup cleanup();
- interface Cleanup {
- /**
- * Maximum age of feed entries in the database. Older entries will be deleted.
- *
- * 0 to disable.
- */
- @WithDefault("365d")
- Duration entriesMaxAge();
+ interface Cleanup {
+ /**
+ * Maximum age of feed entries in the database. Older entries will be deleted.
+ *
+ *
0 to disable.
+ */
+ @WithDefault("365d")
+ Duration entriesMaxAge();
- /**
- * Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted.
- *
- * 0 to disable.
- */
- @WithDefault("0")
- Duration statusesMaxAge();
+ /**
+ * Maximum age of feed entry statuses (read/unread) in the database. Older statuses will
+ * be deleted.
+ *
+ *
0 to disable.
+ */
+ @WithDefault("0")
+ Duration statusesMaxAge();
- /**
- * Maximum number of entries per feed to keep in the database.
- *
- * 0 to disable.
- */
- @WithDefault("500")
- int maxFeedCapacity();
+ /**
+ * Maximum number of entries per feed to keep in the database.
+ *
+ *
0 to disable.
+ */
+ @WithDefault("500")
+ int maxFeedCapacity();
- /**
- * Limit the number of feeds a user can subscribe to.
- *
- * 0 to disable.
- */
- @WithDefault("0")
- int maxFeedsPerUser();
+ /**
+ * Limit the number of feeds a user can subscribe to.
+ *
+ *
0 to disable.
+ */
+ @WithDefault("0")
+ int maxFeedsPerUser();
- /**
- * Rows to delete per query while cleaning up old entries.
- */
- @Positive
- @WithDefault("100")
- int batchSize();
+ /** Rows to delete per query while cleaning up old entries. */
+ @Positive
+ @WithDefault("100")
+ int batchSize();
- /**
- * Whether to keep starred entries when cleaning up old entries.
- */
- @WithDefault("true")
- boolean keepStarredEntries();
+ /** Whether to keep starred entries when cleaning up old entries. */
+ @WithDefault("true")
+ boolean keepStarredEntries();
- default Instant statusesInstantThreshold() {
- return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
- }
- }
- }
+ default Instant statusesInstantThreshold() {
+ return statusesMaxAge().toMillis() > 0
+ ? Instant.now().minus(statusesMaxAge())
+ : null;
+ }
+ }
+ }
- interface Users {
- /**
- * Whether to let users create accounts for themselves.
- */
- @WithDefault("false")
- boolean allowRegistrations();
+ interface Users {
+ /** Whether to let users create accounts for themselves. */
+ @WithDefault("false")
+ boolean allowRegistrations();
- /**
- * Minimum password length for user accounts.
- */
- @WithDefault("4")
- int minimumPasswordLength();
+ /** Minimum password length for user accounts. */
+ @WithDefault("4")
+ int minimumPasswordLength();
- /**
- * Whether an email address is required when creating a user account.
- */
- @WithDefault("false")
- boolean emailAddressRequired();
+ /** Whether an email address is required when creating a user account. */
+ @WithDefault("false")
+ boolean emailAddressRequired();
- /**
- * Whether to create a demo account the first time the app starts.
- */
- @WithDefault("false")
- boolean createDemoAccount();
- }
+ /** Whether to create a demo account the first time the app starts. */
+ @WithDefault("false")
+ boolean createDemoAccount();
+ }
- interface Websocket {
- /**
- * Enable websocket connection so the server can notify web clients that there are new entries for feeds.
- */
- @WithDefault("true")
- boolean enabled();
+ interface Websocket {
+ /**
+ * Enable websocket connection so the server can notify web clients that there are new
+ * entries for feeds.
+ */
+ @WithDefault("true")
+ boolean enabled();
- /**
- * Interval at which the client will send a ping message on the websocket to keep the connection alive.
- */
- @WithDefault("15m")
- Duration pingInterval();
-
- /**
- * If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
- */
- @WithDefault("30s")
- Duration treeReloadInterval();
- }
+ /**
+ * Interval at which the client will send a ping message on the websocket to keep the
+ * connection alive.
+ */
+ @WithDefault("15m")
+ Duration pingInterval();
+ /**
+ * If the websocket connection is disabled or the connection is lost, the client will reload
+ * the feed tree at this interval.
+ */
+ @WithDefault("30s")
+ Duration treeReloadInterval();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConstants.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConstants.java
index c9c39a32..43778b4b 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConstants.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConstants.java
@@ -4,5 +4,5 @@ import lombok.experimental.UtilityClass;
@UtilityClass
public class CommaFeedConstants {
- public static final String USERNAME_DEMO = "demo";
+ public static final String USERNAME_DEMO = "demo";
}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java
index 8dc10932..7b24a0b9 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java
@@ -1,25 +1,22 @@
package com.commafeed;
-import java.time.InstantSource;
-
+import com.codahale.metrics.MetricRegistry;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
-
-import com.codahale.metrics.MetricRegistry;
+import java.time.InstantSource;
@Singleton
public class CommaFeedProducers {
- @Produces
- @Singleton
- public InstantSource instantSource() {
- return InstantSource.system();
- }
-
- @Produces
- @Singleton
- public MetricRegistry metricRegistry() {
- return new MetricRegistry();
- }
+ @Produces
+ @Singleton
+ public InstantSource instantSource() {
+ return InstantSource.system();
+ }
+ @Produces
+ @Singleton
+ public MetricRegistry metricRegistry() {
+ return new MetricRegistry();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java
index 16476b37..6840325d 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java
@@ -1,30 +1,27 @@
package com.commafeed;
+import jakarta.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
-
-import jakarta.inject.Singleton;
-
import lombok.Getter;
@Singleton
@Getter
public class CommaFeedVersion {
- private final String version;
- private final String gitCommit;
+ private final String version;
+ private final String gitCommit;
- public CommaFeedVersion() throws IOException {
- Properties properties = new Properties();
- try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
- if (stream != null) {
- properties.load(stream);
- }
- }
-
- this.version = properties.getProperty("git.build.version", "unknown");
- this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
- }
+ public CommaFeedVersion() throws IOException {
+ Properties properties = new Properties();
+ try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
+ if (stream != null) {
+ properties.load(stream);
+ }
+ }
+ this.version = properties.getProperty("git.build.version", "unknown");
+ this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java
index 752738c2..ccb2eca9 100644
--- a/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java
+++ b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java
@@ -1,52 +1,54 @@
package com.commafeed;
+import com.commafeed.security.CookieService;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+import io.quarkus.security.AuthenticationFailedException;
+import io.quarkus.security.UnauthorizedException;
import jakarta.annotation.Priority;
import jakarta.validation.ValidationException;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.ext.Provider;
-
+import lombok.RequiredArgsConstructor;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder;
import org.jboss.resteasy.reactive.RestResponse.Status;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
-import com.commafeed.security.CookieService;
-
-import io.quarkus.runtime.annotations.RegisterForReflection;
-import io.quarkus.security.AuthenticationFailedException;
-import io.quarkus.security.UnauthorizedException;
-import lombok.RequiredArgsConstructor;
-
@RequiredArgsConstructor
@Provider
@Priority(1)
public class ExceptionMappers {
- private final CookieService cookieService;
- private final CommaFeedConfiguration config;
+ private final CookieService cookieService;
+ private final CommaFeedConfiguration config;
- @ServerExceptionMapper(UnauthorizedException.class)
- public RestResponse unauthorized(UnauthorizedException e) {
- return RestResponse.status(Status.UNAUTHORIZED, new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
- }
+ @ServerExceptionMapper(UnauthorizedException.class)
+ public RestResponse unauthorized(UnauthorizedException e) {
+ return RestResponse.status(
+ Status.UNAUTHORIZED,
+ new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
+ }
- @ServerExceptionMapper(AuthenticationFailedException.class)
- public RestResponse authenticationFailed(AuthenticationFailedException e) {
- NewCookie logoutCookie = cookieService.buildLogoutCookie();
- return ResponseBuilder.create(Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage())).cookie(logoutCookie).build();
- }
+ @ServerExceptionMapper(AuthenticationFailedException.class)
+ public RestResponse authenticationFailed(
+ AuthenticationFailedException e) {
+ NewCookie logoutCookie = cookieService.buildLogoutCookie();
+ return ResponseBuilder.create(Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage()))
+ .cookie(logoutCookie)
+ .build();
+ }
- @ServerExceptionMapper(ValidationException.class)
- public RestResponse validationFailed(ValidationException e) {
- return RestResponse.status(Status.BAD_REQUEST, new ValidationFailed(e.getMessage()));
- }
+ @ServerExceptionMapper(ValidationException.class)
+ public RestResponse validationFailed(ValidationException e) {
+ return RestResponse.status(Status.BAD_REQUEST, new ValidationFailed(e.getMessage()));
+ }
- @RegisterForReflection
- public record UnauthorizedResponse(String message, boolean allowRegistrations) {}
+ @RegisterForReflection
+ public record UnauthorizedResponse(String message, boolean allowRegistrations) {}
- @RegisterForReflection
- public record AuthenticationFailed(String message) {}
+ @RegisterForReflection
+ public record AuthenticationFailed(String message) {}
- @RegisterForReflection
- public record ValidationFailed(String message) {}
+ @RegisterForReflection
+ public record ValidationFailed(String message) {}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java
index fb80944e..3e985c11 100644
--- a/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java
+++ b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java
@@ -1,29 +1,27 @@
package com.commafeed;
-import java.util.concurrent.TimeUnit;
-
-import jakarta.inject.Singleton;
-
import com.codahale.metrics.json.MetricsModule;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-
import io.quarkus.jackson.ObjectMapperCustomizer;
+import jakarta.inject.Singleton;
+import java.util.concurrent.TimeUnit;
@Singleton
public class JacksonCustomizer implements ObjectMapperCustomizer {
- @Override
- public void customize(ObjectMapper objectMapper) {
- objectMapper.registerModule(new JavaTimeModule());
+ @Override
+ public void customize(ObjectMapper objectMapper) {
+ objectMapper.registerModule(new JavaTimeModule());
- // read and write instants as milliseconds instead of nanoseconds
- objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
- .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
- .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
+ // read and write instants as milliseconds instead of nanoseconds
+ objectMapper
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
- // add support for serializing metrics
- objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
- }
+ // add support for serializing metrics
+ objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java
index 3bfc9df9..bbe8f03c 100644
--- a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java
+++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java
@@ -6,221 +6,343 @@ import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
-
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection(
- targets = {
- // metrics
- MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class,
+ targets = {
+ // metrics
+ MetricRegistry.class,
+ Meter.class,
+ Gauge.class,
+ Counter.class,
+ Timer.class,
+ Histogram.class,
- // rome
- java.util.Date.class, com.rometools.opml.feed.synd.impl.TreeCategoryImpl.class,
- com.rometools.rome.feed.synd.SyndFeedImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class,
- com.rometools.rome.feed.synd.SyndEntryImpl.class, com.rometools.modules.psc.types.SimpleChapter.class,
- com.rometools.rome.feed.synd.SyndCategoryImpl.class, com.rometools.rome.feed.synd.SyndImageImpl.class,
- com.rometools.rome.feed.synd.SyndContentImpl.class, com.rometools.rome.feed.synd.SyndEnclosureImpl.class,
+ // rome
+ java.util.Date.class,
+ com.rometools.opml.feed.synd.impl.TreeCategoryImpl.class,
+ com.rometools.rome.feed.synd.SyndFeedImpl.class,
+ com.rometools.rome.feed.module.DCSubjectImpl.class,
+ com.rometools.rome.feed.synd.SyndEntryImpl.class,
+ com.rometools.modules.psc.types.SimpleChapter.class,
+ com.rometools.rome.feed.synd.SyndCategoryImpl.class,
+ com.rometools.rome.feed.synd.SyndImageImpl.class,
+ com.rometools.rome.feed.synd.SyndContentImpl.class,
+ com.rometools.rome.feed.synd.SyndEnclosureImpl.class,
- // rome cloneable
- com.rometools.modules.activitystreams.types.Article.class, com.rometools.modules.activitystreams.types.Audio.class,
- com.rometools.modules.activitystreams.types.Bookmark.class, com.rometools.modules.activitystreams.types.Comment.class,
- com.rometools.modules.activitystreams.types.Event.class, com.rometools.modules.activitystreams.types.File.class,
- com.rometools.modules.activitystreams.types.Folder.class, com.rometools.modules.activitystreams.types.List.class,
- com.rometools.modules.activitystreams.types.Note.class, com.rometools.modules.activitystreams.types.Person.class,
- com.rometools.modules.activitystreams.types.Photo.class, com.rometools.modules.activitystreams.types.PhotoAlbum.class,
- com.rometools.modules.activitystreams.types.Place.class, com.rometools.modules.activitystreams.types.Playlist.class,
- com.rometools.modules.activitystreams.types.Product.class, com.rometools.modules.activitystreams.types.Review.class,
- com.rometools.modules.activitystreams.types.Service.class, com.rometools.modules.activitystreams.types.Song.class,
- com.rometools.modules.activitystreams.types.Status.class, com.rometools.modules.base.types.DateTimeRange.class,
- com.rometools.modules.base.types.FloatUnit.class, com.rometools.modules.base.types.GenderEnumeration.class,
- com.rometools.modules.base.types.IntUnit.class, com.rometools.modules.base.types.PriceTypeEnumeration.class,
- com.rometools.modules.base.types.ShippingType.class, com.rometools.modules.base.types.ShortDate.class,
- com.rometools.modules.base.types.Size.class, com.rometools.modules.base.types.YearType.class,
- com.rometools.modules.content.ContentItem.class, com.rometools.modules.georss.GeoRSSPoint.class,
- com.rometools.modules.georss.geometries.Envelope.class, com.rometools.modules.georss.geometries.LineString.class,
- com.rometools.modules.georss.geometries.LinearRing.class, com.rometools.modules.georss.geometries.Point.class,
- com.rometools.modules.georss.geometries.Polygon.class, com.rometools.modules.georss.geometries.Position.class,
- com.rometools.modules.georss.geometries.PositionList.class, com.rometools.modules.mediarss.types.MediaGroup.class,
- com.rometools.modules.mediarss.types.Metadata.class, com.rometools.modules.mediarss.types.Thumbnail.class,
- com.rometools.modules.opensearch.entity.OSQuery.class, com.rometools.modules.photocast.types.PhotoDate.class,
- com.rometools.modules.sle.types.DateValue.class, com.rometools.modules.sle.types.Group.class,
- com.rometools.modules.sle.types.NumberValue.class, com.rometools.modules.sle.types.Sort.class,
- com.rometools.modules.sle.types.StringValue.class, com.rometools.modules.yahooweather.types.Astronomy.class,
- com.rometools.modules.yahooweather.types.Atmosphere.class, com.rometools.modules.yahooweather.types.Condition.class,
- com.rometools.modules.yahooweather.types.Forecast.class, com.rometools.modules.yahooweather.types.Location.class,
- com.rometools.modules.yahooweather.types.Units.class, com.rometools.modules.yahooweather.types.Wind.class,
- com.rometools.opml.feed.opml.Attribute.class, com.rometools.opml.feed.opml.Opml.class,
- com.rometools.opml.feed.opml.Outline.class, com.rometools.rome.feed.atom.Category.class,
- com.rometools.rome.feed.atom.Content.class, com.rometools.rome.feed.atom.Entry.class,
- com.rometools.rome.feed.atom.Feed.class, com.rometools.rome.feed.atom.Generator.class,
- com.rometools.rome.feed.atom.Link.class, com.rometools.rome.feed.atom.Person.class,
- com.rometools.rome.feed.rss.Category.class, com.rometools.rome.feed.rss.Channel.class,
- com.rometools.rome.feed.rss.Cloud.class, com.rometools.rome.feed.rss.Content.class,
- com.rometools.rome.feed.rss.Description.class, com.rometools.rome.feed.rss.Enclosure.class,
- com.rometools.rome.feed.rss.Guid.class, com.rometools.rome.feed.rss.Image.class, com.rometools.rome.feed.rss.Item.class,
- com.rometools.rome.feed.rss.Source.class, com.rometools.rome.feed.rss.TextInput.class,
- com.rometools.rome.feed.synd.SyndLinkImpl.class, com.rometools.rome.feed.synd.SyndPersonImpl.class,
- java.util.ArrayList.class,
+ // rome cloneable
+ com.rometools.modules.activitystreams.types.Article.class,
+ com.rometools.modules.activitystreams.types.Audio.class,
+ com.rometools.modules.activitystreams.types.Bookmark.class,
+ com.rometools.modules.activitystreams.types.Comment.class,
+ com.rometools.modules.activitystreams.types.Event.class,
+ com.rometools.modules.activitystreams.types.File.class,
+ com.rometools.modules.activitystreams.types.Folder.class,
+ com.rometools.modules.activitystreams.types.List.class,
+ com.rometools.modules.activitystreams.types.Note.class,
+ com.rometools.modules.activitystreams.types.Person.class,
+ com.rometools.modules.activitystreams.types.Photo.class,
+ com.rometools.modules.activitystreams.types.PhotoAlbum.class,
+ com.rometools.modules.activitystreams.types.Place.class,
+ com.rometools.modules.activitystreams.types.Playlist.class,
+ com.rometools.modules.activitystreams.types.Product.class,
+ com.rometools.modules.activitystreams.types.Review.class,
+ com.rometools.modules.activitystreams.types.Service.class,
+ com.rometools.modules.activitystreams.types.Song.class,
+ com.rometools.modules.activitystreams.types.Status.class,
+ com.rometools.modules.base.types.DateTimeRange.class,
+ com.rometools.modules.base.types.FloatUnit.class,
+ com.rometools.modules.base.types.GenderEnumeration.class,
+ com.rometools.modules.base.types.IntUnit.class,
+ com.rometools.modules.base.types.PriceTypeEnumeration.class,
+ com.rometools.modules.base.types.ShippingType.class,
+ com.rometools.modules.base.types.ShortDate.class,
+ com.rometools.modules.base.types.Size.class,
+ com.rometools.modules.base.types.YearType.class,
+ com.rometools.modules.content.ContentItem.class,
+ com.rometools.modules.georss.GeoRSSPoint.class,
+ com.rometools.modules.georss.geometries.Envelope.class,
+ com.rometools.modules.georss.geometries.LineString.class,
+ com.rometools.modules.georss.geometries.LinearRing.class,
+ com.rometools.modules.georss.geometries.Point.class,
+ com.rometools.modules.georss.geometries.Polygon.class,
+ com.rometools.modules.georss.geometries.Position.class,
+ com.rometools.modules.georss.geometries.PositionList.class,
+ com.rometools.modules.mediarss.types.MediaGroup.class,
+ com.rometools.modules.mediarss.types.Metadata.class,
+ com.rometools.modules.mediarss.types.Thumbnail.class,
+ com.rometools.modules.opensearch.entity.OSQuery.class,
+ com.rometools.modules.photocast.types.PhotoDate.class,
+ com.rometools.modules.sle.types.DateValue.class,
+ com.rometools.modules.sle.types.Group.class,
+ com.rometools.modules.sle.types.NumberValue.class,
+ com.rometools.modules.sle.types.Sort.class,
+ com.rometools.modules.sle.types.StringValue.class,
+ com.rometools.modules.yahooweather.types.Astronomy.class,
+ com.rometools.modules.yahooweather.types.Atmosphere.class,
+ com.rometools.modules.yahooweather.types.Condition.class,
+ com.rometools.modules.yahooweather.types.Forecast.class,
+ com.rometools.modules.yahooweather.types.Location.class,
+ com.rometools.modules.yahooweather.types.Units.class,
+ com.rometools.modules.yahooweather.types.Wind.class,
+ com.rometools.opml.feed.opml.Attribute.class,
+ com.rometools.opml.feed.opml.Opml.class,
+ com.rometools.opml.feed.opml.Outline.class,
+ com.rometools.rome.feed.atom.Category.class,
+ com.rometools.rome.feed.atom.Content.class,
+ com.rometools.rome.feed.atom.Entry.class,
+ com.rometools.rome.feed.atom.Feed.class,
+ com.rometools.rome.feed.atom.Generator.class,
+ com.rometools.rome.feed.atom.Link.class,
+ com.rometools.rome.feed.atom.Person.class,
+ com.rometools.rome.feed.rss.Category.class,
+ com.rometools.rome.feed.rss.Channel.class,
+ com.rometools.rome.feed.rss.Cloud.class,
+ com.rometools.rome.feed.rss.Content.class,
+ com.rometools.rome.feed.rss.Description.class,
+ com.rometools.rome.feed.rss.Enclosure.class,
+ com.rometools.rome.feed.rss.Guid.class,
+ com.rometools.rome.feed.rss.Image.class,
+ com.rometools.rome.feed.rss.Item.class,
+ com.rometools.rome.feed.rss.Source.class,
+ com.rometools.rome.feed.rss.TextInput.class,
+ com.rometools.rome.feed.synd.SyndLinkImpl.class,
+ com.rometools.rome.feed.synd.SyndPersonImpl.class,
+ java.util.ArrayList.class,
- // rome modules
- com.rometools.modules.sse.modules.Conflict.class, com.rometools.modules.sse.modules.Conflicts.class,
- com.rometools.modules.cc.CreativeCommonsImpl.class, com.rometools.modules.feedpress.modules.FeedpressModuleImpl.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleImpl.class, com.rometools.modules.sse.modules.Sharing.class,
- com.rometools.modules.georss.SimpleModuleImpl.class, com.rometools.modules.atom.modules.AtomLinkModuleImpl.class,
- com.rometools.modules.itunes.EntryInformationImpl.class, com.rometools.modules.sse.modules.Update.class,
- com.rometools.modules.photocast.PhotocastModuleImpl.class, com.rometools.modules.itunes.FeedInformationImpl.class,
- com.rometools.modules.yahooweather.YWeatherModuleImpl.class, com.rometools.modules.feedburner.FeedBurnerImpl.class,
- com.rometools.modules.sse.modules.Related.class, com.rometools.modules.fyyd.modules.FyydModuleImpl.class,
- com.rometools.modules.psc.modules.PodloveSimpleChapterModuleImpl.class, com.rometools.modules.thr.ThreadingModuleImpl.class,
- com.rometools.modules.sse.modules.Sync.class, com.rometools.modules.sle.SimpleListExtensionImpl.class,
- com.rometools.modules.slash.SlashImpl.class, com.rometools.modules.sse.modules.History.class,
- com.rometools.modules.georss.GMLModuleImpl.class, com.rometools.modules.base.CustomTagsImpl.class,
- com.rometools.modules.base.GoogleBaseImpl.class, com.rometools.modules.sle.SleEntryImpl.class,
- com.rometools.modules.mediarss.MediaEntryModuleImpl.class, com.rometools.modules.content.ContentModuleImpl.class,
- com.rometools.modules.georss.W3CGeoModuleImpl.class, com.rometools.rome.feed.module.DCModuleImpl.class,
- com.rometools.modules.mediarss.MediaModuleImpl.class, com.rometools.rome.feed.module.SyModuleImpl.class,
+ // rome modules
+ com.rometools.modules.sse.modules.Conflict.class,
+ com.rometools.modules.sse.modules.Conflicts.class,
+ com.rometools.modules.cc.CreativeCommonsImpl.class,
+ com.rometools.modules.feedpress.modules.FeedpressModuleImpl.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleImpl.class,
+ com.rometools.modules.sse.modules.Sharing.class,
+ com.rometools.modules.georss.SimpleModuleImpl.class,
+ com.rometools.modules.atom.modules.AtomLinkModuleImpl.class,
+ com.rometools.modules.itunes.EntryInformationImpl.class,
+ com.rometools.modules.sse.modules.Update.class,
+ com.rometools.modules.photocast.PhotocastModuleImpl.class,
+ com.rometools.modules.itunes.FeedInformationImpl.class,
+ com.rometools.modules.yahooweather.YWeatherModuleImpl.class,
+ com.rometools.modules.feedburner.FeedBurnerImpl.class,
+ com.rometools.modules.sse.modules.Related.class,
+ com.rometools.modules.fyyd.modules.FyydModuleImpl.class,
+ com.rometools.modules.psc.modules.PodloveSimpleChapterModuleImpl.class,
+ com.rometools.modules.thr.ThreadingModuleImpl.class,
+ com.rometools.modules.sse.modules.Sync.class,
+ com.rometools.modules.sle.SimpleListExtensionImpl.class,
+ com.rometools.modules.slash.SlashImpl.class,
+ com.rometools.modules.sse.modules.History.class,
+ com.rometools.modules.georss.GMLModuleImpl.class,
+ com.rometools.modules.base.CustomTagsImpl.class,
+ com.rometools.modules.base.GoogleBaseImpl.class,
+ com.rometools.modules.sle.SleEntryImpl.class,
+ com.rometools.modules.mediarss.MediaEntryModuleImpl.class,
+ com.rometools.modules.content.ContentModuleImpl.class,
+ com.rometools.modules.georss.W3CGeoModuleImpl.class,
+ com.rometools.rome.feed.module.DCModuleImpl.class,
+ com.rometools.modules.mediarss.MediaModuleImpl.class,
+ com.rometools.rome.feed.module.SyModuleImpl.class,
- // extracted from all 3 rome.properties files of rome library
- com.rometools.rome.io.impl.RSS090Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class,
- com.rometools.rome.io.impl.RSS091UserlandParser.class, com.rometools.rome.io.impl.RSS092Parser.class,
- com.rometools.rome.io.impl.RSS093Parser.class, com.rometools.rome.io.impl.RSS094Parser.class,
- com.rometools.rome.io.impl.RSS10Parser.class, com.rometools.rome.io.impl.RSS20wNSParser.class,
- com.rometools.rome.io.impl.RSS20Parser.class, com.rometools.rome.io.impl.Atom10Parser.class,
- com.rometools.rome.io.impl.Atom03Parser.class,
-
- com.rometools.rome.io.impl.SyModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class,
-
- com.rometools.rome.io.impl.RSS090Generator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class,
- com.rometools.rome.io.impl.RSS091UserlandGenerator.class, com.rometools.rome.io.impl.RSS092Generator.class,
- com.rometools.rome.io.impl.RSS093Generator.class, com.rometools.rome.io.impl.RSS094Generator.class,
- com.rometools.rome.io.impl.RSS10Generator.class, com.rometools.rome.io.impl.RSS20Generator.class,
- com.rometools.rome.io.impl.Atom10Generator.class, com.rometools.rome.io.impl.Atom03Generator.class,
-
- com.rometools.rome.feed.synd.impl.ConverterForAtom10.class, com.rometools.rome.feed.synd.impl.ConverterForAtom03.class,
- com.rometools.rome.feed.synd.impl.ConverterForRSS090.class,
- com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class,
- com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class,
- com.rometools.rome.feed.synd.impl.ConverterForRSS092.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class,
- com.rometools.rome.feed.synd.impl.ConverterForRSS094.class, com.rometools.rome.feed.synd.impl.ConverterForRSS10.class,
- com.rometools.rome.feed.synd.impl.ConverterForRSS20.class,
-
- com.rometools.modules.mediarss.io.RSS20YahooParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
- com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
- com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
- com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
- com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
- com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
- com.rometools.modules.fyyd.io.FyydParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
- com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
- com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
- com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
- com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
- com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
- com.rometools.modules.fyyd.io.FyydParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.content.io.ContentModuleParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
- com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
- com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
- com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
- com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
- com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.fyyd.io.FyydParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
- com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
- com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
- com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
- com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ItemParser.class,
- com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
- com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.base.io.GoogleBaseParser.class,
- com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
- com.rometools.modules.slash.io.SlashModuleParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
- com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
- com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
- com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
-
- com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
- com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
- com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
- com.rometools.modules.mediarss.io.MediaModuleParser.class,
- com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
- com.rometools.modules.thr.io.ThreadingModuleParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
-
- com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
- com.rometools.modules.itunes.io.ITunesGenerator.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
- com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
- com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
- com.rometools.modules.sle.io.ModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
- com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
-
- com.rometools.modules.content.io.ContentModuleGenerator.class,
-
- com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
- com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
- com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
-
- com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
- com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
- com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
- com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
-
- com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
- com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
- com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.itunes.io.ITunesGenerator.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
- com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
- com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
- com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
- com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
-
- com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
- com.rometools.modules.slash.io.SlashModuleGenerator.class,
-
- com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
- com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
- com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
- com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
-
- com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class,
- com.rometools.modules.slash.io.SlashModuleGenerator.class,
- com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
- com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
- com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.thr.io.ThreadingModuleGenerator.class,
- com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
-
- com.rometools.modules.mediarss.io.MediaModuleParser.class,
-
- com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
-
- com.rometools.opml.io.impl.OPML10Generator.class, com.rometools.opml.io.impl.OPML20Generator.class,
-
- com.rometools.opml.io.impl.OPML10Parser.class, com.rometools.opml.io.impl.OPML20Parser.class,
-
- com.rometools.opml.feed.synd.impl.ConverterForOPML10.class, com.rometools.opml.feed.synd.impl.ConverterForOPML20.class, })
-
-public class NativeImageClasses {
-}
+ // extracted from all 3 rome.properties files of rome library
+ com.rometools.rome.io.impl.RSS090Parser.class,
+ com.rometools.rome.io.impl.RSS091NetscapeParser.class,
+ com.rometools.rome.io.impl.RSS091UserlandParser.class,
+ com.rometools.rome.io.impl.RSS092Parser.class,
+ com.rometools.rome.io.impl.RSS093Parser.class,
+ com.rometools.rome.io.impl.RSS094Parser.class,
+ com.rometools.rome.io.impl.RSS10Parser.class,
+ com.rometools.rome.io.impl.RSS20wNSParser.class,
+ com.rometools.rome.io.impl.RSS20Parser.class,
+ com.rometools.rome.io.impl.Atom10Parser.class,
+ com.rometools.rome.io.impl.Atom03Parser.class,
+ com.rometools.rome.io.impl.SyModuleParser.class,
+ com.rometools.rome.io.impl.DCModuleParser.class,
+ com.rometools.rome.io.impl.RSS090Generator.class,
+ com.rometools.rome.io.impl.RSS091NetscapeGenerator.class,
+ com.rometools.rome.io.impl.RSS091UserlandGenerator.class,
+ com.rometools.rome.io.impl.RSS092Generator.class,
+ com.rometools.rome.io.impl.RSS093Generator.class,
+ com.rometools.rome.io.impl.RSS094Generator.class,
+ com.rometools.rome.io.impl.RSS10Generator.class,
+ com.rometools.rome.io.impl.RSS20Generator.class,
+ com.rometools.rome.io.impl.Atom10Generator.class,
+ com.rometools.rome.io.impl.Atom03Generator.class,
+ com.rometools.rome.feed.synd.impl.ConverterForAtom10.class,
+ com.rometools.rome.feed.synd.impl.ConverterForAtom03.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS090.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS092.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS093.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS094.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS10.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS20.class,
+ com.rometools.modules.mediarss.io.RSS20YahooParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.atom.io.AtomModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.sle.io.ModuleParser.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
+ com.rometools.modules.feedpress.io.FeedpressParser.class,
+ com.rometools.modules.fyyd.io.FyydParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.atom.io.AtomModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.sle.io.ModuleParser.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
+ com.rometools.modules.feedpress.io.FeedpressParser.class,
+ com.rometools.modules.fyyd.io.FyydParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS1.class,
+ com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.feedpress.io.FeedpressParser.class,
+ com.rometools.modules.fyyd.io.FyydParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.atom.io.AtomModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.sle.io.ItemParser.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS1.class,
+ com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.base.io.CustomTagParser.class,
+ com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.base.io.CustomTagParser.class,
+ com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.cc.io.ModuleParserRSS2.class,
+ com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.base.io.CustomTagParser.class,
+ com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.thr.io.ThreadingModuleParser.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
+ com.rometools.modules.cc.io.CCModuleGenerator.class,
+ com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.itunes.io.ITunesGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.atom.io.AtomModuleGenerator.class,
+ com.rometools.modules.sle.io.ModuleGenerator.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
+ com.rometools.modules.feedpress.io.FeedpressGenerator.class,
+ com.rometools.modules.fyyd.io.FyydGenerator.class,
+ com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.cc.io.CCModuleGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.cc.io.CCModuleGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.feedpress.io.FeedpressGenerator.class,
+ com.rometools.modules.fyyd.io.FyydGenerator.class,
+ com.rometools.modules.cc.io.CCModuleGenerator.class,
+ com.rometools.modules.base.io.GoogleBaseGenerator.class,
+ com.rometools.modules.base.io.CustomTagGenerator.class,
+ com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class,
+ com.rometools.modules.itunes.io.ITunesGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.atom.io.AtomModuleGenerator.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
+ com.rometools.modules.base.io.GoogleBaseGenerator.class,
+ com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class,
+ com.rometools.modules.cc.io.CCModuleGenerator.class,
+ com.rometools.modules.base.io.GoogleBaseGenerator.class,
+ com.rometools.modules.base.io.CustomTagGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.cc.io.CCModuleGenerator.class,
+ com.rometools.modules.base.io.CustomTagGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.thr.io.ThreadingModuleGenerator.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.opml.io.impl.OPML10Generator.class,
+ com.rometools.opml.io.impl.OPML20Generator.class,
+ com.rometools.opml.io.impl.OPML10Parser.class,
+ com.rometools.opml.io.impl.OPML20Parser.class,
+ com.rometools.opml.feed.synd.impl.ConverterForOPML10.class,
+ com.rometools.opml.feed.synd.impl.ConverterForOPML20.class,
+ })
+public class NativeImageClasses {}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/Digests.java b/commafeed-server/src/main/java/com/commafeed/backend/Digests.java
index 86819e81..603b50e1 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/Digests.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/Digests.java
@@ -1,29 +1,27 @@
package com.commafeed.backend;
-import java.nio.charset.StandardCharsets;
-
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
-
+import java.nio.charset.StandardCharsets;
import lombok.experimental.UtilityClass;
@UtilityClass
@SuppressWarnings("deprecation")
public class Digests {
- public static String sha1Hex(byte[] input) {
- return hashBytesToHex(Hashing.sha1(), input);
- }
+ public static String sha1Hex(byte[] input) {
+ return hashBytesToHex(Hashing.sha1(), input);
+ }
- public static String sha1Hex(String input) {
- return hashBytesToHex(Hashing.sha1(), input.getBytes(StandardCharsets.UTF_8));
- }
+ public static String sha1Hex(String input) {
+ return hashBytesToHex(Hashing.sha1(), input.getBytes(StandardCharsets.UTF_8));
+ }
- public static String md5Hex(String input) {
- return hashBytesToHex(Hashing.md5(), input.getBytes(StandardCharsets.UTF_8));
- }
+ public static String md5Hex(String input) {
+ return hashBytesToHex(Hashing.md5(), input.getBytes(StandardCharsets.UTF_8));
+ }
- private static String hashBytesToHex(HashFunction function, byte[] input) {
- return function.hashBytes(input).toString();
- }
+ private static String hashBytesToHex(HashFunction function, byte[] input) {
+ return function.hashBytes(input).toString();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java
index 53de6d4b..05e13da7 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpClientFactory.java
@@ -1,5 +1,12 @@
package com.commafeed.backend;
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.CommaFeedVersion;
+import com.google.common.net.HttpHeaders;
+import inet.ipaddr.IPAddress;
+import inet.ipaddr.IPAddressNetwork;
+import inet.ipaddr.IPAddressString;
+import jakarta.inject.Singleton;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
@@ -8,9 +15,9 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedMap;
import java.util.zip.GZIPInputStream;
-
-import jakarta.inject.Singleton;
-
+import lombok.RequiredArgsConstructor;
+import nl.altindag.ssl.SSLFactory;
+import nl.altindag.ssl.apache5.util.Apache5SslUtils;
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.config.ConnectionConfig;
@@ -35,137 +42,148 @@ import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.brotli.dec.BrotliInputStream;
-import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.CommaFeedVersion;
-import com.google.common.net.HttpHeaders;
-
-import inet.ipaddr.IPAddress;
-import inet.ipaddr.IPAddressNetwork;
-import inet.ipaddr.IPAddressString;
-import lombok.RequiredArgsConstructor;
-import nl.altindag.ssl.SSLFactory;
-import nl.altindag.ssl.apache5.util.Apache5SslUtils;
-
@Singleton
@RequiredArgsConstructor
public class HttpClientFactory {
- private static final DnsResolver DNS_RESOLVER = SystemDefaultDnsResolver.INSTANCE;
- private static final IPAddress CGNAT_RANGE = new IPAddressString("100.64.0.0/10").getAddress();
+ private static final DnsResolver DNS_RESOLVER = SystemDefaultDnsResolver.INSTANCE;
+ private static final IPAddress CGNAT_RANGE = new IPAddressString("100.64.0.0/10").getAddress();
- private final CommaFeedConfiguration config;
- private final CommaFeedVersion version;
+ private final CommaFeedConfiguration config;
+ private final CommaFeedVersion version;
- public CloseableHttpClient newClient(int poolSize) {
- PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config, poolSize);
- String userAgent = config.httpClient()
- .userAgent()
- .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
- return newClient(config, connectionManager, userAgent);
- }
+ public CloseableHttpClient newClient(int poolSize) {
+ PoolingHttpClientConnectionManager connectionManager =
+ newConnectionManager(config, poolSize);
+ String userAgent =
+ config.httpClient()
+ .userAgent()
+ .orElseGet(
+ () ->
+ String.format(
+ "CommaFeed/%s (https://github.com/Athou/commafeed)",
+ version.getVersion()));
+ return newClient(config, connectionManager, userAgent);
+ }
- private CloseableHttpClient newClient(CommaFeedConfiguration config, HttpClientConnectionManager connectionManager, String userAgent) {
- List headers = new ArrayList<>();
- headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
- headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
- headers.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, "no-cache"));
+ private CloseableHttpClient newClient(
+ CommaFeedConfiguration config,
+ HttpClientConnectionManager connectionManager,
+ String userAgent) {
+ List headers = new ArrayList<>();
+ headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
+ headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
+ headers.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, "no-cache"));
- SequencedMap contentDecoderMap = new LinkedHashMap<>();
- contentDecoderMap.put(ContentCoding.GZIP.token(), GZIPInputStream::new);
- contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new);
- contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new);
+ SequencedMap contentDecoderMap = new LinkedHashMap<>();
+ contentDecoderMap.put(ContentCoding.GZIP.token(), GZIPInputStream::new);
+ contentDecoderMap.put(ContentCoding.DEFLATE.token(), DeflateInputStream::new);
+ contentDecoderMap.put(ContentCoding.BROTLI.token(), BrotliInputStream::new);
- RedirectStrategy redirectStrategy = config.httpClient().blockLocalAddresses()
- ? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER)
- : new DefaultRedirectStrategy();
+ RedirectStrategy redirectStrategy =
+ config.httpClient().blockLocalAddresses()
+ ? new BlockLocalAddressesRedirectStrategy(DNS_RESOLVER)
+ : new DefaultRedirectStrategy();
- return HttpClientBuilder.create()
- .disableConnectionState()
- .useSystemProperties()
- .disableAutomaticRetries()
- .disableCookieManagement()
- .setUserAgent(userAgent)
- .setDefaultHeaders(headers)
- .setConnectionManager(connectionManager)
- .evictExpiredConnections()
- .evictIdleConnections(TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
- .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap))
- .setRedirectStrategy(redirectStrategy)
- .build();
- }
+ return HttpClientBuilder.create()
+ .disableConnectionState()
+ .useSystemProperties()
+ .disableAutomaticRetries()
+ .disableCookieManagement()
+ .setUserAgent(userAgent)
+ .setDefaultHeaders(headers)
+ .setConnectionManager(connectionManager)
+ .evictExpiredConnections()
+ .evictIdleConnections(
+ TimeValue.of(config.httpClient().idleConnectionsEvictionInterval()))
+ .setContentDecoderRegistry(new LinkedHashMap<>(contentDecoderMap))
+ .setRedirectStrategy(redirectStrategy)
+ .build();
+ }
- private PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config, int poolSize) {
- SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
- DnsResolver dnsResolver = config.httpClient().blockLocalAddresses() ? new BlockLocalAddressesDnsResolver(DNS_RESOLVER)
- : DNS_RESOLVER;
+ private PoolingHttpClientConnectionManager newConnectionManager(
+ CommaFeedConfiguration config, int poolSize) {
+ SSLFactory sslFactory =
+ SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
+ DnsResolver dnsResolver =
+ config.httpClient().blockLocalAddresses()
+ ? new BlockLocalAddressesDnsResolver(DNS_RESOLVER)
+ : DNS_RESOLVER;
- return PoolingHttpClientConnectionManagerBuilder.create()
- .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
- .setDefaultConnectionConfig(ConnectionConfig.custom()
- .setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
- .setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
- .setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive()))
- .build())
- .setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
- .setMaxConnPerRoute(poolSize)
- .setMaxConnTotal(poolSize)
- .setDnsResolver(dnsResolver)
- .build();
+ return PoolingHttpClientConnectionManagerBuilder.create()
+ .setTlsSocketStrategy(Apache5SslUtils.toTlsSocketStrategy(sslFactory))
+ .setDefaultConnectionConfig(
+ ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
+ .setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
+ .setTimeToLive(
+ Timeout.of(config.httpClient().connectionTimeToLive()))
+ .build())
+ .setDefaultTlsConfig(
+ TlsConfig.custom()
+ .setHandshakeTimeout(
+ Timeout.of(config.httpClient().sslHandshakeTimeout()))
+ .build())
+ .setMaxConnPerRoute(poolSize)
+ .setMaxConnTotal(poolSize)
+ .setDnsResolver(dnsResolver)
+ .build();
+ }
- }
+ private static boolean isLocalAddress(InetAddress address) {
+ IPAddress ip = new IPAddressNetwork.IPAddressGenerator().from(address);
+ return ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip);
+ }
- private static boolean isLocalAddress(InetAddress address) {
- IPAddress ip = new IPAddressNetwork.IPAddressGenerator().from(address);
- return ip.isLocal() || ip.isLoopback() || ip.isMulticast() || CGNAT_RANGE.contains(ip);
- }
+ private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver {
+ @Override
+ public InetAddress[] resolve(String host) throws UnknownHostException {
+ InetAddress[] addresses = delegate.resolve(host);
+ for (InetAddress addr : addresses) {
+ if (isLocalAddress(addr)) {
+ throw new UnknownHostException(
+ "Access to local address blocked: " + addr.getHostAddress());
+ }
+ }
+ return addresses;
+ }
- private record BlockLocalAddressesDnsResolver(DnsResolver delegate) implements DnsResolver {
- @Override
- public InetAddress[] resolve(String host) throws UnknownHostException {
- InetAddress[] addresses = delegate.resolve(host);
- for (InetAddress addr : addresses) {
- if (isLocalAddress(addr)) {
- throw new UnknownHostException("Access to local address blocked: " + addr.getHostAddress());
- }
- }
- return addresses;
- }
+ @Override
+ public String resolveCanonicalHostname(String host) throws UnknownHostException {
+ return delegate.resolveCanonicalHostname(host);
+ }
+ }
- @Override
- public String resolveCanonicalHostname(String host) throws UnknownHostException {
- return delegate.resolveCanonicalHostname(host);
- }
- }
+ @RequiredArgsConstructor
+ private static class BlockLocalAddressesRedirectStrategy extends DefaultRedirectStrategy {
- @RequiredArgsConstructor
- private static class BlockLocalAddressesRedirectStrategy extends DefaultRedirectStrategy {
+ private final DnsResolver delegate;
- private final DnsResolver delegate;
+ @Override
+ public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException {
+ URI redirectUri = super.getLocationURI(request, response, context);
- @Override
- public URI getLocationURI(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException {
- URI redirectUri = super.getLocationURI(request, response, context);
+ String host = redirectUri.getHost();
+ if (host == null) {
+ throw new HttpException("Redirect URI does not have a host: " + redirectUri);
+ }
- String host = redirectUri.getHost();
- if (host == null) {
- throw new HttpException("Redirect URI does not have a host: " + redirectUri);
- }
+ InetAddress[] addresses;
+ try {
+ addresses = delegate.resolve(host);
+ } catch (UnknownHostException e) {
+ throw new HttpException("Unknown host: " + host);
+ }
- InetAddress[] addresses;
- try {
- addresses = delegate.resolve(host);
- } catch (UnknownHostException e) {
- throw new HttpException("Unknown host: " + host);
- }
-
- for (InetAddress addr : addresses) {
- if (isLocalAddress(addr)) {
- throw new HttpException("Access to local address blocked: " + addr.getHostAddress());
- }
- }
-
- return redirectUri;
- }
- }
+ for (InetAddress addr : addresses) {
+ if (isLocalAddress(addr)) {
+ throw new HttpException(
+ "Access to local address blocked: " + addr.getHostAddress());
+ }
+ }
+ return redirectUri;
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
index 76bf5609..08f43724 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
@@ -1,5 +1,15 @@
package com.commafeed.backend;
+import com.codahale.metrics.MetricRegistry;
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.CommaFeedConfiguration.HttpClientCache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Iterables;
+import com.google.common.io.ByteStreams;
+import com.google.common.net.HttpHeaders;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.core.CacheControl;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
@@ -8,10 +18,12 @@ import java.time.Instant;
import java.time.InstantSource;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
-
-import jakarta.inject.Singleton;
-import jakarta.ws.rs.core.CacheControl;
-
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Lombok;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.config.RequestConfig;
@@ -27,282 +39,326 @@ import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Timeout;
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
-import com.codahale.metrics.MetricRegistry;
-import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.CommaFeedConfiguration.HttpClientCache;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.Iterables;
-import com.google.common.io.ByteStreams;
-import com.google.common.net.HttpHeaders;
-
-import lombok.Builder;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.Lombok;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * Smart HTTP getter: handles gzip, ssl, last modified and etag headers
- */
+/** Smart HTTP getter: handles gzip, ssl, last modified and etag headers */
@Singleton
@Slf4j
public class HttpGetter {
- private final CommaFeedConfiguration config;
- private final InstantSource instantSource;
- private final CloseableHttpClient client;
- private final Cache cache;
+ private final CommaFeedConfiguration config;
+ private final InstantSource instantSource;
+ private final CloseableHttpClient client;
+ private final Cache cache;
- public HttpGetter(CommaFeedConfiguration config, InstantSource instantSource, HttpClientFactory httpClientFactory,
- MetricRegistry metrics) {
- this.config = config;
- this.instantSource = instantSource;
- this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads());
- this.cache = newCache(config);
+ public HttpGetter(
+ CommaFeedConfiguration config,
+ InstantSource instantSource,
+ HttpClientFactory httpClientFactory,
+ MetricRegistry metrics) {
+ this.config = config;
+ this.instantSource = instantSource;
+ this.client = httpClientFactory.newClient(config.feedRefresh().httpThreads());
+ this.cache = newCache(config);
- metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "size"), () -> cache == null ? 0 : cache.size());
- metrics.registerGauge(MetricRegistry.name(getClass(), "cache", "memoryUsage"),
- () -> cache == null ? 0 : cache.asMap().values().stream().mapToInt(e -> ArrayUtils.getLength(e.content)).sum());
- }
+ metrics.registerGauge(
+ MetricRegistry.name(getClass(), "cache", "size"),
+ () -> cache == null ? 0 : cache.size());
+ metrics.registerGauge(
+ MetricRegistry.name(getClass(), "cache", "memoryUsage"),
+ () ->
+ cache == null
+ ? 0
+ : cache.asMap().values().stream()
+ .mapToInt(e -> ArrayUtils.getLength(e.content))
+ .sum());
+ }
- public HttpResult get(String url) throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
- return get(HttpRequest.builder(url).build());
- }
+ public HttpResult get(String url)
+ throws IOException,
+ NotModifiedException,
+ TooManyRequestsException,
+ SchemeNotAllowedException {
+ return get(HttpRequest.builder(url).build());
+ }
- public HttpResult get(HttpRequest request)
- throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
- URI uri = URI.create(request.getUrl());
- ensureHttpScheme(uri.getScheme());
+ public HttpResult get(HttpRequest request)
+ throws IOException,
+ NotModifiedException,
+ TooManyRequestsException,
+ SchemeNotAllowedException {
+ URI uri = URI.create(request.getUrl());
+ ensureHttpScheme(uri.getScheme());
- final HttpResponse response;
- if (cache == null) {
- response = invoke(request);
- } else {
- try {
- response = cache.get(request, () -> invoke(request));
- } catch (ExecutionException e) {
- if (e.getCause() instanceof IOException ioe) {
- throw ioe;
- } else {
- throw Lombok.sneakyThrow(e);
- }
- }
- }
+ final HttpResponse response;
+ if (cache == null) {
+ response = invoke(request);
+ } else {
+ try {
+ response = cache.get(request, () -> invoke(request));
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof IOException ioe) {
+ throw ioe;
+ } else {
+ throw Lombok.sneakyThrow(e);
+ }
+ }
+ }
- int code = response.code();
- if (code == HttpStatus.SC_TOO_MANY_REQUESTS || code == HttpStatus.SC_SERVICE_UNAVAILABLE && response.retryAfter() != null) {
- throw new TooManyRequestsException(response.retryAfter());
- }
+ int code = response.code();
+ if (code == HttpStatus.SC_TOO_MANY_REQUESTS
+ || code == HttpStatus.SC_SERVICE_UNAVAILABLE && response.retryAfter() != null) {
+ throw new TooManyRequestsException(response.retryAfter());
+ }
- if (code == HttpStatus.SC_NOT_MODIFIED) {
- throw new NotModifiedException("'304 - not modified' http code received");
- }
+ if (code == HttpStatus.SC_NOT_MODIFIED) {
+ throw new NotModifiedException("'304 - not modified' http code received");
+ }
- if (code >= 300) {
- throw new HttpResponseException(code, "Server returned HTTP error code " + code);
- }
+ if (code >= 300) {
+ throw new HttpResponseException(code, "Server returned HTTP error code " + code);
+ }
- String lastModifiedHeader = response.lastModifiedHeader();
- String eTagHeader = response.eTagHeader();
+ String lastModifiedHeader = response.lastModifiedHeader();
+ String eTagHeader = response.eTagHeader();
- Duration validFor = Optional.ofNullable(response.cacheControl())
- .filter(cc -> cc.getMaxAge() >= 0)
- .map(cc -> Duration.ofSeconds(cc.getMaxAge()))
- .orElse(Duration.ZERO);
+ Duration validFor =
+ Optional.ofNullable(response.cacheControl())
+ .filter(cc -> cc.getMaxAge() >= 0)
+ .map(cc -> Duration.ofSeconds(cc.getMaxAge()))
+ .orElse(Duration.ZERO);
- return new HttpResult(response.content(), response.contentType(), lastModifiedHeader, eTagHeader, response.urlAfterRedirect(),
- validFor);
- }
+ return new HttpResult(
+ response.content(),
+ response.contentType(),
+ lastModifiedHeader,
+ eTagHeader,
+ response.urlAfterRedirect(),
+ validFor);
+ }
- private void ensureHttpScheme(String scheme) throws SchemeNotAllowedException {
- if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
- throw new SchemeNotAllowedException(scheme);
- }
- }
+ private void ensureHttpScheme(String scheme) throws SchemeNotAllowedException {
+ if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
+ throw new SchemeNotAllowedException(scheme);
+ }
+ }
- private HttpResponse invoke(HttpRequest request) throws IOException {
- log.debug("fetching {}", request.getUrl());
+ private HttpResponse invoke(HttpRequest request) throws IOException {
+ log.debug("fetching {}", request.getUrl());
- HttpClientContext context = HttpClientContext.create();
- context.setRequestConfig(RequestConfig.custom()
- .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
- // causes issues with some feeds
- // see https://github.com/Athou/commafeed/issues/1572
- // and https://issues.apache.org/jira/browse/HTTPCLIENT-2344
- .setProtocolUpgradeEnabled(false)
- .build());
+ HttpClientContext context = HttpClientContext.create();
+ context.setRequestConfig(
+ RequestConfig.custom()
+ .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
+ // causes issues with some feeds
+ // see https://github.com/Athou/commafeed/issues/1572
+ // and https://issues.apache.org/jira/browse/HTTPCLIENT-2344
+ .setProtocolUpgradeEnabled(false)
+ .build());
- return client.execute(request.toClassicHttpRequest(), context, resp -> {
- byte[] content = resp.getEntity() == null ? null
- : toByteArray(resp.getEntity(), config.httpClient().maxResponseSize().asLongValue());
- int code = resp.getCode();
- String lastModifiedHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED))
- .map(NameValuePair::getValue)
- .map(StringUtils::trimToNull)
- .orElse(null);
- String eTagHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.ETAG))
- .map(NameValuePair::getValue)
- .map(StringUtils::trimToNull)
- .orElse(null);
+ return client.execute(
+ request.toClassicHttpRequest(),
+ context,
+ resp -> {
+ byte[] content =
+ resp.getEntity() == null
+ ? null
+ : toByteArray(
+ resp.getEntity(),
+ config.httpClient().maxResponseSize().asLongValue());
+ int code = resp.getCode();
+ String lastModifiedHeader =
+ Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED))
+ .map(NameValuePair::getValue)
+ .map(StringUtils::trimToNull)
+ .orElse(null);
+ String eTagHeader =
+ Optional.ofNullable(resp.getFirstHeader(HttpHeaders.ETAG))
+ .map(NameValuePair::getValue)
+ .map(StringUtils::trimToNull)
+ .orElse(null);
- CacheControl cacheControl = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL))
- .map(NameValuePair::getValue)
- .map(StringUtils::trimToNull)
- .map(HttpGetter::toCacheControl)
- .orElse(null);
+ CacheControl cacheControl =
+ Optional.ofNullable(resp.getFirstHeader(HttpHeaders.CACHE_CONTROL))
+ .map(NameValuePair::getValue)
+ .map(StringUtils::trimToNull)
+ .map(HttpGetter::toCacheControl)
+ .orElse(null);
- Instant retryAfter = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER))
- .map(NameValuePair::getValue)
- .map(StringUtils::trimToNull)
- .map(this::toInstant)
- .orElse(null);
+ Instant retryAfter =
+ Optional.ofNullable(resp.getFirstHeader(HttpHeaders.RETRY_AFTER))
+ .map(NameValuePair::getValue)
+ .map(StringUtils::trimToNull)
+ .map(this::toInstant)
+ .orElse(null);
- String contentType = Optional.ofNullable(resp.getEntity()).map(HttpEntity::getContentType).orElse(null);
- String urlAfterRedirect = Optional.ofNullable(context.getRedirectLocations())
- .map(RedirectLocations::getAll)
- .map(l -> Iterables.getLast(l, null))
- .map(URI::toString)
- .orElse(request.getUrl());
+ String contentType =
+ Optional.ofNullable(resp.getEntity())
+ .map(HttpEntity::getContentType)
+ .orElse(null);
+ String urlAfterRedirect =
+ Optional.ofNullable(context.getRedirectLocations())
+ .map(RedirectLocations::getAll)
+ .map(l -> Iterables.getLast(l, null))
+ .map(URI::toString)
+ .orElse(request.getUrl());
- return new HttpResponse(code, lastModifiedHeader, eTagHeader, cacheControl, retryAfter, content, contentType, urlAfterRedirect);
- });
- }
+ return new HttpResponse(
+ code,
+ lastModifiedHeader,
+ eTagHeader,
+ cacheControl,
+ retryAfter,
+ content,
+ contentType,
+ urlAfterRedirect);
+ });
+ }
- private static CacheControl toCacheControl(String headerValue) {
- try {
- return CacheControlDelegate.INSTANCE.fromString(headerValue);
- } catch (Exception e) {
- log.debug("Invalid Cache-Control header: {}", headerValue);
- return null;
- }
- }
+ private static CacheControl toCacheControl(String headerValue) {
+ try {
+ return CacheControlDelegate.INSTANCE.fromString(headerValue);
+ } catch (Exception e) {
+ log.debug("Invalid Cache-Control header: {}", headerValue);
+ return null;
+ }
+ }
- private Instant toInstant(String headerValue) {
- if (headerValue == null) {
- return null;
- }
+ private Instant toInstant(String headerValue) {
+ if (headerValue == null) {
+ return null;
+ }
- if (StringUtils.isNumeric(headerValue)) {
- return instantSource.instant().plusSeconds(Long.parseLong(headerValue));
- }
+ if (StringUtils.isNumeric(headerValue)) {
+ return instantSource.instant().plusSeconds(Long.parseLong(headerValue));
+ }
- return DateUtils.parseStandardDate(headerValue);
- }
+ return DateUtils.parseStandardDate(headerValue);
+ }
- private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
- if (entity.getContentLength() > maxBytes) {
- throw new IOException(
- "Response size (%s bytes) exceeds the maximum allowed size (%s bytes)".formatted(entity.getContentLength(), maxBytes));
- }
+ private static byte[] toByteArray(HttpEntity entity, long maxBytes) throws IOException {
+ if (entity.getContentLength() > maxBytes) {
+ throw new IOException(
+ "Response size (%s bytes) exceeds the maximum allowed size (%s bytes)"
+ .formatted(entity.getContentLength(), maxBytes));
+ }
- try (InputStream input = entity.getContent()) {
- if (input == null) {
- return null;
- }
+ try (InputStream input = entity.getContent()) {
+ if (input == null) {
+ return null;
+ }
- byte[] bytes = ByteStreams.limit(input, maxBytes + 1).readAllBytes();
- if (bytes.length > maxBytes) {
- throw new IOException("Response size exceeds the maximum allowed size (%s bytes)".formatted(maxBytes));
- }
- return bytes;
- }
- }
+ byte[] bytes = ByteStreams.limit(input, maxBytes + 1).readAllBytes();
+ if (bytes.length > maxBytes) {
+ throw new IOException(
+ "Response size exceeds the maximum allowed size (%s bytes)"
+ .formatted(maxBytes));
+ }
+ return bytes;
+ }
+ }
- private static Cache newCache(CommaFeedConfiguration config) {
- HttpClientCache cacheConfig = config.httpClient().cache();
- if (!cacheConfig.enabled()) {
- return null;
- }
+ private static Cache newCache(CommaFeedConfiguration config) {
+ HttpClientCache cacheConfig = config.httpClient().cache();
+ if (!cacheConfig.enabled()) {
+ return null;
+ }
- return CacheBuilder.newBuilder()
- .weigher((HttpRequest key, HttpResponse value) -> value.content() != null ? value.content().length : 0)
- .maximumWeight(cacheConfig.maximumMemorySize().asLongValue())
- .expireAfterWrite(cacheConfig.expiration())
- .build();
- }
+ return CacheBuilder.newBuilder()
+ .weigher(
+ (HttpRequest key, HttpResponse value) ->
+ value.content() != null ? value.content().length : 0)
+ .maximumWeight(cacheConfig.maximumMemorySize().asLongValue())
+ .expireAfterWrite(cacheConfig.expiration())
+ .build();
+ }
- public static class SchemeNotAllowedException extends Exception {
- private static final long serialVersionUID = 1L;
+ public static class SchemeNotAllowedException extends Exception {
+ private static final long serialVersionUID = 1L;
- public SchemeNotAllowedException(String scheme) {
- super("Scheme not allowed: " + scheme);
- }
- }
+ public SchemeNotAllowedException(String scheme) {
+ super("Scheme not allowed: " + scheme);
+ }
+ }
- @Getter
- public static class NotModifiedException extends Exception {
- private static final long serialVersionUID = 1L;
+ @Getter
+ public static class NotModifiedException extends Exception {
+ private static final long serialVersionUID = 1L;
- /**
- * if the value of this header changed, this is its new value
- */
- private final String newLastModifiedHeader;
+ /** if the value of this header changed, this is its new value */
+ private final String newLastModifiedHeader;
- /**
- * if the value of this header changed, this is its new value
- */
- private final String newEtagHeader;
+ /** if the value of this header changed, this is its new value */
+ private final String newEtagHeader;
- public NotModifiedException(String message) {
- this(message, null, null);
- }
+ public NotModifiedException(String message) {
+ this(message, null, null);
+ }
- public NotModifiedException(String message, String newLastModifiedHeader, String newEtagHeader) {
- super(message);
- this.newLastModifiedHeader = newLastModifiedHeader;
- this.newEtagHeader = newEtagHeader;
- }
- }
+ public NotModifiedException(
+ String message, String newLastModifiedHeader, String newEtagHeader) {
+ super(message);
+ this.newLastModifiedHeader = newLastModifiedHeader;
+ this.newEtagHeader = newEtagHeader;
+ }
+ }
- @RequiredArgsConstructor
- @Getter
- public static class TooManyRequestsException extends Exception {
- private static final long serialVersionUID = 1L;
+ @RequiredArgsConstructor
+ @Getter
+ public static class TooManyRequestsException extends Exception {
+ private static final long serialVersionUID = 1L;
- private final Instant retryAfter;
- }
+ private final Instant retryAfter;
+ }
- @Getter
- public static class HttpResponseException extends IOException {
- private static final long serialVersionUID = 1L;
+ @Getter
+ public static class HttpResponseException extends IOException {
+ private static final long serialVersionUID = 1L;
- private final int code;
+ private final int code;
- public HttpResponseException(int code, String message) {
- super(message);
- this.code = code;
- }
- }
+ public HttpResponseException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+ }
- @Builder(builderMethodName = "")
- @EqualsAndHashCode
- @Getter
- public static class HttpRequest {
- private String url;
- private String lastModified;
- private String eTag;
+ @Builder(builderMethodName = "")
+ @EqualsAndHashCode
+ @Getter
+ public static class HttpRequest {
+ private String url;
+ private String lastModified;
+ private String eTag;
- public static HttpRequestBuilder builder(String url) {
- return new HttpRequestBuilder().url(url);
- }
+ public static HttpRequestBuilder builder(String url) {
+ return new HttpRequestBuilder().url(url);
+ }
- public ClassicHttpRequest toClassicHttpRequest() {
- ClassicHttpRequest req = ClassicRequestBuilder.get(url).build();
- if (lastModified != null) {
- req.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
- }
- if (eTag != null) {
- req.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
- }
- return req;
- }
- }
+ public ClassicHttpRequest toClassicHttpRequest() {
+ ClassicHttpRequest req = ClassicRequestBuilder.get(url).build();
+ if (lastModified != null) {
+ req.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
+ }
+ if (eTag != null) {
+ req.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
+ }
+ return req;
+ }
+ }
- private record HttpResponse(int code, String lastModifiedHeader, String eTagHeader, CacheControl cacheControl, Instant retryAfter,
- byte[] content, String contentType, String urlAfterRedirect) {}
-
- public record HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, String urlAfterRedirect,
- Duration validFor) {}
+ private record HttpResponse(
+ int code,
+ String lastModifiedHeader,
+ String eTagHeader,
+ CacheControl cacheControl,
+ Instant retryAfter,
+ byte[] content,
+ String contentType,
+ String urlAfterRedirect) {}
+ public record HttpResult(
+ byte[] content,
+ String contentType,
+ String lastModifiedSince,
+ String eTag,
+ String urlAfterRedirect,
+ Duration validFor) {}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/Urls.java b/commafeed-server/src/main/java/com/commafeed/backend/Urls.java
index f3be86b7..635356f2 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/Urls.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/Urls.java
@@ -2,105 +2,104 @@ package com.commafeed.backend;
import java.net.URI;
import java.util.regex.Pattern;
-
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Strings;
import org.netpreserve.urlcanon.Canonicalizer;
import org.netpreserve.urlcanon.ParsedUrl;
-import lombok.experimental.UtilityClass;
-import lombok.extern.slf4j.Slf4j;
-
@UtilityClass
@Slf4j
public class Urls {
- private static final Pattern QUESTION_MARK = Pattern.compile(Pattern.quote("?"));
+ private static final Pattern QUESTION_MARK = Pattern.compile(Pattern.quote("?"));
- public static boolean isHttp(String url) {
- return url.startsWith("http://");
- }
+ public static boolean isHttp(String url) {
+ return url.startsWith("http://");
+ }
- public static boolean isHttps(String url) {
- return url.startsWith("https://");
- }
+ public static boolean isHttps(String url) {
+ return url.startsWith("https://");
+ }
- public static boolean isAbsolute(String url) {
- return isHttp(url) || isHttps(url);
- }
+ public static boolean isAbsolute(String url) {
+ return isHttp(url) || isHttps(url);
+ }
- /**
- *
- * @param relativeUrl
- * the url of the entry
- * @param feedLink
- * the url of the feed as described in the feed
- * @param feedUrl
- * the url of the feed that we used to fetch the feed
- * @return an absolute url pointing to the entry
- */
- public static String toAbsolute(String relativeUrl, String feedLink, String feedUrl) {
- String baseUrl = (feedLink != null && isAbsolute(feedLink)) ? feedLink : feedUrl;
- if (baseUrl == null) {
- return null;
- }
+ /**
+ * @param relativeUrl the url of the entry
+ * @param feedLink the url of the feed as described in the feed
+ * @param feedUrl the url of the feed that we used to fetch the feed
+ * @return an absolute url pointing to the entry
+ */
+ public static String toAbsolute(String relativeUrl, String feedLink, String feedUrl) {
+ String baseUrl = (feedLink != null && isAbsolute(feedLink)) ? feedLink : feedUrl;
+ if (baseUrl == null) {
+ return null;
+ }
- try {
- return URI.create(baseUrl).resolve(relativeUrl).toString();
- } catch (IllegalArgumentException e) {
- log.debug("Unable to create absolute url from relative url: {} base: {}", relativeUrl, baseUrl, e);
- return null;
- }
- }
+ try {
+ return URI.create(baseUrl).resolve(relativeUrl).toString();
+ } catch (IllegalArgumentException e) {
+ log.debug(
+ "Unable to create absolute url from relative url: {} base: {}",
+ relativeUrl,
+ baseUrl,
+ e);
+ return null;
+ }
+ }
- public static String removeTrailingSlash(String url) {
- if (url == null) {
- return null;
- }
+ public static String removeTrailingSlash(String url) {
+ if (url == null) {
+ return null;
+ }
- if (url.endsWith("/")) {
- url = url.substring(0, url.length() - 1);
- }
- return url;
- }
+ if (url.endsWith("/")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ return url;
+ }
- /**
- * Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and avoid duplicates
- */
- public static String normalize(String url) {
- if (url == null) {
- return null;
- }
+ /**
+ * Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to
+ * identify a feed and avoid duplicates
+ */
+ public static String normalize(String url) {
+ if (url == null) {
+ return null;
+ }
- ParsedUrl parsedUrl = ParsedUrl.parseUrl(url);
- Canonicalizer.AGGRESSIVE.canonicalize(parsedUrl);
- String normalized = parsedUrl.toString();
- if (normalized == null) {
- normalized = url;
- }
+ ParsedUrl parsedUrl = ParsedUrl.parseUrl(url);
+ Canonicalizer.AGGRESSIVE.canonicalize(parsedUrl);
+ String normalized = parsedUrl.toString();
+ if (normalized == null) {
+ normalized = url;
+ }
- // convert to lower case, the url probably won't work in some cases
- // after that but we don't care we just want to compare urls to avoid
- // duplicates
- normalized = normalized.toLowerCase();
+ // convert to lower case, the url probably won't work in some cases
+ // after that but we don't care we just want to compare urls to avoid
+ // duplicates
+ normalized = normalized.toLowerCase();
- // store all urls as http
- if (normalized.startsWith("https")) {
- normalized = "http" + normalized.substring(5);
- }
+ // store all urls as http
+ if (normalized.startsWith("https")) {
+ normalized = "http" + normalized.substring(5);
+ }
- // remove the www. part
- normalized = normalized.replace("//www.", "//");
+ // remove the www. part
+ normalized = normalized.replace("//www.", "//");
- // feedproxy redirects to feedburner
- normalized = normalized.replace("feedproxy.google.com", "feeds.feedburner.com");
+ // feedproxy redirects to feedburner
+ normalized = normalized.replace("feedproxy.google.com", "feeds.feedburner.com");
- // feedburner feeds have a special treatment
- if (QUESTION_MARK.split(normalized)[0].contains("feedburner.com")) {
- normalized = normalized.replace("feeds2.feedburner.com", "feeds.feedburner.com");
- normalized = QUESTION_MARK.split(normalized)[0];
- normalized = Strings.CS.removeEnd(normalized, "/");
- }
+ // feedburner feeds have a special treatment
+ if (QUESTION_MARK.split(normalized)[0].contains("feedburner.com")) {
+ normalized = normalized.replace("feeds2.feedburner.com", "feeds.feedburner.com");
+ normalized = QUESTION_MARK.split(normalized)[0];
+ normalized = Strings.CS.removeEnd(normalized, "/");
+ }
- return normalized;
- }
+ return normalized;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java
index 6659e776..1282faa5 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java
@@ -1,71 +1,76 @@
package com.commafeed.backend.dao;
-import java.util.List;
-import java.util.Objects;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.QFeedCategory;
import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User;
import com.querydsl.core.types.Predicate;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.util.List;
+import java.util.Objects;
@Singleton
public class FeedCategoryDAO extends GenericDAO {
- private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
+ private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
- public FeedCategoryDAO(EntityManager entityManager) {
- super(entityManager, FeedCategory.class);
- }
+ public FeedCategoryDAO(EntityManager entityManager) {
+ super(entityManager, FeedCategory.class);
+ }
- public List findAll(User user) {
- return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user)).join(CATEGORY.user, QUser.user).fetchJoin().fetch();
- }
+ public List findAll(User user) {
+ return query().selectFrom(CATEGORY)
+ .where(CATEGORY.user.eq(user))
+ .join(CATEGORY.user, QUser.user)
+ .fetchJoin()
+ .fetch();
+ }
- public FeedCategory findById(User user, Long id) {
- return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.id.eq(id)).fetchOne();
- }
+ public FeedCategory findById(User user, Long id) {
+ return query().selectFrom(CATEGORY)
+ .where(CATEGORY.user.eq(user), CATEGORY.id.eq(id))
+ .fetchOne();
+ }
- public FeedCategory findByName(User user, String name, FeedCategory parent) {
- Predicate parentPredicate;
- if (parent == null) {
- parentPredicate = CATEGORY.parent.isNull();
- } else {
- parentPredicate = CATEGORY.parent.eq(parent);
- }
- return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.name.eq(name), parentPredicate).fetchOne();
- }
+ public FeedCategory findByName(User user, String name, FeedCategory parent) {
+ Predicate parentPredicate;
+ if (parent == null) {
+ parentPredicate = CATEGORY.parent.isNull();
+ } else {
+ parentPredicate = CATEGORY.parent.eq(parent);
+ }
+ return query().selectFrom(CATEGORY)
+ .where(CATEGORY.user.eq(user), CATEGORY.name.eq(name), parentPredicate)
+ .fetchOne();
+ }
- public List findByParent(User user, FeedCategory parent) {
- Predicate parentPredicate;
- if (parent == null) {
- parentPredicate = CATEGORY.parent.isNull();
- } else {
- parentPredicate = CATEGORY.parent.eq(parent);
- }
- return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), parentPredicate).fetch();
- }
+ public List findByParent(User user, FeedCategory parent) {
+ Predicate parentPredicate;
+ if (parent == null) {
+ parentPredicate = CATEGORY.parent.isNull();
+ } else {
+ parentPredicate = CATEGORY.parent.eq(parent);
+ }
+ return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), parentPredicate).fetch();
+ }
- public List findAllChildrenCategories(User user, FeedCategory parent) {
- return findAll(user).stream().filter(c -> isChild(c, parent)).toList();
- }
-
- private boolean isChild(FeedCategory child, FeedCategory parent) {
- if (parent == null) {
- return true;
- }
- boolean isChild = false;
- while (child != null) {
- if (Objects.equals(child.getId(), parent.getId())) {
- isChild = true;
- break;
- }
- child = child.getParent();
- }
- return isChild;
- }
+ public List findAllChildrenCategories(User user, FeedCategory parent) {
+ return findAll(user).stream().filter(c -> isChild(c, parent)).toList();
+ }
+ private boolean isChild(FeedCategory child, FeedCategory parent) {
+ if (parent == null) {
+ return true;
+ }
+ boolean isChild = false;
+ while (child != null) {
+ if (Objects.equals(child.getId(), parent.getId())) {
+ isChild = true;
+ break;
+ }
+ child = child.getParent();
+ }
+ return isChild;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java
index 80c56e36..281ec8f6 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java
@@ -1,64 +1,69 @@
package com.commafeed.backend.dao;
-import java.time.Instant;
-import java.util.List;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
-import org.apache.commons.lang3.Strings;
-
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.time.Instant;
+import java.util.List;
+import org.apache.commons.lang3.Strings;
@Singleton
public class FeedDAO extends GenericDAO {
- private static final QFeed FEED = QFeed.feed;
- private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
+ private static final QFeed FEED = QFeed.feed;
+ private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
- public FeedDAO(EntityManager entityManager) {
- super(entityManager, Feed.class);
- }
+ public FeedDAO(EntityManager entityManager) {
+ super(entityManager, Feed.class);
+ }
- public List findByIds(List id) {
- return query().selectFrom(FEED).where(FEED.id.in(id)).fetch();
- }
+ public List findByIds(List id) {
+ return query().selectFrom(FEED).where(FEED.id.in(id)).fetch();
+ }
- public List findNextUpdatable(int count, Instant lastLoginThreshold) {
- JPAQuery query = query().selectFrom(FEED)
- .distinct()
- // join on subscriptions to only refresh feeds that have subscribers
- .join(SUBSCRIPTION)
- .on(SUBSCRIPTION.feed.eq(FEED))
- .where(FEED.disabledUntil.isNull().or(FEED.disabledUntil.lt(Instant.now())));
+ public List findNextUpdatable(int count, Instant lastLoginThreshold) {
+ JPAQuery query =
+ query().selectFrom(FEED)
+ .distinct()
+ // join on subscriptions to only refresh feeds that have subscribers
+ .join(SUBSCRIPTION)
+ .on(SUBSCRIPTION.feed.eq(FEED))
+ .where(
+ FEED.disabledUntil
+ .isNull()
+ .or(FEED.disabledUntil.lt(Instant.now())));
- if (lastLoginThreshold != null) {
- query.join(SUBSCRIPTION.user).where(SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold));
- }
+ if (lastLoginThreshold != null) {
+ query.join(SUBSCRIPTION.user).where(SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold));
+ }
- return query.orderBy(FEED.disabledUntil.asc()).limit(count).fetch();
- }
+ return query.orderBy(FEED.disabledUntil.asc()).limit(count).fetch();
+ }
- public void setDisabledUntil(List feedIds, Instant date) {
- updateQuery(FEED).set(FEED.disabledUntil, date).where(FEED.id.in(feedIds)).execute();
- }
+ public void setDisabledUntil(List feedIds, Instant date) {
+ updateQuery(FEED).set(FEED.disabledUntil, date).where(FEED.id.in(feedIds)).execute();
+ }
- public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
- return query().selectFrom(FEED)
- .where(FEED.normalizedUrlHash.eq(normalizedUrlHash))
- .fetch()
- .stream()
- .filter(f -> Strings.CS.equals(normalizedUrl, f.getNormalizedUrl()))
- .findFirst()
- .orElse(null);
- }
+ public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
+ return query()
+ .selectFrom(FEED)
+ .where(FEED.normalizedUrlHash.eq(normalizedUrlHash))
+ .fetch()
+ .stream()
+ .filter(f -> Strings.CS.equals(normalizedUrl, f.getNormalizedUrl()))
+ .findFirst()
+ .orElse(null);
+ }
- public List findWithoutSubscriptions(int max) {
- QFeedSubscription sub = QFeedSubscription.feedSubscription;
- return query().selectFrom(FEED).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(FEED)).notExists()).limit(max).fetch();
- }
+ public List findWithoutSubscriptions(int max) {
+ QFeedSubscription sub = QFeedSubscription.feedSubscription;
+ return query().selectFrom(FEED)
+ .where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(FEED)).notExists())
+ .limit(max)
+ .fetch();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java
index 863e7226..46d77147 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java
@@ -1,36 +1,38 @@
package com.commafeed.backend.dao;
-import java.util.List;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.util.List;
@Singleton
public class FeedEntryContentDAO extends GenericDAO {
- private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
- private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
+ private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
+ private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
- public FeedEntryContentDAO(EntityManager entityManager) {
- super(entityManager, FeedEntryContent.class);
- }
+ public FeedEntryContentDAO(EntityManager entityManager) {
+ super(entityManager, FeedEntryContent.class);
+ }
- public List findExisting(String contentHash, String titleHash) {
- return query().select(CONTENT).from(CONTENT).where(CONTENT.contentHash.eq(contentHash), CONTENT.titleHash.eq(titleHash)).fetch();
- }
+ public List findExisting(String contentHash, String titleHash) {
+ return query().select(CONTENT)
+ .from(CONTENT)
+ .where(CONTENT.contentHash.eq(contentHash), CONTENT.titleHash.eq(titleHash))
+ .fetch();
+ }
- public long deleteWithoutEntries(int max) {
- List ids = query().select(CONTENT.id)
- .from(CONTENT)
- .leftJoin(ENTRY)
- .on(ENTRY.content.id.eq(CONTENT.id))
- .where(ENTRY.id.isNull())
- .limit(max)
- .fetch();
- return deleteQuery(CONTENT).where(CONTENT.id.in(ids)).execute();
- }
+ public long deleteWithoutEntries(int max) {
+ List ids =
+ query().select(CONTENT.id)
+ .from(CONTENT)
+ .leftJoin(ENTRY)
+ .on(ENTRY.content.id.eq(CONTENT.id))
+ .where(ENTRY.id.isNull())
+ .limit(max)
+ .fetch();
+ return deleteQuery(CONTENT).where(CONTENT.id.in(ids)).execute();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java
index a9bf5841..e1b18758 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java
@@ -1,14 +1,5 @@
package com.commafeed.backend.dao;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeedEntry;
@@ -16,83 +7,98 @@ import com.google.common.collect.Lists;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQuery;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
@Singleton
public class FeedEntryDAO extends GenericDAO {
- private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
- private static final int IN_CLAUSE_BATCH_SIZE = 1000;
+ private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
+ private static final int IN_CLAUSE_BATCH_SIZE = 1000;
- public FeedEntryDAO(EntityManager entityManager) {
- super(entityManager, FeedEntry.class);
- }
+ public FeedEntryDAO(EntityManager entityManager) {
+ super(entityManager, FeedEntry.class);
+ }
- public FeedEntry findExisting(String guidHash, Feed feed) {
- return query().select(ENTRY).from(ENTRY).where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed)).limit(1).fetchOne();
- }
+ public FeedEntry findExisting(String guidHash, Feed feed) {
+ return query().select(ENTRY)
+ .from(ENTRY)
+ .where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed))
+ .limit(1)
+ .fetchOne();
+ }
- public Set findExistingGuidHashes(Set guidHashes, Feed feed) {
- if (guidHashes.isEmpty()) {
- return Set.of();
- }
+ public Set findExistingGuidHashes(Set guidHashes, Feed feed) {
+ if (guidHashes.isEmpty()) {
+ return Set.of();
+ }
- Set result = new HashSet<>();
- for (List batch : Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) {
- result.addAll(query().select(ENTRY.guidHash).from(ENTRY).where(ENTRY.feed.eq(feed), ENTRY.guidHash.in(batch)).fetch());
- }
- return result;
- }
+ Set result = new HashSet<>();
+ for (List batch :
+ Lists.partition(new ArrayList<>(guidHashes), IN_CLAUSE_BATCH_SIZE)) {
+ result.addAll(
+ query().select(ENTRY.guidHash)
+ .from(ENTRY)
+ .where(ENTRY.feed.eq(feed), ENTRY.guidHash.in(batch))
+ .fetch());
+ }
+ return result;
+ }
- public List findFeedsExceedingCapacity(long maxCapacity, long max, boolean keepStarredEntries) {
- NumberExpression count = ENTRY.id.count();
- JPAQuery query = query().select(ENTRY.feed.id, count).from(ENTRY);
+ public List findFeedsExceedingCapacity(
+ long maxCapacity, long max, boolean keepStarredEntries) {
+ NumberExpression count = ENTRY.id.count();
+ JPAQuery query = query().select(ENTRY.feed.id, count).from(ENTRY);
- if (keepStarredEntries) {
- query.where(Predicates.isNotStarred(ENTRY));
- }
+ if (keepStarredEntries) {
+ query.where(Predicates.isNotStarred(ENTRY));
+ }
- return query.groupBy(ENTRY.feed)
- .having(count.gt(maxCapacity))
- .limit(max)
- .fetch()
- .stream()
- .map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count)))
- .toList();
- }
+ return query.groupBy(ENTRY.feed).having(count.gt(maxCapacity)).limit(max).fetch().stream()
+ .map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count)))
+ .toList();
+ }
- public int delete(Long feedId, long max) {
- List list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
- return delete(list);
- }
+ public int delete(Long feedId, long max) {
+ List list =
+ query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
+ return delete(list);
+ }
- /**
- * Delete entries older than a certain date
- */
- public int deleteEntriesOlderThan(Instant olderThan, long max, boolean keepStarredEntries) {
- JPAQuery query = query().selectFrom(ENTRY)
- .where(ENTRY.published.lt(olderThan))
- .orderBy(ENTRY.published.asc())
- .limit(max);
+ /** Delete entries older than a certain date */
+ public int deleteEntriesOlderThan(Instant olderThan, long max, boolean keepStarredEntries) {
+ JPAQuery query =
+ query().selectFrom(ENTRY)
+ .where(ENTRY.published.lt(olderThan))
+ .orderBy(ENTRY.published.asc())
+ .limit(max);
- if (keepStarredEntries) {
- query.where(Predicates.isNotStarred(ENTRY));
- }
+ if (keepStarredEntries) {
+ query.where(Predicates.isNotStarred(ENTRY));
+ }
- return delete(query.fetch());
- }
+ return delete(query.fetch());
+ }
- /**
- * Delete the oldest entries of a feed
- */
- public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) {
- JPAQuery query = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max);
+ /** Delete the oldest entries of a feed */
+ public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) {
+ JPAQuery query =
+ query().selectFrom(ENTRY)
+ .where(ENTRY.feed.id.eq(feedId))
+ .orderBy(ENTRY.published.asc())
+ .limit(max);
- if (keepStarredEntries) {
- query.where(Predicates.isNotStarred(ENTRY));
- }
+ if (keepStarredEntries) {
+ query.where(Predicates.isNotStarred(ENTRY));
+ }
- return delete(query.fetch());
- }
+ return delete(query.fetch());
+ }
- public record FeedCapacity(Long id, Long capacity) {}
+ public record FeedCapacity(Long id, Long capacity) {}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java
index 6a816e93..1bc97c31 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java
@@ -1,16 +1,5 @@
package com.commafeed.backend.dao;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
-import org.apache.commons.collections4.CollectionUtils;
-
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
@@ -32,273 +21,316 @@ import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQuery;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.commons.collections4.CollectionUtils;
@Singleton
public class FeedEntryStatusDAO extends GenericDAO {
- private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
- private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
- private static final QFeed FEED = QFeed.feed;
- private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
- private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
- private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
+ private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
+ private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
+ private static final QFeed FEED = QFeed.feed;
+ private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
+ private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
+ private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
- private final FeedEntryTagDAO feedEntryTagDAO;
- private final CommaFeedConfiguration config;
+ private final FeedEntryTagDAO feedEntryTagDAO;
+ private final CommaFeedConfiguration config;
- public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
- super(entityManager, FeedEntryStatus.class);
- this.feedEntryTagDAO = feedEntryTagDAO;
- this.config = config;
- }
+ public FeedEntryStatusDAO(
+ EntityManager entityManager,
+ FeedEntryTagDAO feedEntryTagDAO,
+ CommaFeedConfiguration config) {
+ super(entityManager, FeedEntryStatus.class);
+ this.feedEntryTagDAO = feedEntryTagDAO;
+ this.config = config;
+ }
- public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
- List statuses = query().selectFrom(STATUS).where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub)).fetch();
- FeedEntryStatus status = statuses.stream().findFirst().orElse(null);
- return handleStatus(user, status, sub, entry);
- }
+ public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
+ List statuses =
+ query().selectFrom(STATUS)
+ .where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub))
+ .fetch();
+ FeedEntryStatus status = statuses.stream().findFirst().orElse(null);
+ return handleStatus(user, status, sub, entry);
+ }
- /**
- * creates an artificial "unread" status if status is null
- */
- private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
- if (status == null) {
- Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
- boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold);
- status = new FeedEntryStatus(user, sub, entry);
- status.setRead(read);
- status.setMarkable(!read);
- } else {
- status.setMarkable(true);
- }
- return status;
- }
+ /** creates an artificial "unread" status if status is null */
+ private FeedEntryStatus handleStatus(
+ User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
+ if (status == null) {
+ Instant statusesInstantThreshold =
+ config.database().cleanup().statusesInstantThreshold();
+ boolean read =
+ statusesInstantThreshold != null
+ && entry.getPublished().isBefore(statusesInstantThreshold);
+ status = new FeedEntryStatus(user, sub, entry);
+ status.setRead(read);
+ status.setMarkable(!read);
+ } else {
+ status.setMarkable(true);
+ }
+ return status;
+ }
- private void fetchTags(User user, List statuses) {
- Map> tagsByEntryIds = feedEntryTagDAO.findByEntries(user,
- statuses.stream().map(FeedEntryStatus::getEntry).toList());
- for (FeedEntryStatus status : statuses) {
- List tags = tagsByEntryIds.get(status.getEntry().getId());
- status.setTags(tags == null ? List.of() : tags);
- }
- }
+ private void fetchTags(User user, List statuses) {
+ Map> tagsByEntryIds =
+ feedEntryTagDAO.findByEntries(
+ user, statuses.stream().map(FeedEntryStatus::getEntry).toList());
+ for (FeedEntryStatus status : statuses) {
+ List tags = tagsByEntryIds.get(status.getEntry().getId());
+ status.setTags(tags == null ? List.of() : tags);
+ }
+ }
- public List findStarred(User user, List keywords, Instant newerThan, int offset, int limit,
- ReadingOrder order, boolean includeContent) {
- JPAQuery query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
- if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
- query.join(STATUS.entry).fetchJoin();
- query.join(STATUS.entry.content, CONTENT).fetchJoin();
- }
+ public List findStarred(
+ User user,
+ List keywords,
+ Instant newerThan,
+ int offset,
+ int limit,
+ ReadingOrder order,
+ boolean includeContent) {
+ JPAQuery query =
+ query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
+ if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
+ query.join(STATUS.entry).fetchJoin();
+ query.join(STATUS.entry.content, CONTENT).fetchJoin();
+ }
- if (CollectionUtils.isNotEmpty(keywords)) {
- applyKeywordsFilter(query, keywords);
- }
+ if (CollectionUtils.isNotEmpty(keywords)) {
+ applyKeywordsFilter(query, keywords);
+ }
- if (newerThan != null) {
- query.where(STATUS.entryInserted.gt(newerThan));
- }
+ if (newerThan != null) {
+ query.where(STATUS.entryInserted.gt(newerThan));
+ }
- if (order == ReadingOrder.ASC) {
- query.orderBy(STATUS.entryPublished.asc(), STATUS.id.asc());
- } else {
- query.orderBy(STATUS.entryPublished.desc(), STATUS.id.desc());
- }
+ if (order == ReadingOrder.ASC) {
+ query.orderBy(STATUS.entryPublished.asc(), STATUS.id.asc());
+ } else {
+ query.orderBy(STATUS.entryPublished.desc(), STATUS.id.desc());
+ }
- if (offset > -1) {
- query.offset(offset);
- }
+ if (offset > -1) {
+ query.offset(offset);
+ }
- if (limit > -1) {
- query.limit(limit);
- }
+ if (limit > -1) {
+ query.limit(limit);
+ }
- setTimeout(query, config.database().queryTimeout());
+ setTimeout(query, config.database().queryTimeout());
- List statuses = query.fetch();
- statuses.forEach(s -> s.setMarkable(true));
- if (includeContent) {
- fetchTags(user, statuses);
- }
+ List statuses = query.fetch();
+ statuses.forEach(s -> s.setMarkable(true));
+ if (includeContent) {
+ fetchTags(user, statuses);
+ }
- return statuses;
- }
+ return statuses;
+ }
- public List findBySubscriptions(User user, List subs, boolean unreadOnly,
- List keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
- String tag, Long minEntryId, Long maxEntryId) {
- Map> subsByFeedId = subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId()));
+ public List findBySubscriptions(
+ User user,
+ List subs,
+ boolean unreadOnly,
+ List keywords,
+ Instant newerThan,
+ int offset,
+ int limit,
+ ReadingOrder order,
+ boolean includeContent,
+ String tag,
+ Long minEntryId,
+ Long maxEntryId) {
+ Map> subsByFeedId =
+ subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId()));
- JPAQuery query = query().select(ENTRY, STATUS).from(ENTRY);
- query.leftJoin(ENTRY.statuses, STATUS).on(STATUS.subscription.in(subs));
- query.where(ENTRY.feed.id.in(subsByFeedId.keySet()));
+ JPAQuery query = query().select(ENTRY, STATUS).from(ENTRY);
+ query.leftJoin(ENTRY.statuses, STATUS).on(STATUS.subscription.in(subs));
+ query.where(ENTRY.feed.id.in(subsByFeedId.keySet()));
- if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
- query.join(ENTRY.content, CONTENT).fetchJoin();
- }
+ if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
+ query.join(ENTRY.content, CONTENT).fetchJoin();
+ }
- if (CollectionUtils.isNotEmpty(keywords)) {
- applyKeywordsFilter(query, keywords);
- }
+ if (CollectionUtils.isNotEmpty(keywords)) {
+ applyKeywordsFilter(query, keywords);
+ }
- if (unreadOnly && tag == null) {
- query.where(buildUnreadPredicate());
- }
+ if (unreadOnly && tag == null) {
+ query.where(buildUnreadPredicate());
+ }
- if (tag != null) {
- BooleanBuilder and = new BooleanBuilder();
- and.and(TAG.user.id.eq(user.getId()));
- and.and(TAG.name.eq(tag));
- query.join(ENTRY.tags, TAG).on(and);
- }
+ if (tag != null) {
+ BooleanBuilder and = new BooleanBuilder();
+ and.and(TAG.user.id.eq(user.getId()));
+ and.and(TAG.name.eq(tag));
+ query.join(ENTRY.tags, TAG).on(and);
+ }
- if (newerThan != null) {
- query.where(ENTRY.inserted.goe(newerThan));
- }
+ if (newerThan != null) {
+ query.where(ENTRY.inserted.goe(newerThan));
+ }
- if (minEntryId != null) {
- query.where(ENTRY.id.gt(minEntryId));
- }
+ if (minEntryId != null) {
+ query.where(ENTRY.id.gt(minEntryId));
+ }
- if (maxEntryId != null) {
- query.where(ENTRY.id.lt(maxEntryId));
- }
+ if (maxEntryId != null) {
+ query.where(ENTRY.id.lt(maxEntryId));
+ }
- if (order != null) {
- if (order == ReadingOrder.ASC) {
- query.orderBy(ENTRY.published.asc(), ENTRY.id.asc());
- } else {
- query.orderBy(ENTRY.published.desc(), ENTRY.id.desc());
- }
- }
+ if (order != null) {
+ if (order == ReadingOrder.ASC) {
+ query.orderBy(ENTRY.published.asc(), ENTRY.id.asc());
+ } else {
+ query.orderBy(ENTRY.published.desc(), ENTRY.id.desc());
+ }
+ }
- if (offset > -1) {
- query.offset(offset);
- }
+ if (offset > -1) {
+ query.offset(offset);
+ }
- if (limit > -1) {
- query.limit(limit);
- }
+ if (limit > -1) {
+ query.limit(limit);
+ }
- setTimeout(query, config.database().queryTimeout());
+ setTimeout(query, config.database().queryTimeout());
- List statuses = new ArrayList<>();
- List tuples = query.fetch();
- for (Tuple tuple : tuples) {
- FeedEntry e = tuple.get(ENTRY);
- FeedEntryStatus s = tuple.get(STATUS);
- for (FeedSubscription sub : subsByFeedId.get(e.getFeed().getId())) {
- statuses.add(handleStatus(user, s, sub, e));
- }
- }
+ List statuses = new ArrayList<>();
+ List tuples = query.fetch();
+ for (Tuple tuple : tuples) {
+ FeedEntry e = tuple.get(ENTRY);
+ FeedEntryStatus s = tuple.get(STATUS);
+ for (FeedSubscription sub : subsByFeedId.get(e.getFeed().getId())) {
+ statuses.add(handleStatus(user, s, sub, e));
+ }
+ }
- if (includeContent) {
- fetchTags(user, statuses);
- }
+ if (includeContent) {
+ fetchTags(user, statuses);
+ }
- return statuses;
- }
+ return statuses;
+ }
- private void applyKeywordsFilter(JPAQuery> query, List keywords) {
- for (FeedEntryKeyword keyword : keywords) {
- BooleanBuilder or = new BooleanBuilder();
- or.or(CONTENT.content.containsIgnoreCase(keyword.keyword()));
- or.or(CONTENT.title.containsIgnoreCase(keyword.keyword()));
- if (keyword.mode() == Mode.EXCLUDE) {
- or.not();
- }
- query.where(or);
- }
- }
+ private void applyKeywordsFilter(JPAQuery> query, List keywords) {
+ for (FeedEntryKeyword keyword : keywords) {
+ BooleanBuilder or = new BooleanBuilder();
+ or.or(CONTENT.content.containsIgnoreCase(keyword.keyword()));
+ or.or(CONTENT.title.containsIgnoreCase(keyword.keyword()));
+ if (keyword.mode() == Mode.EXCLUDE) {
+ or.not();
+ }
+ query.where(or);
+ }
+ }
- public UnreadCount getUnreadCount(FeedSubscription sub) {
- JPAQuery query = query().select(ENTRY.count(), ENTRY.published.max())
- .from(ENTRY)
- .leftJoin(ENTRY.statuses, STATUS)
- .on(STATUS.subscription.eq(sub))
- .where(ENTRY.feed.eq(sub.getFeed()))
- .where(buildUnreadPredicate());
+ public UnreadCount getUnreadCount(FeedSubscription sub) {
+ JPAQuery query =
+ query().select(ENTRY.count(), ENTRY.published.max())
+ .from(ENTRY)
+ .leftJoin(ENTRY.statuses, STATUS)
+ .on(STATUS.subscription.eq(sub))
+ .where(ENTRY.feed.eq(sub.getFeed()))
+ .where(buildUnreadPredicate());
- Tuple tuple = query.fetchOne();
- Long count = tuple.get(ENTRY.count());
- Instant published = tuple.get(ENTRY.published.max());
- return new UnreadCount(sub.getId(), count == null ? 0 : count, published);
- }
+ Tuple tuple = query.fetchOne();
+ Long count = tuple.get(ENTRY.count());
+ Instant published = tuple.get(ENTRY.published.max());
+ return new UnreadCount(sub.getId(), count == null ? 0 : count, published);
+ }
- private BooleanBuilder buildUnreadPredicate() {
- BooleanBuilder or = new BooleanBuilder();
- or.or(STATUS.read.isNull());
- or.or(STATUS.read.isFalse());
+ private BooleanBuilder buildUnreadPredicate() {
+ BooleanBuilder or = new BooleanBuilder();
+ or.or(STATUS.read.isNull());
+ or.or(STATUS.read.isFalse());
- Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
- if (statusesInstantThreshold != null) {
- return or.and(ENTRY.published.goe(statusesInstantThreshold));
- } else {
- return or;
- }
- }
+ Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
+ if (statusesInstantThreshold != null) {
+ return or.and(ENTRY.published.goe(statusesInstantThreshold));
+ } else {
+ return or;
+ }
+ }
- public long deleteOldStatuses(Instant olderThan, int limit) {
- List ids = query().select(STATUS.id)
- .from(STATUS)
- .where(STATUS.entryInserted.lt(olderThan), STATUS.starred.isFalse())
- .limit(limit)
- .fetch();
- return deleteQuery(STATUS).where(STATUS.id.in(ids)).execute();
- }
+ public long deleteOldStatuses(Instant olderThan, int limit) {
+ List ids =
+ query().select(STATUS.id)
+ .from(STATUS)
+ .where(STATUS.entryInserted.lt(olderThan), STATUS.starred.isFalse())
+ .limit(limit)
+ .fetch();
+ return deleteQuery(STATUS).where(STATUS.id.in(ids)).execute();
+ }
- public long autoMarkAsRead(int limit) {
- Instant now = Instant.now();
+ public long autoMarkAsRead(int limit) {
+ Instant now = Instant.now();
- BooleanBuilder where = new BooleanBuilder();
- where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.isNotNull());
- where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.gt(0));
+ BooleanBuilder where = new BooleanBuilder();
+ where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.isNotNull());
+ where.and(SUBSCRIPTION.autoMarkAsReadAfterDays.gt(0));
- NumberExpression daysDiff = Expressions.numberTemplate(Integer.class, "TIMESTAMPDIFF(DAY, {0}, {1})", ENTRY.published,
- now);
- where.and(daysDiff.goe(SUBSCRIPTION.autoMarkAsReadAfterDays));
+ NumberExpression daysDiff =
+ Expressions.numberTemplate(
+ Integer.class, "TIMESTAMPDIFF(DAY, {0}, {1})", ENTRY.published, now);
+ where.and(daysDiff.goe(SUBSCRIPTION.autoMarkAsReadAfterDays));
- where.and(buildUnreadPredicate());
+ where.and(buildUnreadPredicate());
- List tuples = query().select(ENTRY, STATUS, SUBSCRIPTION)
- .from(ENTRY)
- .join(ENTRY.feed, FEED)
- .join(SUBSCRIPTION)
- .on(SUBSCRIPTION.feed.eq(FEED))
- .leftJoin(ENTRY.statuses, STATUS)
- .on(STATUS.subscription.eq(SUBSCRIPTION))
- .where(where)
- .limit(limit)
- .fetch();
+ List tuples =
+ query().select(ENTRY, STATUS, SUBSCRIPTION)
+ .from(ENTRY)
+ .join(ENTRY.feed, FEED)
+ .join(SUBSCRIPTION)
+ .on(SUBSCRIPTION.feed.eq(FEED))
+ .leftJoin(ENTRY.statuses, STATUS)
+ .on(STATUS.subscription.eq(SUBSCRIPTION))
+ .where(where)
+ .limit(limit)
+ .fetch();
- long updated = 0;
+ long updated = 0;
- // Update existing statuses
- List statusIdsToUpdate = tuples.stream()
- .map(t -> t.get(STATUS))
- .filter(s -> s != null && s.getId() != null)
- .map(FeedEntryStatus::getId)
- .distinct()
- .toList();
+ // Update existing statuses
+ List statusIdsToUpdate =
+ tuples.stream()
+ .map(t -> t.get(STATUS))
+ .filter(s -> s != null && s.getId() != null)
+ .map(FeedEntryStatus::getId)
+ .distinct()
+ .toList();
- if (!statusIdsToUpdate.isEmpty()) {
- updated += updateQuery(STATUS).where(STATUS.id.in(statusIdsToUpdate)).set(STATUS.read, true).execute();
- }
+ if (!statusIdsToUpdate.isEmpty()) {
+ updated +=
+ updateQuery(STATUS)
+ .where(STATUS.id.in(statusIdsToUpdate))
+ .set(STATUS.read, true)
+ .execute();
+ }
- // Insert new statuses for entries without existing status
- for (Tuple tuple : tuples) {
- FeedEntryStatus status = tuple.get(STATUS);
- if (status == null || status.getId() == null) {
- FeedEntry entry = tuple.get(ENTRY);
- FeedSubscription sub = tuple.get(SUBSCRIPTION);
- FeedEntryStatus newStatus = new FeedEntryStatus(sub.getUser(), sub, entry);
- newStatus.setRead(true);
- persist(newStatus);
- updated++;
- }
- }
-
- return updated;
- }
+ // Insert new statuses for entries without existing status
+ for (Tuple tuple : tuples) {
+ FeedEntryStatus status = tuple.get(STATUS);
+ if (status == null || status.getId() == null) {
+ FeedEntry entry = tuple.get(ENTRY);
+ FeedSubscription sub = tuple.get(SUBSCRIPTION);
+ FeedEntryStatus newStatus = new FeedEntryStatus(sub.getUser(), sub, entry);
+ newStatus.setRead(true);
+ persist(newStatus);
+ updated++;
+ }
+ }
+ return updated;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
index 07d90af2..3afd1ad3 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
@@ -1,39 +1,38 @@
package com.commafeed.backend.dao;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.QFeedEntryTag;
import com.commafeed.backend.model.User;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
@Singleton
public class FeedEntryTagDAO extends GenericDAO {
- private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
+ private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
- public FeedEntryTagDAO(EntityManager entityManager) {
- super(entityManager, FeedEntryTag.class);
- }
+ public FeedEntryTagDAO(EntityManager entityManager) {
+ super(entityManager, FeedEntryTag.class);
+ }
- public List findByUser(User user) {
- return query().selectDistinct(TAG.name).from(TAG).where(TAG.user.eq(user)).fetch();
- }
+ public List findByUser(User user) {
+ return query().selectDistinct(TAG.name).from(TAG).where(TAG.user.eq(user)).fetch();
+ }
- public List findByEntry(User user, FeedEntry entry) {
- return query().selectFrom(TAG).where(TAG.user.eq(user), TAG.entry.eq(entry)).fetch();
- }
+ public List findByEntry(User user, FeedEntry entry) {
+ return query().selectFrom(TAG).where(TAG.user.eq(user), TAG.entry.eq(entry)).fetch();
+ }
- public Map> findByEntries(User user, List entries) {
- return query().selectFrom(TAG)
- .where(TAG.user.eq(user), TAG.entry.in(entries))
- .fetch()
- .stream()
- .collect(Collectors.groupingBy(t -> t.getEntry().getId()));
- }
+ public Map> findByEntries(User user, List entries) {
+ return query()
+ .selectFrom(TAG)
+ .where(TAG.user.eq(user), TAG.entry.in(entries))
+ .fetch()
+ .stream()
+ .collect(Collectors.groupingBy(t -> t.getEntry().getId()));
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java
index 0a063eb1..22c1e838 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java
@@ -1,20 +1,5 @@
package com.commafeed.backend.dao;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
-import org.hibernate.engine.spi.SharedSessionContractImplementor;
-import org.hibernate.event.service.spi.EventListenerRegistry;
-import org.hibernate.event.spi.EventType;
-import org.hibernate.event.spi.PostCommitInsertEventListener;
-import org.hibernate.event.spi.PostInsertEvent;
-import org.hibernate.persister.entity.EntityPersister;
-
import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
@@ -23,109 +8,136 @@ import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.User;
import com.querydsl.jpa.JPQLQuery;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.event.service.spi.EventListenerRegistry;
+import org.hibernate.event.spi.EventType;
+import org.hibernate.event.spi.PostCommitInsertEventListener;
+import org.hibernate.event.spi.PostInsertEvent;
+import org.hibernate.persister.entity.EntityPersister;
@Singleton
public class FeedSubscriptionDAO extends GenericDAO {
- private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
+ private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
- private final EntityManager entityManager;
+ private final EntityManager entityManager;
- public FeedSubscriptionDAO(EntityManager entityManager) {
- super(entityManager, FeedSubscription.class);
- this.entityManager = entityManager;
- }
+ public FeedSubscriptionDAO(EntityManager entityManager) {
+ super(entityManager, FeedSubscription.class);
+ this.entityManager = entityManager;
+ }
- public void onPostCommitInsert(Consumer consumer) {
- entityManager.unwrap(SharedSessionContractImplementor.class)
- .getFactory()
- .getServiceRegistry()
- .getService(EventListenerRegistry.class)
- .getEventListenerGroup(EventType.POST_COMMIT_INSERT)
- .appendListener(new PostCommitInsertEventListener() {
- @Override
- public void onPostInsert(PostInsertEvent event) {
- if (event.getEntity() instanceof FeedSubscription s) {
- consumer.accept(s);
- }
- }
+ public void onPostCommitInsert(Consumer consumer) {
+ entityManager
+ .unwrap(SharedSessionContractImplementor.class)
+ .getFactory()
+ .getServiceRegistry()
+ .getService(EventListenerRegistry.class)
+ .getEventListenerGroup(EventType.POST_COMMIT_INSERT)
+ .appendListener(
+ new PostCommitInsertEventListener() {
+ @Override
+ public void onPostInsert(PostInsertEvent event) {
+ if (event.getEntity() instanceof FeedSubscription s) {
+ consumer.accept(s);
+ }
+ }
- @Override
- public boolean requiresPostCommitHandling(EntityPersister persister) {
- return true;
- }
+ @Override
+ public boolean requiresPostCommitHandling(EntityPersister persister) {
+ return true;
+ }
- @Override
- public void onPostInsertCommitFailed(PostInsertEvent event) {
- // do nothing
- }
- });
- }
+ @Override
+ public void onPostInsertCommitFailed(PostInsertEvent event) {
+ // do nothing
+ }
+ });
+ }
- public FeedSubscription findById(User user, Long id) {
- List subs = query().selectFrom(SUBSCRIPTION)
- .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.id.eq(id))
- .leftJoin(SUBSCRIPTION.feed)
- .fetchJoin()
- .leftJoin(SUBSCRIPTION.category)
- .fetchJoin()
- .fetch();
- FeedSubscription sub = subs.stream().findFirst().orElse(null);
- return initRelations(sub);
- }
+ public FeedSubscription findById(User user, Long id) {
+ List subs =
+ query().selectFrom(SUBSCRIPTION)
+ .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.id.eq(id))
+ .leftJoin(SUBSCRIPTION.feed)
+ .fetchJoin()
+ .leftJoin(SUBSCRIPTION.category)
+ .fetchJoin()
+ .fetch();
+ FeedSubscription sub = subs.stream().findFirst().orElse(null);
+ return initRelations(sub);
+ }
- public List findByFeed(Feed feed) {
- return query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.feed.eq(feed)).fetch();
- }
+ public List findByFeed(Feed feed) {
+ return query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.feed.eq(feed)).fetch();
+ }
- public FeedSubscription findByFeed(User user, Feed feed) {
- List subs = query().selectFrom(SUBSCRIPTION)
- .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed))
- .fetch();
- FeedSubscription sub = subs.stream().findFirst().orElse(null);
- return initRelations(sub);
- }
+ public FeedSubscription findByFeed(User user, Feed feed) {
+ List subs =
+ query().selectFrom(SUBSCRIPTION)
+ .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed))
+ .fetch();
+ FeedSubscription sub = subs.stream().findFirst().orElse(null);
+ return initRelations(sub);
+ }
- public List findAll(User user) {
- List subs = query().selectFrom(SUBSCRIPTION)
- .where(SUBSCRIPTION.user.eq(user))
- .leftJoin(SUBSCRIPTION.feed)
- .fetchJoin()
- .leftJoin(SUBSCRIPTION.category)
- .fetchJoin()
- .fetch();
- return initRelations(subs);
- }
+ public List findAll(User user) {
+ List subs =
+ query().selectFrom(SUBSCRIPTION)
+ .where(SUBSCRIPTION.user.eq(user))
+ .leftJoin(SUBSCRIPTION.feed)
+ .fetchJoin()
+ .leftJoin(SUBSCRIPTION.category)
+ .fetchJoin()
+ .fetch();
+ return initRelations(subs);
+ }
- public Long count(User user) {
- return query().select(SUBSCRIPTION.count()).from(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user)).fetchOne();
- }
+ public Long count(User user) {
+ return query().select(SUBSCRIPTION.count())
+ .from(SUBSCRIPTION)
+ .where(SUBSCRIPTION.user.eq(user))
+ .fetchOne();
+ }
- public List findByCategory(User user, FeedCategory category) {
- JPQLQuery query = query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
- if (category == null) {
- query.where(SUBSCRIPTION.category.isNull());
- } else {
- query.where(SUBSCRIPTION.category.eq(category));
- }
- return initRelations(query.fetch());
- }
+ public List findByCategory(User user, FeedCategory category) {
+ JPQLQuery query =
+ query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
+ if (category == null) {
+ query.where(SUBSCRIPTION.category.isNull());
+ } else {
+ query.where(SUBSCRIPTION.category.eq(category));
+ }
+ return initRelations(query.fetch());
+ }
- public List findByCategories(User user, List categories) {
- Set categoryIds = categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
- return findAll(user).stream().filter(s -> s.getCategory() != null && categoryIds.contains(s.getCategory().getId())).toList();
- }
+ public List findByCategories(User user, List categories) {
+ Set categoryIds =
+ categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
+ return findAll(user).stream()
+ .filter(
+ s ->
+ s.getCategory() != null
+ && categoryIds.contains(s.getCategory().getId()))
+ .toList();
+ }
- private List initRelations(List list) {
- list.forEach(this::initRelations);
- return list;
- }
+ private List initRelations(List list) {
+ list.forEach(this::initRelations);
+ return list;
+ }
- private FeedSubscription initRelations(FeedSubscription sub) {
- if (sub != null) {
- Models.initialize(sub.getFeed());
- Models.initialize(sub.getCategory());
- }
- return sub;
- }
+ private FeedSubscription initRelations(FeedSubscription sub) {
+ if (sub != null) {
+ Models.initialize(sub.getFeed());
+ Models.initialize(sub.getCategory());
+ }
+ return sub;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java
index c602e591..ea275701 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java
@@ -1,66 +1,61 @@
package com.commafeed.backend.dao;
-import java.time.Duration;
-import java.util.Collection;
-
-import jakarta.persistence.EntityManager;
-
-import org.hibernate.jpa.SpecHints;
-
import com.commafeed.backend.model.AbstractModel;
import com.querydsl.core.types.EntityPath;
import com.querydsl.jpa.impl.JPADeleteClause;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.impl.JPAUpdateClause;
-
+import jakarta.persistence.EntityManager;
+import java.time.Duration;
+import java.util.Collection;
import lombok.RequiredArgsConstructor;
+import org.hibernate.jpa.SpecHints;
@RequiredArgsConstructor
public abstract class GenericDAO {
- private final EntityManager entityManager;
- private final Class entityClass;
+ private final EntityManager entityManager;
+ private final Class entityClass;
- protected JPAQueryFactory query() {
- return new JPAQueryFactory(entityManager);
- }
+ protected JPAQueryFactory query() {
+ return new JPAQueryFactory(entityManager);
+ }
- protected JPAUpdateClause updateQuery(EntityPath entityPath) {
- return new JPAUpdateClause(entityManager, entityPath);
- }
+ protected JPAUpdateClause updateQuery(EntityPath entityPath) {
+ return new JPAUpdateClause(entityManager, entityPath);
+ }
- protected JPADeleteClause deleteQuery(EntityPath entityPath) {
- return new JPADeleteClause(entityManager, entityPath);
- }
+ protected JPADeleteClause deleteQuery(EntityPath entityPath) {
+ return new JPADeleteClause(entityManager, entityPath);
+ }
- public void persist(T model) {
- entityManager.persist(model);
- }
+ public void persist(T model) {
+ entityManager.persist(model);
+ }
- public T merge(T model) {
- return entityManager.merge(model);
- }
+ public T merge(T model) {
+ return entityManager.merge(model);
+ }
- public T findById(Long id) {
- return entityManager.find(entityClass, id);
- }
+ public T findById(Long id) {
+ return entityManager.find(entityClass, id);
+ }
- public void delete(T object) {
- if (object != null) {
- entityManager.remove(object);
- }
- }
+ public void delete(T object) {
+ if (object != null) {
+ entityManager.remove(object);
+ }
+ }
- public int delete(Collection objects) {
- objects.forEach(this::delete);
- return objects.size();
- }
-
- protected void setTimeout(JPAQuery> query, Duration timeout) {
- if (!timeout.isZero()) {
- query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis()));
- }
- }
+ public int delete(Collection objects) {
+ objects.forEach(this::delete);
+ return objects.size();
+ }
+ protected void setTimeout(JPAQuery> query, Duration timeout) {
+ if (!timeout.isZero()) {
+ query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis()));
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/Predicates.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/Predicates.java
index 817dd3de..1b482eb5 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/Predicates.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/Predicates.java
@@ -4,15 +4,17 @@ import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryStatus;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
-
import lombok.experimental.UtilityClass;
@UtilityClass
public class Predicates {
- private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
+ private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
- public static BooleanExpression isNotStarred(QFeedEntry entry) {
- return JPAExpressions.selectOne().from(STATUS).where(STATUS.entry.eq(entry).and(STATUS.starred.isTrue())).notExists();
- }
+ public static BooleanExpression isNotStarred(QFeedEntry entry) {
+ return JPAExpressions.selectOne()
+ .from(STATUS)
+ .where(STATUS.entry.eq(entry).and(STATUS.starred.isTrue()))
+ .notExists();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java
index e168392e..b2d22983 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java
@@ -1,19 +1,17 @@
package com.commafeed.backend.dao;
-import java.util.concurrent.Callable;
-
-import jakarta.inject.Singleton;
-
import io.quarkus.narayana.jta.QuarkusTransaction;
+import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
@Singleton
public class UnitOfWork {
- public void run(Runnable runnable) {
- QuarkusTransaction.joiningExisting().run(runnable);
- }
+ public void run(Runnable runnable) {
+ QuarkusTransaction.joiningExisting().run(runnable);
+ }
- public T call(Callable callable) {
- return QuarkusTransaction.joiningExisting().call(callable);
- }
+ public T call(Callable callable) {
+ return QuarkusTransaction.joiningExisting().call(callable);
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java
index 8353fadf..ae06f4d6 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java
@@ -1,33 +1,32 @@
package com.commafeed.backend.dao;
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class UserDAO extends GenericDAO {
- private static final QUser USER = QUser.user;
+ private static final QUser USER = QUser.user;
- public UserDAO(EntityManager entityManager) {
- super(entityManager, User.class);
- }
+ public UserDAO(EntityManager entityManager) {
+ super(entityManager, User.class);
+ }
- public User findByName(String name) {
- return query().selectFrom(USER).where(USER.name.equalsIgnoreCase(name)).fetchOne();
- }
+ public User findByName(String name) {
+ return query().selectFrom(USER).where(USER.name.equalsIgnoreCase(name)).fetchOne();
+ }
- public User findByApiKey(String key) {
- return query().selectFrom(USER).where(USER.apiKey.equalsIgnoreCase(key)).fetchOne();
- }
+ public User findByApiKey(String key) {
+ return query().selectFrom(USER).where(USER.apiKey.equalsIgnoreCase(key)).fetchOne();
+ }
- public User findByEmail(String email) {
- return query().selectFrom(USER).where(USER.email.equalsIgnoreCase(email)).fetchOne();
- }
+ public User findByEmail(String email) {
+ return query().selectFrom(USER).where(USER.email.equalsIgnoreCase(email)).fetchOne();
+ }
- public long count() {
- return query().select(USER.count()).from(USER).fetchOne();
- }
+ public long count() {
+ return query().select(USER.count()).from(USER).fetchOne();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java
index 61ae8030..d582b13d 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java
@@ -1,39 +1,37 @@
package com.commafeed.backend.dao;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.QUserRole;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
@Singleton
public class UserRoleDAO extends GenericDAO {
- private static final QUserRole ROLE = QUserRole.userRole;
+ private static final QUserRole ROLE = QUserRole.userRole;
- public UserRoleDAO(EntityManager entityManager) {
- super(entityManager, UserRole.class);
- }
+ public UserRoleDAO(EntityManager entityManager) {
+ super(entityManager, UserRole.class);
+ }
- public List findAll() {
- return query().selectFrom(ROLE).leftJoin(ROLE.user).fetchJoin().distinct().fetch();
- }
+ public List findAll() {
+ return query().selectFrom(ROLE).leftJoin(ROLE.user).fetchJoin().distinct().fetch();
+ }
- public List findAll(User user) {
- return query().selectFrom(ROLE).where(ROLE.user.eq(user)).distinct().fetch();
- }
+ public List findAll(User user) {
+ return query().selectFrom(ROLE).where(ROLE.user.eq(user)).distinct().fetch();
+ }
- public Set findRoles(User user) {
- return findAll(user).stream().map(UserRole::getRole).collect(Collectors.toSet());
- }
+ public Set findRoles(User user) {
+ return findAll(user).stream().map(UserRole::getRole).collect(Collectors.toSet());
+ }
- public long countAdmins() {
- return query().select(ROLE.count()).from(ROLE).where(ROLE.role.eq(Role.ADMIN)).fetchOne();
- }
+ public long countAdmins() {
+ return query().select(ROLE.count()).from(ROLE).where(ROLE.role.eq(Role.ADMIN)).fetchOne();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java
index 75e5a269..b17aa780 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java
@@ -1,22 +1,21 @@
package com.commafeed.backend.dao;
-import jakarta.inject.Singleton;
-import jakarta.persistence.EntityManager;
-
import com.commafeed.backend.model.QUserSettings;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
+import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class UserSettingsDAO extends GenericDAO {
- private static final QUserSettings SETTINGS = QUserSettings.userSettings;
+ private static final QUserSettings SETTINGS = QUserSettings.userSettings;
- public UserSettingsDAO(EntityManager entityManager) {
- super(entityManager, UserSettings.class);
- }
+ public UserSettingsDAO(EntityManager entityManager) {
+ super(entityManager, UserSettings.class);
+ }
- public UserSettings findByUser(User user) {
- return query().selectFrom(SETTINGS).where(SETTINGS.user.eq(user)).fetchFirst();
- }
+ public UserSettings findByUser(User user) {
+ return query().selectFrom(SETTINGS).where(SETTINGS.user.eq(user)).fetchFirst();
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java
index cbb58fe1..45bf8943 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java
@@ -1,21 +1,17 @@
package com.commafeed.backend.favicon;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.List;
-
-import jakarta.annotation.Priority;
-import jakarta.inject.Singleton;
-
-import org.apache.hc.core5.http.NameValuePair;
-import org.apache.hc.core5.net.URIBuilder;
-
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
-
+import jakarta.annotation.Priority;
+import jakarta.inject.Singleton;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.net.URIBuilder;
@Slf4j
@RequiredArgsConstructor
@@ -23,44 +19,49 @@ import lombok.extern.slf4j.Slf4j;
@Priority(3)
public class FacebookFaviconFetcher implements FaviconFetcher {
- private final HttpGetter getter;
+ private final HttpGetter getter;
- @Override
- public Favicon fetch(Feed feed) {
- String url = feed.getUrl();
- if (!url.toLowerCase().contains("www.facebook.com")) {
- return null;
- }
+ @Override
+ public Favicon fetch(Feed feed) {
+ String url = feed.getUrl();
+ if (!url.toLowerCase().contains("www.facebook.com")) {
+ return null;
+ }
- String userName = extractUserName(url);
- if (userName == null) {
- return null;
- }
+ String userName = extractUserName(url);
+ if (userName == null) {
+ return null;
+ }
- String iconUrl = String.format("https://graph.facebook.com/%s/picture?type=square&height=16", userName);
+ String iconUrl =
+ String.format(
+ "https://graph.facebook.com/%s/picture?type=square&height=16", userName);
- try {
- log.debug("Getting Facebook user's icon, {}", url);
+ try {
+ log.debug("Getting Facebook user's icon, {}", url);
- HttpResult iconResult = getter.get(iconUrl);
- return new Favicon(iconResult.content(), iconResult.contentType());
- } catch (Exception e) {
- log.debug("Failed to retrieve Facebook icon", e);
- return null;
- }
- }
+ HttpResult iconResult = getter.get(iconUrl);
+ return new Favicon(iconResult.content(), iconResult.contentType());
+ } catch (Exception e) {
+ log.debug("Failed to retrieve Facebook icon", e);
+ return null;
+ }
+ }
- private String extractUserName(String url) {
- URI uri;
- try {
- uri = new URI(url);
- } catch (URISyntaxException e) {
- log.debug("could not parse url", e);
- return null;
- }
-
- List params = new URIBuilder(uri).getQueryParams();
- return params.stream().filter(p -> "id".equals(p.getName())).map(NameValuePair::getValue).findFirst().orElse(null);
- }
+ private String extractUserName(String url) {
+ URI uri;
+ try {
+ uri = new URI(url);
+ } catch (URISyntaxException e) {
+ log.debug("could not parse url", e);
+ return null;
+ }
+ List params = new URIBuilder(uri).getQueryParams();
+ return params.stream()
+ .filter(p -> "id".equals(p.getName()))
+ .map(NameValuePair::getValue)
+ .findFirst()
+ .orElse(null);
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/Favicon.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/Favicon.java
index 67f6daa7..73d267f8 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/Favicon.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/Favicon.java
@@ -1,24 +1,24 @@
package com.commafeed.backend.favicon;
import jakarta.ws.rs.core.MediaType;
-
import lombok.extern.slf4j.Slf4j;
@Slf4j
public record Favicon(byte[] icon, MediaType mediaType) {
- private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.valueOf("image/x-icon");
+ private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.valueOf("image/x-icon");
- public Favicon(byte[] icon, String contentType) {
- this(icon, parseMediaType(contentType));
- }
+ public Favicon(byte[] icon, String contentType) {
+ this(icon, parseMediaType(contentType));
+ }
- private static MediaType parseMediaType(String contentType) {
- try {
- return MediaType.valueOf(contentType);
- } catch (Exception e) {
- log.debug("invalid content type '{}' received, returning default value", contentType, e);
- return DEFAULT_MEDIA_TYPE;
- }
- }
-}
\ No newline at end of file
+ private static MediaType parseMediaType(String contentType) {
+ try {
+ return MediaType.valueOf(contentType);
+ } catch (Exception e) {
+ log.debug(
+ "invalid content type '{}' received, returning default value", contentType, e);
+ return DEFAULT_MEDIA_TYPE;
+ }
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FaviconFetcher.java
index 85fcdfaf..b2bad744 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FaviconFetcher.java
@@ -4,6 +4,5 @@ import com.commafeed.backend.model.Feed;
public interface FaviconFetcher {
- Favicon fetch(Feed feed);
-
+ Favicon fetch(Feed feed);
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FeedFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FeedFaviconFetcher.java
index 3b5b1369..28df4a39 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FeedFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FeedFaviconFetcher.java
@@ -1,39 +1,35 @@
package com.commafeed.backend.favicon;
-import jakarta.annotation.Priority;
-import jakarta.inject.Singleton;
-
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
-
+import jakarta.annotation.Priority;
+import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-/**
- * Fetch favicon from the url declared in the feed.
- */
+/** Fetch favicon from the url declared in the feed. */
@Slf4j
@RequiredArgsConstructor
@Singleton
@Priority(2)
public class FeedFaviconFetcher implements FaviconFetcher {
- private final HttpGetter getter;
+ private final HttpGetter getter;
- @Override
- public Favicon fetch(Feed feed) {
- String url = feed.getIconUrl();
- if (url == null) {
- return null;
- }
+ @Override
+ public Favicon fetch(Feed feed) {
+ String url = feed.getIconUrl();
+ if (url == null) {
+ return null;
+ }
- try {
- HttpResult result = getter.get(url);
- return new Favicon(result.content(), result.contentType());
- } catch (Exception e) {
- log.debug("Failed to retrieve icon declared in the feed {}", url, e);
- return null;
- }
- }
+ try {
+ HttpResult result = getter.get(url);
+ return new Favicon(result.content(), result.contentType());
+ } catch (Exception e) {
+ log.debug("Failed to retrieve icon declared in the feed {}", url, e);
+ return null;
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/HtmlFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/HtmlFaviconFetcher.java
index 128266c1..7a254f30 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/HtmlFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/HtmlFaviconFetcher.java
@@ -1,65 +1,60 @@
package com.commafeed.backend.favicon;
+import com.commafeed.backend.HttpGetter;
+import com.commafeed.backend.HttpGetter.HttpResult;
+import com.commafeed.backend.model.Feed;
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
-
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
-import com.commafeed.backend.HttpGetter;
-import com.commafeed.backend.HttpGetter.HttpResult;
-import com.commafeed.backend.model.Feed;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * Extracts favicon url from html page.
- */
+/** Extracts favicon url from html page. */
@Slf4j
@RequiredArgsConstructor
@Singleton
@Priority(1)
public class HtmlFaviconFetcher implements FaviconFetcher {
- private final HttpGetter getter;
+ private final HttpGetter getter;
- @Override
- public Favicon fetch(Feed feed) {
- String url = feed.getLink();
- if (url == null) {
- return null;
- }
+ @Override
+ public Favicon fetch(Feed feed) {
+ String url = feed.getLink();
+ if (url == null) {
+ return null;
+ }
- Document doc;
- try {
- HttpResult result = getter.get(url);
- doc = Jsoup.parse(new String(result.content()), url);
- } catch (Exception e) {
- log.debug("Failed to retrieve page to find icon", e);
- return null;
- }
+ Document doc;
+ try {
+ HttpResult result = getter.get(url);
+ doc = Jsoup.parse(new String(result.content()), url);
+ } catch (Exception e) {
+ log.debug("Failed to retrieve page to find icon", e);
+ return null;
+ }
- Elements icons = doc.select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]");
- if (icons.isEmpty()) {
- log.debug("No icon found in page {}", url);
- return null;
- }
+ Elements icons = doc.select("link[rel~=(?i)^(shortcut|icon|shortcut icon)$]");
+ if (icons.isEmpty()) {
+ log.debug("No icon found in page {}", url);
+ return null;
+ }
- String href = icons.getFirst().attr("abs:href");
- if (StringUtils.isBlank(href)) {
- log.debug("No icon found in page");
- return null;
- }
+ String href = icons.getFirst().attr("abs:href");
+ if (StringUtils.isBlank(href)) {
+ log.debug("No icon found in page");
+ return null;
+ }
- try {
- HttpResult result = getter.get(href);
- return new Favicon(result.content(), result.contentType());
- } catch (Exception e) {
- log.debug("Failed to retrieve icon found in page {}", href, e);
- return null;
- }
- }
+ try {
+ HttpResult result = getter.get(href);
+ return new Favicon(result.content(), result.contentType());
+ } catch (Exception e) {
+ log.debug("Failed to retrieve icon found in page {}", href, e);
+ return null;
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/RootFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/RootFaviconFetcher.java
index b28a1fa6..92439f8a 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/RootFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/RootFaviconFetcher.java
@@ -1,45 +1,40 @@
package com.commafeed.backend.favicon;
-import java.net.URI;
-
-import jakarta.annotation.Priority;
-import jakarta.inject.Singleton;
-
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
-
+import jakarta.annotation.Priority;
+import jakarta.inject.Singleton;
+import java.net.URI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-/**
- * Fetches favicon from root of the domain (e.g. https://example.com/favicon.ico)
- */
+/** Fetches favicon from root of the domain (e.g. https://example.com/favicon.ico) */
@Slf4j
@RequiredArgsConstructor
@Singleton
@Priority(0)
public class RootFaviconFetcher implements FaviconFetcher {
- private final HttpGetter getter;
+ private final HttpGetter getter;
- @Override
- public Favicon fetch(Feed feed) {
- String url = feed.getLink();
- if (url == null) {
- url = feed.getUrl();
- }
+ @Override
+ public Favicon fetch(Feed feed) {
+ String url = feed.getLink();
+ if (url == null) {
+ url = feed.getUrl();
+ }
- try {
- URI uri = URI.create(url.trim());
- String faviconUrl = "%s://%s/favicon.ico".formatted(uri.getScheme(), uri.getHost());
+ try {
+ URI uri = URI.create(url.trim());
+ String faviconUrl = "%s://%s/favicon.ico".formatted(uri.getScheme(), uri.getHost());
- log.debug("getting root icon at {}", faviconUrl);
- HttpResult result = getter.get(faviconUrl);
- return new Favicon(result.content(), result.contentType());
- } catch (Exception e) {
- log.debug("Failed to retrieve iconAtRoot for url {}: ", url, e);
- return null;
- }
- }
+ log.debug("getting root icon at {}", faviconUrl);
+ HttpResult result = getter.get(faviconUrl);
+ return new Favicon(result.content(), result.contentType());
+ } catch (Exception e) {
+ log.debug("Failed to retrieve iconAtRoot for url {}: ", url, e);
+ return null;
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
index ca5d2a08..6fd7f841 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
@@ -1,18 +1,5 @@
package com.commafeed.backend.favicon;
-import java.io.IOException;
-import java.net.URI;
-import java.util.List;
-import java.util.Optional;
-
-import jakarta.annotation.Priority;
-import jakarta.inject.Singleton;
-import jakarta.ws.rs.core.UriBuilder;
-
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.hc.core5.http.NameValuePair;
-import org.apache.hc.core5.net.URIBuilder;
-
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
@@ -23,9 +10,18 @@ import com.commafeed.backend.model.Feed;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-
+import jakarta.annotation.Priority;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.net.URIBuilder;
@Slf4j
@RequiredArgsConstructor
@@ -33,99 +29,121 @@ import lombok.extern.slf4j.Slf4j;
@Priority(3)
public class YoutubeFaviconFetcher implements FaviconFetcher {
- private static final String PART_SNIPPET = "snippet";
+ private static final String PART_SNIPPET = "snippet";
- private static final JsonPointer CHANNEL_THUMBNAIL_URL = JsonPointer.compile("/items/0/snippet/thumbnails/default/url");
- private static final JsonPointer PLAYLIST_CHANNEL_ID = JsonPointer.compile("/items/0/snippet/channelId");
+ private static final JsonPointer CHANNEL_THUMBNAIL_URL =
+ JsonPointer.compile("/items/0/snippet/thumbnails/default/url");
+ private static final JsonPointer PLAYLIST_CHANNEL_ID =
+ JsonPointer.compile("/items/0/snippet/channelId");
- private final HttpGetter getter;
- private final CommaFeedConfiguration config;
- private final ObjectMapper objectMapper;
+ private final HttpGetter getter;
+ private final CommaFeedConfiguration config;
+ private final ObjectMapper objectMapper;
- @Override
- public Favicon fetch(Feed feed) {
- String url = feed.getUrl();
- if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) {
- return null;
- }
+ @Override
+ public Favicon fetch(Feed feed) {
+ String url = feed.getUrl();
+ if (!url.toLowerCase().contains("youtube.com/feeds/videos.xml")) {
+ return null;
+ }
- Optional googleAuthKey = config.googleAuthKey();
- if (googleAuthKey.isEmpty()) {
- log.debug("no google auth key configured");
- return null;
- }
+ Optional googleAuthKey = config.googleAuthKey();
+ if (googleAuthKey.isEmpty()) {
+ log.debug("no google auth key configured");
+ return null;
+ }
- try {
- List params = new URIBuilder(url).getQueryParams();
- Optional userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst();
- Optional channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst();
- Optional playlistId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id")).findFirst();
+ try {
+ List params = new URIBuilder(url).getQueryParams();
+ Optional userId =
+ params.stream()
+ .filter(nvp -> nvp.getName().equalsIgnoreCase("user"))
+ .findFirst();
+ Optional channelId =
+ params.stream()
+ .filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id"))
+ .findFirst();
+ Optional playlistId =
+ params.stream()
+ .filter(nvp -> nvp.getName().equalsIgnoreCase("playlist_id"))
+ .findFirst();
- byte[] response = null;
- if (userId.isPresent()) {
- log.debug("contacting youtube api for user {}", userId.get().getValue());
- response = fetchForUser(googleAuthKey.get(), userId.get().getValue());
- } else if (channelId.isPresent()) {
- log.debug("contacting youtube api for channel {}", channelId.get().getValue());
- response = fetchForChannel(googleAuthKey.get(), channelId.get().getValue());
- } else if (playlistId.isPresent()) {
- log.debug("contacting youtube api for playlist {}", playlistId.get().getValue());
- response = fetchForPlaylist(googleAuthKey.get(), playlistId.get().getValue());
- }
- if (ArrayUtils.isEmpty(response)) {
- log.debug("youtube api returned empty response");
- return null;
- }
+ byte[] response = null;
+ if (userId.isPresent()) {
+ log.debug("contacting youtube api for user {}", userId.get().getValue());
+ response = fetchForUser(googleAuthKey.get(), userId.get().getValue());
+ } else if (channelId.isPresent()) {
+ log.debug("contacting youtube api for channel {}", channelId.get().getValue());
+ response = fetchForChannel(googleAuthKey.get(), channelId.get().getValue());
+ } else if (playlistId.isPresent()) {
+ log.debug("contacting youtube api for playlist {}", playlistId.get().getValue());
+ response = fetchForPlaylist(googleAuthKey.get(), playlistId.get().getValue());
+ }
+ if (ArrayUtils.isEmpty(response)) {
+ log.debug("youtube api returned empty response");
+ return null;
+ }
- JsonNode thumbnailUrl = objectMapper.readTree(response).at(CHANNEL_THUMBNAIL_URL);
- if (thumbnailUrl.isMissingNode()) {
- log.debug("youtube api returned invalid response");
- return null;
- }
+ JsonNode thumbnailUrl = objectMapper.readTree(response).at(CHANNEL_THUMBNAIL_URL);
+ if (thumbnailUrl.isMissingNode()) {
+ log.debug("youtube api returned invalid response");
+ return null;
+ }
- HttpResult iconResult = getter.get(thumbnailUrl.asText());
- return new Favicon(iconResult.content(), iconResult.contentType());
- } catch (Exception e) {
- log.debug("Failed to retrieve YouTube icon", e);
- return null;
- }
- }
+ HttpResult iconResult = getter.get(thumbnailUrl.asText());
+ return new Favicon(iconResult.content(), iconResult.contentType());
+ } catch (Exception e) {
+ log.debug("Failed to retrieve YouTube icon", e);
+ return null;
+ }
+ }
- private byte[] fetchForUser(String googleAuthKey, String userId)
- throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
- URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
- .queryParam("part", PART_SNIPPET)
- .queryParam("key", googleAuthKey)
- .queryParam("forUsername", userId)
- .build();
- return getter.get(uri.toString()).content();
- }
+ private byte[] fetchForUser(String googleAuthKey, String userId)
+ throws IOException,
+ NotModifiedException,
+ TooManyRequestsException,
+ SchemeNotAllowedException {
+ URI uri =
+ UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
+ .queryParam("part", PART_SNIPPET)
+ .queryParam("key", googleAuthKey)
+ .queryParam("forUsername", userId)
+ .build();
+ return getter.get(uri.toString()).content();
+ }
- private byte[] fetchForChannel(String googleAuthKey, String channelId)
- throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
- URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
- .queryParam("part", PART_SNIPPET)
- .queryParam("key", googleAuthKey)
- .queryParam("id", channelId)
- .build();
- return getter.get(uri.toString()).content();
- }
+ private byte[] fetchForChannel(String googleAuthKey, String channelId)
+ throws IOException,
+ NotModifiedException,
+ TooManyRequestsException,
+ SchemeNotAllowedException {
+ URI uri =
+ UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/channels")
+ .queryParam("part", PART_SNIPPET)
+ .queryParam("key", googleAuthKey)
+ .queryParam("id", channelId)
+ .build();
+ return getter.get(uri.toString()).content();
+ }
- private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
- throws IOException, NotModifiedException, TooManyRequestsException, SchemeNotAllowedException {
- URI uri = UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
- .queryParam("part", PART_SNIPPET)
- .queryParam("key", googleAuthKey)
- .queryParam("id", playlistId)
- .build();
- byte[] playlistBytes = getter.get(uri.toString()).content();
+ private byte[] fetchForPlaylist(String googleAuthKey, String playlistId)
+ throws IOException,
+ NotModifiedException,
+ TooManyRequestsException,
+ SchemeNotAllowedException {
+ URI uri =
+ UriBuilder.fromUri("https://www.googleapis.com/youtube/v3/playlists")
+ .queryParam("part", PART_SNIPPET)
+ .queryParam("key", googleAuthKey)
+ .queryParam("id", playlistId)
+ .build();
+ byte[] playlistBytes = getter.get(uri.toString()).content();
- JsonNode channelId = objectMapper.readTree(playlistBytes).at(PLAYLIST_CHANNEL_ID);
- if (channelId.isMissingNode()) {
- return new byte[0];
- }
-
- return fetchForChannel(googleAuthKey, channelId.asText());
- }
+ JsonNode channelId = objectMapper.readTree(playlistBytes).at(PLAYLIST_CHANNEL_ID);
+ if (channelId.isMissingNode()) {
+ return new byte[0];
+ }
+ return fetchForChannel(googleAuthKey, channelId.asText());
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedEntryKeyword.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedEntryKeyword.java
index 782edb96..2612472c 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedEntryKeyword.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedEntryKeyword.java
@@ -2,30 +2,28 @@ package com.commafeed.backend.feed;
import java.util.ArrayList;
import java.util.List;
-
import org.apache.commons.lang3.StringUtils;
-/**
- * A keyword used in a search query
- */
+/** A keyword used in a search query */
public record FeedEntryKeyword(String keyword, Mode mode) {
- public enum Mode {
- INCLUDE, EXCLUDE
- }
+ public enum Mode {
+ INCLUDE,
+ EXCLUDE
+ }
- public static List fromQueryString(String keywords) {
- List list = new ArrayList<>();
- if (keywords != null) {
- for (String keyword : StringUtils.split(keywords)) {
- boolean not = false;
- if (keyword.startsWith("-") || keyword.startsWith("!")) {
- not = true;
- keyword = keyword.substring(1);
- }
- list.add(new FeedEntryKeyword(keyword, not ? Mode.EXCLUDE : Mode.INCLUDE));
- }
- }
- return list;
- }
+ public static List fromQueryString(String keywords) {
+ List list = new ArrayList<>();
+ if (keywords != null) {
+ for (String keyword : StringUtils.split(keywords)) {
+ boolean not = false;
+ if (keyword.startsWith("-") || keyword.startsWith("!")) {
+ not = true;
+ keyword = keyword.substring(1);
+ }
+ list.add(new FeedEntryKeyword(keyword, not ? Mode.EXCLUDE : Mode.INCLUDE));
+ }
+ }
+ return list;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
index f9b5d77d..737a8988 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
@@ -1,16 +1,5 @@
package com.commafeed.backend.feed;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-
-import jakarta.inject.Singleton;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Strings;
-
import com.commafeed.backend.Digests;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpRequest;
@@ -22,96 +11,139 @@ import com.commafeed.backend.feed.parser.FeedParser;
import com.commafeed.backend.feed.parser.FeedParser.FeedParsingException;
import com.commafeed.backend.feed.parser.FeedParserResult;
import com.commafeed.backend.urlprovider.FeedURLProvider;
-
import io.quarkus.arc.All;
+import jakarta.inject.Singleton;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
-/**
- * Fetches a feed then parses it
- */
+/** Fetches a feed then parses it */
@Slf4j
@Singleton
public class FeedFetcher {
- private final FeedParser parser;
- private final HttpGetter getter;
- private final List urlProviders;
+ private final FeedParser parser;
+ private final HttpGetter getter;
+ private final List urlProviders;
- public FeedFetcher(FeedParser parser, HttpGetter getter, @All List urlProviders) {
- this.parser = parser;
- this.getter = getter;
- this.urlProviders = urlProviders;
- }
+ public FeedFetcher(
+ FeedParser parser, HttpGetter getter, @All List urlProviders) {
+ this.parser = parser;
+ this.getter = getter;
+ this.urlProviders = urlProviders;
+ }
- public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
- Instant lastPublishedDate, String lastContentHash) throws FeedParsingException, IOException, NotModifiedException,
- TooManyRequestsException, SchemeNotAllowedException, NoFeedFoundException {
- log.debug("Fetching feed {}", feedUrl);
+ public FeedFetcherResult fetch(
+ String feedUrl,
+ boolean extractFeedUrlFromHtml,
+ String lastModified,
+ String eTag,
+ Instant lastPublishedDate,
+ String lastContentHash)
+ throws FeedParsingException,
+ IOException,
+ NotModifiedException,
+ TooManyRequestsException,
+ SchemeNotAllowedException,
+ NoFeedFoundException {
+ log.debug("Fetching feed {}", feedUrl);
- HttpResult result = getter.get(HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
- byte[] content = result.content();
+ HttpResult result =
+ getter.get(
+ HttpRequest.builder(feedUrl).lastModified(lastModified).eTag(eTag).build());
+ byte[] content = result.content();
- FeedParserResult parserResult;
- try {
- parserResult = parser.parse(result.urlAfterRedirect(), content);
- } catch (FeedParsingException e) {
- if (extractFeedUrlFromHtml) {
- String extractedUrl = extractFeedUrl(urlProviders, feedUrl, new String(result.content(), StandardCharsets.UTF_8));
- if (StringUtils.isNotBlank(extractedUrl)) {
- feedUrl = extractedUrl;
+ FeedParserResult parserResult;
+ try {
+ parserResult = parser.parse(result.urlAfterRedirect(), content);
+ } catch (FeedParsingException e) {
+ if (extractFeedUrlFromHtml) {
+ String extractedUrl =
+ extractFeedUrl(
+ urlProviders,
+ feedUrl,
+ new String(result.content(), StandardCharsets.UTF_8));
+ if (StringUtils.isNotBlank(extractedUrl)) {
+ feedUrl = extractedUrl;
- result = getter.get(HttpRequest.builder(extractedUrl).lastModified(lastModified).eTag(eTag).build());
- content = result.content();
- parserResult = parser.parse(result.urlAfterRedirect(), content);
- } else {
- throw new NoFeedFoundException(e);
- }
- } else {
- throw e;
- }
- }
+ result =
+ getter.get(
+ HttpRequest.builder(extractedUrl)
+ .lastModified(lastModified)
+ .eTag(eTag)
+ .build());
+ content = result.content();
+ parserResult = parser.parse(result.urlAfterRedirect(), content);
+ } else {
+ throw new NoFeedFoundException(e);
+ }
+ } else {
+ throw e;
+ }
+ }
- if (content == null) {
- throw new IOException("Feed content is empty.");
- }
+ if (content == null) {
+ throw new IOException("Feed content is empty.");
+ }
- boolean lastModifiedHeaderValueChanged = !Strings.CS.equals(lastModified, result.lastModifiedSince());
- boolean etagHeaderValueChanged = !Strings.CS.equals(eTag, result.eTag());
+ boolean lastModifiedHeaderValueChanged =
+ !Strings.CS.equals(lastModified, result.lastModifiedSince());
+ boolean etagHeaderValueChanged = !Strings.CS.equals(eTag, result.eTag());
- String hash = Digests.sha1Hex(content);
- if (lastContentHash != null && lastContentHash.equals(hash)) {
- log.debug("content hash not modified: {}", feedUrl);
- throw new NotModifiedException("content hash not modified", lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null,
- etagHeaderValueChanged ? result.eTag() : null);
- }
+ String hash = Digests.sha1Hex(content);
+ if (lastContentHash != null && lastContentHash.equals(hash)) {
+ log.debug("content hash not modified: {}", feedUrl);
+ throw new NotModifiedException(
+ "content hash not modified",
+ lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null,
+ etagHeaderValueChanged ? result.eTag() : null);
+ }
- if (lastPublishedDate != null && lastPublishedDate.equals(parserResult.lastPublishedDate())) {
- log.debug("publishedDate not modified: {}", feedUrl);
- throw new NotModifiedException("publishedDate not modified", lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null,
- etagHeaderValueChanged ? result.eTag() : null);
- }
+ if (lastPublishedDate != null
+ && lastPublishedDate.equals(parserResult.lastPublishedDate())) {
+ log.debug("publishedDate not modified: {}", feedUrl);
+ throw new NotModifiedException(
+ "publishedDate not modified",
+ lastModifiedHeaderValueChanged ? result.lastModifiedSince() : null,
+ etagHeaderValueChanged ? result.eTag() : null);
+ }
- return new FeedFetcherResult(parserResult, result.urlAfterRedirect(), result.lastModifiedSince(), result.eTag(), hash,
- result.validFor());
- }
+ return new FeedFetcherResult(
+ parserResult,
+ result.urlAfterRedirect(),
+ result.lastModifiedSince(),
+ result.eTag(),
+ hash,
+ result.validFor());
+ }
- private static String extractFeedUrl(List urlProviders, String url, String urlContent) {
- return urlProviders.stream()
- .flatMap(provider -> provider.get(url, urlContent).stream())
- .filter(StringUtils::isNotBlank)
- .findFirst()
- .orElse(null);
- }
+ private static String extractFeedUrl(
+ List urlProviders, String url, String urlContent) {
+ return urlProviders.stream()
+ .flatMap(provider -> provider.get(url, urlContent).stream())
+ .filter(StringUtils::isNotBlank)
+ .findFirst()
+ .orElse(null);
+ }
- public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader,
- String contentHash, Duration validFor) {}
+ public record FeedFetcherResult(
+ FeedParserResult feed,
+ String urlAfterRedirect,
+ String lastModifiedHeader,
+ String lastETagHeader,
+ String contentHash,
+ Duration validFor) {}
- public static class NoFeedFoundException extends Exception {
- private static final long serialVersionUID = 1L;
-
- public NoFeedFoundException(Throwable cause) {
- super("This URL does not point to an RSS feed or a website with an RSS feed.", cause);
- }
- }
+ public static class NoFeedFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+ public NoFeedFoundException(Throwable cause) {
+ super("This URL does not point to an RSS feed or a website with an RSS feed.", cause);
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
index 0ef815df..9735c209 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
@@ -1,19 +1,5 @@
package com.commafeed.backend.feed;
-import java.time.Instant;
-import java.util.List;
-import java.util.concurrent.BlockingDeque;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-import jakarta.inject.Singleton;
-
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
@@ -25,229 +11,297 @@ import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedSubscription;
import com.google.common.util.concurrent.MoreExecutors;
-
+import jakarta.inject.Singleton;
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class FeedRefreshEngine {
- private final UnitOfWork unitOfWork;
- private final FeedDAO feedDAO;
- private final FeedRefreshWorker worker;
- private final FeedRefreshUpdater updater;
- private final FeedUpdateNotifier notifier;
- private final CommaFeedConfiguration config;
- private final Meter refill;
+ private final UnitOfWork unitOfWork;
+ private final FeedDAO feedDAO;
+ private final FeedRefreshWorker worker;
+ private final FeedRefreshUpdater updater;
+ private final FeedUpdateNotifier notifier;
+ private final CommaFeedConfiguration config;
+ private final Meter refill;
- private final BlockingDeque queue;
+ private final BlockingDeque queue;
- private ExecutorService feedProcessingLoopExecutor;
- private ExecutorService refillLoopExecutor;
- private ThreadPoolExecutor refillExecutor;
- private ThreadPoolExecutor workerExecutor;
- private ThreadPoolExecutor databaseUpdaterExecutor;
- private ThreadPoolExecutor notifierExecutor;
+ private ExecutorService feedProcessingLoopExecutor;
+ private ExecutorService refillLoopExecutor;
+ private ThreadPoolExecutor refillExecutor;
+ private ThreadPoolExecutor workerExecutor;
+ private ThreadPoolExecutor databaseUpdaterExecutor;
+ private ThreadPoolExecutor notifierExecutor;
- public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
- FeedUpdateNotifier notifier, CommaFeedConfiguration config, MetricRegistry metrics) {
- this.unitOfWork = unitOfWork;
- this.feedDAO = feedDAO;
- this.worker = worker;
- this.updater = updater;
- this.notifier = notifier;
- this.config = config;
- this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
+ public FeedRefreshEngine(
+ UnitOfWork unitOfWork,
+ FeedDAO feedDAO,
+ FeedRefreshWorker worker,
+ FeedRefreshUpdater updater,
+ FeedUpdateNotifier notifier,
+ CommaFeedConfiguration config,
+ MetricRegistry metrics) {
+ this.unitOfWork = unitOfWork;
+ this.feedDAO = feedDAO;
+ this.worker = worker;
+ this.updater = updater;
+ this.notifier = notifier;
+ this.config = config;
+ this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
- this.queue = new LinkedBlockingDeque<>();
+ this.queue = new LinkedBlockingDeque<>();
- metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge) queue::size);
- metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge) () -> workerExecutor.getActiveCount());
- metrics.register(MetricRegistry.name(getClass(), "updater", "active"),
- (Gauge) () -> databaseUpdaterExecutor.getActiveCount());
- metrics.register(MetricRegistry.name(getClass(), "notifier", "active"), (Gauge) () -> notifierExecutor.getActiveCount());
- metrics.register(MetricRegistry.name(getClass(), "notifier", "queue"), (Gauge) () -> notifierExecutor.getQueue().size());
- }
+ metrics.register(
+ MetricRegistry.name(getClass(), "queue", "size"), (Gauge) queue::size);
+ metrics.register(
+ MetricRegistry.name(getClass(), "worker", "active"),
+ (Gauge) () -> workerExecutor.getActiveCount());
+ metrics.register(
+ MetricRegistry.name(getClass(), "updater", "active"),
+ (Gauge) () -> databaseUpdaterExecutor.getActiveCount());
+ metrics.register(
+ MetricRegistry.name(getClass(), "notifier", "active"),
+ (Gauge) () -> notifierExecutor.getActiveCount());
+ metrics.register(
+ MetricRegistry.name(getClass(), "notifier", "queue"),
+ (Gauge) () -> notifierExecutor.getQueue().size());
+ }
- private void createExecutors() {
- this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
- this.refillLoopExecutor = Executors.newSingleThreadExecutor();
- this.refillExecutor = newDiscardingSingleThreadExecutorService();
- this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
- this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
- this.notifierExecutor = newDiscardingExecutorService(config.pushNotifications().threads(),
- config.pushNotifications().queueCapacity());
- }
+ private void createExecutors() {
+ this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
+ this.refillLoopExecutor = Executors.newSingleThreadExecutor();
+ this.refillExecutor = newDiscardingSingleThreadExecutorService();
+ this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
+ this.databaseUpdaterExecutor =
+ newBlockingExecutorService(config.feedRefresh().databaseThreads());
+ this.notifierExecutor =
+ newDiscardingExecutorService(
+ config.pushNotifications().threads(),
+ config.pushNotifications().queueCapacity());
+ }
- public void start() {
- createExecutors();
- startFeedProcessingLoop();
- startRefillLoop();
- }
+ public void start() {
+ createExecutors();
+ startFeedProcessingLoop();
+ startRefillLoop();
+ }
- private void startFeedProcessingLoop() {
- // take a feed from the queue, process it, rince, repeat
- feedProcessingLoopExecutor.submit(() -> {
- while (!feedProcessingLoopExecutor.isShutdown()) {
- try {
- // take() is blocking until a feed is available from the queue
- Feed feed = queue.take();
+ private void startFeedProcessingLoop() {
+ // take a feed from the queue, process it, rince, repeat
+ feedProcessingLoopExecutor.submit(
+ () -> {
+ while (!feedProcessingLoopExecutor.isShutdown()) {
+ try {
+ // take() is blocking until a feed is available from the queue
+ Feed feed = queue.take();
- // send the feed to be processed
- log.debug("got feed {} from the queue, send it for processing", feed.getId());
- processFeedAsync(feed);
+ // send the feed to be processed
+ log.debug(
+ "got feed {} from the queue, send it for processing",
+ feed.getId());
+ processFeedAsync(feed);
- // we removed a feed from the queue, try to refill it as it may now be empty
- if (queue.isEmpty()) {
- log.debug("took the last feed from the queue, try to refill");
- refillQueueAsync();
- }
- } catch (InterruptedException e) {
- log.debug("interrupted while waiting for a feed in the queue");
- Thread.currentThread().interrupt();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- }
- });
- }
+ // we removed a feed from the queue, try to refill it as it may now be
+ // empty
+ if (queue.isEmpty()) {
+ log.debug("took the last feed from the queue, try to refill");
+ refillQueueAsync();
+ }
+ } catch (InterruptedException e) {
+ log.debug("interrupted while waiting for a feed in the queue");
+ Thread.currentThread().interrupt();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ });
+ }
- private void startRefillLoop() {
- // refill the queue at regular intervals if it's empty
- refillLoopExecutor.submit(() -> {
- while (!refillLoopExecutor.isShutdown()) {
- try {
- if (queue.isEmpty()) {
- log.debug("refilling queue");
- refillQueueAsync();
- }
+ private void startRefillLoop() {
+ // refill the queue at regular intervals if it's empty
+ refillLoopExecutor.submit(
+ () -> {
+ while (!refillLoopExecutor.isShutdown()) {
+ try {
+ if (queue.isEmpty()) {
+ log.debug("refilling queue");
+ refillQueueAsync();
+ }
- log.debug("sleeping for 15s");
- TimeUnit.SECONDS.sleep(15);
- } catch (InterruptedException e) {
- log.debug("interrupted while sleeping");
- Thread.currentThread().interrupt();
- } catch (Exception e) {
- log.error(e.getMessage(), e);
- }
- }
- });
- }
+ log.debug("sleeping for 15s");
+ TimeUnit.SECONDS.sleep(15);
+ } catch (InterruptedException e) {
+ log.debug("interrupted while sleeping");
+ Thread.currentThread().interrupt();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ });
+ }
- public void refreshImmediately(Feed feed) {
- log.debug("add feed {} at the start of the queue", feed.getId());
- // remove the feed from the queue if it was already queued to avoid refreshing it twice
- queue.removeIf(f -> f.getId().equals(feed.getId()));
- queue.addFirst(feed);
- }
+ public void refreshImmediately(Feed feed) {
+ log.debug("add feed {} at the start of the queue", feed.getId());
+ // remove the feed from the queue if it was already queued to avoid refreshing it twice
+ queue.removeIf(f -> f.getId().equals(feed.getId()));
+ queue.addFirst(feed);
+ }
- private void refillQueueAsync() {
- CompletableFuture.runAsync(() -> {
- if (!queue.isEmpty()) {
- return;
- }
+ private void refillQueueAsync() {
+ CompletableFuture.runAsync(
+ () -> {
+ if (!queue.isEmpty()) {
+ return;
+ }
- refill.mark();
+ refill.mark();
- List nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
- log.debug("found {} feeds that are up for refresh", nextUpdatableFeeds.size());
- for (Feed feed : nextUpdatableFeeds) {
- // add the feed only if it was not already queued
- if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
- queue.addLast(feed);
- }
- }
- }, refillExecutor).whenComplete((data, ex) -> {
- if (ex != null) {
- log.error("error while refilling the queue", ex);
- }
- });
- }
+ List nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
+ log.debug(
+ "found {} feeds that are up for refresh",
+ nextUpdatableFeeds.size());
+ for (Feed feed : nextUpdatableFeeds) {
+ // add the feed only if it was not already queued
+ if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
+ queue.addLast(feed);
+ }
+ }
+ },
+ refillExecutor)
+ .whenComplete(
+ (data, ex) -> {
+ if (ex != null) {
+ log.error("error while refilling the queue", ex);
+ }
+ });
+ }
- private void processFeedAsync(Feed feed) {
- CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
- .thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
- .thenCompose(r -> {
- List> futures = r.insertedUnreadEntriesBySubscription().entrySet().stream().map(e -> {
- FeedSubscription sub = e.getKey();
- List entries = e.getValue();
+ private void processFeedAsync(Feed feed) {
+ CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
+ .thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
+ .thenCompose(
+ r -> {
+ List> futures =
+ r.insertedUnreadEntriesBySubscription().entrySet().stream()
+ .map(
+ e -> {
+ FeedSubscription sub = e.getKey();
+ List entries = e.getValue();
- notifier.notifyOverWebsocket(sub, entries);
- return CompletableFuture.runAsync(() -> notifier.sendPushNotifications(sub, entries), notifierExecutor);
- }).toList();
- return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
- })
- .exceptionally(ex -> {
- log.error("error while processing feed {}", feed.getUrl(), ex);
- return null;
- });
- }
+ notifier.notifyOverWebsocket(sub, entries);
+ return CompletableFuture.runAsync(
+ () ->
+ notifier
+ .sendPushNotifications(
+ sub,
+ entries),
+ notifierExecutor);
+ })
+ .toList();
+ return CompletableFuture.allOf(
+ futures.toArray(CompletableFuture[]::new));
+ })
+ .exceptionally(
+ ex -> {
+ log.error("error while processing feed {}", feed.getUrl(), ex);
+ return null;
+ });
+ }
- private List getNextUpdatableFeeds(int max) {
- return unitOfWork.call(() -> {
- Instant lastLoginThreshold = config.feedRefresh().userInactivityPeriod().isZero() ? null
- : Instant.now().minus(config.feedRefresh().userInactivityPeriod());
- List feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
- if (!feeds.isEmpty()) {
- // update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
- Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval());
- feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
- }
- return feeds;
- });
- }
+ private List getNextUpdatableFeeds(int max) {
+ return unitOfWork.call(
+ () -> {
+ Instant lastLoginThreshold =
+ config.feedRefresh().userInactivityPeriod().isZero()
+ ? null
+ : Instant.now()
+ .minus(config.feedRefresh().userInactivityPeriod());
+ List feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
+ if (!feeds.isEmpty()) {
+ // update disabledUntil to prevent feeds from being returned again by
+ // feedDAO.findNextUpdatable()
+ Instant nextUpdateDate =
+ Instant.now().plus(config.feedRefresh().interval());
+ feedDAO.setDisabledUntil(
+ feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
+ }
+ return feeds;
+ });
+ }
- private int getBatchSize() {
- return Math.min(100, 3 * config.feedRefresh().httpThreads());
- }
+ private int getBatchSize() {
+ return Math.min(100, 3 * config.feedRefresh().httpThreads());
+ }
- public void stop() {
- MoreExecutors.shutdownAndAwaitTermination(this.feedProcessingLoopExecutor, config.shutdownTimeout());
- MoreExecutors.shutdownAndAwaitTermination(this.refillLoopExecutor, config.shutdownTimeout());
- MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
- MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
- MoreExecutors.shutdownAndAwaitTermination(this.databaseUpdaterExecutor, config.shutdownTimeout());
- MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
+ public void stop() {
+ MoreExecutors.shutdownAndAwaitTermination(
+ this.feedProcessingLoopExecutor, config.shutdownTimeout());
+ MoreExecutors.shutdownAndAwaitTermination(
+ this.refillLoopExecutor, config.shutdownTimeout());
+ MoreExecutors.shutdownAndAwaitTermination(this.refillExecutor, config.shutdownTimeout());
+ MoreExecutors.shutdownAndAwaitTermination(this.workerExecutor, config.shutdownTimeout());
+ MoreExecutors.shutdownAndAwaitTermination(
+ this.databaseUpdaterExecutor, config.shutdownTimeout());
+ MoreExecutors.shutdownAndAwaitTermination(this.notifierExecutor, config.shutdownTimeout());
- queue.clear();
- }
+ queue.clear();
+ }
- /**
- * returns an ExecutorService with a single thread that discards tasks if a task is already running
- */
- private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
- ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
- pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
- return pool;
- }
+ /**
+ * returns an ExecutorService with a single thread that discards tasks if a task is already
+ * running
+ */
+ private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
+ ThreadPoolExecutor pool =
+ new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
+ pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
+ return pool;
+ }
- /**
- * returns an ExecutorService that discards tasks if the queue is full
- */
- private ThreadPoolExecutor newDiscardingExecutorService(int threads, int queueCapacity) {
- ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<>(queueCapacity));
- pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
- return pool;
- }
+ /** returns an ExecutorService that discards tasks if the queue is full */
+ private ThreadPoolExecutor newDiscardingExecutorService(int threads, int queueCapacity) {
+ ThreadPoolExecutor pool =
+ new ThreadPoolExecutor(
+ threads,
+ threads,
+ 0L,
+ TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<>(queueCapacity));
+ pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
+ return pool;
+ }
- /**
- * returns an ExecutorService that blocks submissions until a thread is available
- */
- private ThreadPoolExecutor newBlockingExecutorService(int threads) {
- ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
- pool.setRejectedExecutionHandler((r, e) -> {
- if (e.isShutdown()) {
- return;
- }
+ /** returns an ExecutorService that blocks submissions until a thread is available */
+ private ThreadPoolExecutor newBlockingExecutorService(int threads) {
+ ThreadPoolExecutor pool =
+ new ThreadPoolExecutor(
+ threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
+ pool.setRejectedExecutionHandler(
+ (r, e) -> {
+ if (e.isShutdown()) {
+ return;
+ }
- try {
- e.getQueue().put(r);
- } catch (InterruptedException ex) {
- log.debug("interrupted while waiting for a slot in the queue.", ex);
- Thread.currentThread().interrupt();
- }
- });
- return pool;
- }
+ try {
+ e.getQueue().put(r);
+ } catch (InterruptedException ex) {
+ log.debug("interrupted while waiting for a slot in the queue.", ex);
+ Thread.currentThread().interrupt();
+ }
+ });
+ return pool;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java
index 80a8b67a..4579e611 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java
@@ -1,84 +1,95 @@
package com.commafeed.backend.feed;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.InstantSource;
-import java.time.temporal.ChronoUnit;
-
-import jakarta.inject.Singleton;
-
-import org.apache.commons.lang3.ObjectUtils;
-
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
import com.google.common.primitives.Longs;
+import jakarta.inject.Singleton;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.InstantSource;
+import java.time.temporal.ChronoUnit;
+import org.apache.commons.lang3.ObjectUtils;
@Singleton
public class FeedRefreshIntervalCalculator {
- private final Duration interval;
- private final Duration maxInterval;
- private final boolean empirical;
- private final FeedRefreshErrorHandling errorHandling;
- private final InstantSource instantSource;
+ private final Duration interval;
+ private final Duration maxInterval;
+ private final boolean empirical;
+ private final FeedRefreshErrorHandling errorHandling;
+ private final InstantSource instantSource;
- public FeedRefreshIntervalCalculator(CommaFeedConfiguration config, InstantSource instantSource) {
- this.interval = config.feedRefresh().interval();
- this.maxInterval = config.feedRefresh().maxInterval();
- this.empirical = config.feedRefresh().intervalEmpirical();
- this.errorHandling = config.feedRefresh().errors();
- this.instantSource = instantSource;
- }
+ public FeedRefreshIntervalCalculator(
+ CommaFeedConfiguration config, InstantSource instantSource) {
+ this.interval = config.feedRefresh().interval();
+ this.maxInterval = config.feedRefresh().maxInterval();
+ this.empirical = config.feedRefresh().intervalEmpirical();
+ this.errorHandling = config.feedRefresh().errors();
+ this.instantSource = instantSource;
+ }
- public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval, Duration validFor) {
- Instant instant = empirical ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval)
- : instantSource.instant().plus(interval);
- return constrainToBounds(ObjectUtils.max(instant, instantSource.instant().plus(validFor)));
- }
+ public Instant onFetchSuccess(
+ Instant publishedDate, Long averageEntryInterval, Duration validFor) {
+ Instant instant =
+ empirical
+ ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval)
+ : instantSource.instant().plus(interval);
+ return constrainToBounds(ObjectUtils.max(instant, instantSource.instant().plus(validFor)));
+ }
- public Instant onFeedNotModified(Instant publishedDate, Long averageEntryInterval) {
- return onFetchSuccess(publishedDate, averageEntryInterval, Duration.ZERO);
- }
+ public Instant onFeedNotModified(Instant publishedDate, Long averageEntryInterval) {
+ return onFetchSuccess(publishedDate, averageEntryInterval, Duration.ZERO);
+ }
- public Instant onTooManyRequests(Instant retryAfter, int errorCount) {
- return constrainToBounds(ObjectUtils.max(retryAfter, onFetchError(errorCount)));
- }
+ public Instant onTooManyRequests(Instant retryAfter, int errorCount) {
+ return constrainToBounds(ObjectUtils.max(retryAfter, onFetchError(errorCount)));
+ }
- public Instant onFetchError(int errorCount) {
- if (errorCount < errorHandling.retriesBeforeBackoff()) {
- return constrainToBounds(instantSource.instant().plus(interval));
- }
+ public Instant onFetchError(int errorCount) {
+ if (errorCount < errorHandling.retriesBeforeBackoff()) {
+ return constrainToBounds(instantSource.instant().plus(interval));
+ }
- Duration retryInterval = errorHandling.backoffInterval().multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
- return constrainToBounds(instantSource.instant().plus(retryInterval));
- }
+ Duration retryInterval =
+ errorHandling
+ .backoffInterval()
+ .multipliedBy(errorCount - errorHandling.retriesBeforeBackoff() + 1L);
+ return constrainToBounds(instantSource.instant().plus(retryInterval));
+ }
- private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval) {
- Instant now = instantSource.instant();
+ private Instant computeEmpiricalRefreshInterval(
+ Instant publishedDate, Long averageEntryInterval) {
+ Instant now = instantSource.instant();
- if (publishedDate == null) {
- return now.plus(maxInterval);
- }
+ if (publishedDate == null) {
+ return now.plus(maxInterval);
+ }
- long daysSinceLastPublication = ChronoUnit.DAYS.between(publishedDate, now);
- if (daysSinceLastPublication >= 30) {
- return now.plus(maxInterval);
- } else if (daysSinceLastPublication >= 14) {
- return now.plus(maxInterval.dividedBy(2));
- } else if (daysSinceLastPublication >= 7) {
- return now.plus(maxInterval.dividedBy(4));
- } else if (averageEntryInterval != null) {
- // use average time between entries to decide when to refresh next, divided by factor
- int factor = 2;
- long millis = Longs.constrainToRange(averageEntryInterval / factor, interval.toMillis(), maxInterval.dividedBy(4).toMillis());
- return now.plusMillis(millis);
- } else {
- // unknown case
- return now.plus(maxInterval);
- }
- }
+ long daysSinceLastPublication = ChronoUnit.DAYS.between(publishedDate, now);
+ if (daysSinceLastPublication >= 30) {
+ return now.plus(maxInterval);
+ } else if (daysSinceLastPublication >= 14) {
+ return now.plus(maxInterval.dividedBy(2));
+ } else if (daysSinceLastPublication >= 7) {
+ return now.plus(maxInterval.dividedBy(4));
+ } else if (averageEntryInterval != null) {
+ // use average time between entries to decide when to refresh next, divided by factor
+ int factor = 2;
+ long millis =
+ Longs.constrainToRange(
+ averageEntryInterval / factor,
+ interval.toMillis(),
+ maxInterval.dividedBy(4).toMillis());
+ return now.plusMillis(millis);
+ } else {
+ // unknown case
+ return now.plus(maxInterval);
+ }
+ }
- private Instant constrainToBounds(Instant instant) {
- return ObjectUtils.max(ObjectUtils.min(instant, instantSource.instant().plus(maxInterval)), instantSource.instant().plus(interval));
- }
+ private Instant constrainToBounds(Instant instant) {
+ return ObjectUtils.max(
+ ObjectUtils.min(instant, instantSource.instant().plus(maxInterval)),
+ instantSource.instant().plus(interval));
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
index 9d6fd7ec..f0c48478 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
@@ -1,20 +1,5 @@
package com.commafeed.backend.feed;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Lock;
-
-import jakarta.inject.Singleton;
-
-import org.apache.commons.lang3.StringUtils;
-
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.Digests;
@@ -29,142 +14,171 @@ import com.commafeed.backend.model.Models;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.google.common.util.concurrent.Striped;
-
+import jakarta.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
-/**
- * Updates the feed in the database and inserts new entries
- */
+/** Updates the feed in the database and inserts new entries */
@Slf4j
@Singleton
public class FeedRefreshUpdater {
- private final UnitOfWork unitOfWork;
- private final FeedService feedService;
- private final FeedEntryService feedEntryService;
- private final FeedSubscriptionDAO feedSubscriptionDAO;
+ private final UnitOfWork unitOfWork;
+ private final FeedService feedService;
+ private final FeedEntryService feedEntryService;
+ private final FeedSubscriptionDAO feedSubscriptionDAO;
- private final Striped locks;
+ private final Striped locks;
- private final Meter feedUpdated;
- private final Meter entryInserted;
+ private final Meter feedUpdated;
+ private final Meter entryInserted;
- public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics,
- FeedSubscriptionDAO feedSubscriptionDAO) {
- this.unitOfWork = unitOfWork;
- this.feedService = feedService;
- this.feedEntryService = feedEntryService;
- this.feedSubscriptionDAO = feedSubscriptionDAO;
+ public FeedRefreshUpdater(
+ UnitOfWork unitOfWork,
+ FeedService feedService,
+ FeedEntryService feedEntryService,
+ MetricRegistry metrics,
+ FeedSubscriptionDAO feedSubscriptionDAO) {
+ this.unitOfWork = unitOfWork;
+ this.feedService = feedService;
+ this.feedEntryService = feedEntryService;
+ this.feedSubscriptionDAO = feedSubscriptionDAO;
- locks = Striped.lazyWeakLock(100000);
+ locks = Striped.lazyWeakLock(100000);
- feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
- entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
- }
+ feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
+ entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
+ }
- private AddEntryResult addEntry(final Feed feed, final Entry entry, final List subscriptions) {
- boolean processed = false;
- FeedEntry insertedEntry = null;
- Set subscriptionsForWhichEntryIsUnread = new HashSet<>();
+ private AddEntryResult addEntry(
+ final Feed feed, final Entry entry, final List subscriptions) {
+ boolean processed = false;
+ FeedEntry insertedEntry = null;
+ Set subscriptionsForWhichEntryIsUnread = new HashSet<>();
- // lock on feed, make sure we are not updating the same feed twice at
- // the same time
- String key1 = StringUtils.trimToEmpty(String.valueOf(feed.getId()));
+ // lock on feed, make sure we are not updating the same feed twice at
+ // the same time
+ String key1 = StringUtils.trimToEmpty(String.valueOf(feed.getId()));
- // lock on content, make sure we are not updating the same entry
- // twice at the same time
- Content content = entry.content();
- String key2 = Digests.sha1Hex(StringUtils.trimToEmpty(content.content() + content.title()));
+ // lock on content, make sure we are not updating the same entry
+ // twice at the same time
+ Content content = entry.content();
+ String key2 = Digests.sha1Hex(StringUtils.trimToEmpty(content.content() + content.title()));
- Iterator iterator = locks.bulkGet(Arrays.asList(key1, key2)).iterator();
- Lock lock1 = iterator.next();
- Lock lock2 = iterator.next();
- boolean locked1 = false;
- boolean locked2 = false;
- try {
- // try to lock, give up after 1 minute
- locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
- locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
- if (locked1 && locked2) {
- processed = true;
- insertedEntry = unitOfWork.call(() -> {
- if (feedEntryService.find(feed, entry) != null) {
- // entry already exists, nothing to do
- return null;
- }
+ Iterator iterator = locks.bulkGet(Arrays.asList(key1, key2)).iterator();
+ Lock lock1 = iterator.next();
+ Lock lock2 = iterator.next();
+ boolean locked1 = false;
+ boolean locked2 = false;
+ try {
+ // try to lock, give up after 1 minute
+ locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
+ locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
+ if (locked1 && locked2) {
+ processed = true;
+ insertedEntry =
+ unitOfWork.call(
+ () -> {
+ if (feedEntryService.find(feed, entry) != null) {
+ // entry already exists, nothing to do
+ return null;
+ }
- FeedEntry feedEntry = feedEntryService.create(feed, entry);
- entryInserted.mark();
- for (FeedSubscription sub : subscriptions) {
- boolean unread = feedEntryService.applyFilter(sub, feedEntry);
- if (unread) {
- subscriptionsForWhichEntryIsUnread.add(sub);
- }
- }
- return feedEntry;
- });
- } else {
- log.error("lock timeout for {} - {}", feed.getUrl(), key1);
- }
- } catch (InterruptedException e) {
- log.error("interrupted while waiting for lock for {} : {}", feed.getUrl(), e.getMessage(), e);
- Thread.currentThread().interrupt();
- } finally {
- if (locked1) {
- lock1.unlock();
- }
- if (locked2) {
- lock2.unlock();
- }
- }
- return new AddEntryResult(processed, insertedEntry, subscriptionsForWhichEntryIsUnread);
- }
+ FeedEntry feedEntry = feedEntryService.create(feed, entry);
+ entryInserted.mark();
+ for (FeedSubscription sub : subscriptions) {
+ boolean unread =
+ feedEntryService.applyFilter(sub, feedEntry);
+ if (unread) {
+ subscriptionsForWhichEntryIsUnread.add(sub);
+ }
+ }
+ return feedEntry;
+ });
+ } else {
+ log.error("lock timeout for {} - {}", feed.getUrl(), key1);
+ }
+ } catch (InterruptedException e) {
+ log.error(
+ "interrupted while waiting for lock for {} : {}",
+ feed.getUrl(),
+ e.getMessage(),
+ e);
+ Thread.currentThread().interrupt();
+ } finally {
+ if (locked1) {
+ lock1.unlock();
+ }
+ if (locked2) {
+ lock2.unlock();
+ }
+ }
+ return new AddEntryResult(processed, insertedEntry, subscriptionsForWhichEntryIsUnread);
+ }
- public FeedRefreshUpdaterResult update(Feed feed, List entries) {
- boolean processed = true;
- long inserted = 0;
- Map> insertedUnreadEntriesBySubscription = new HashMap<>();
+ public FeedRefreshUpdaterResult update(Feed feed, List entries) {
+ boolean processed = true;
+ long inserted = 0;
+ Map> insertedUnreadEntriesBySubscription =
+ new HashMap<>();
- if (!entries.isEmpty()) {
- List subscriptions = null;
- List newEntries = unitOfWork.call(() -> feedEntryService.removeExistingEntries(feed, entries));
- for (Entry entry : newEntries) {
- if (subscriptions == null) {
- subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
- }
- AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
- processed &= addEntryResult.processed;
- inserted += addEntryResult.insertedEntry != null ? 1 : 0;
- addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> {
- if (addEntryResult.insertedEntry != null) {
- insertedUnreadEntriesBySubscription.computeIfAbsent(sub, k -> new ArrayList<>()).add(addEntryResult.insertedEntry);
- }
- });
- }
+ if (!entries.isEmpty()) {
+ List subscriptions = null;
+ List newEntries =
+ unitOfWork.call(() -> feedEntryService.removeExistingEntries(feed, entries));
+ for (Entry entry : newEntries) {
+ if (subscriptions == null) {
+ subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
+ }
+ AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
+ processed &= addEntryResult.processed;
+ inserted += addEntryResult.insertedEntry != null ? 1 : 0;
+ addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(
+ sub -> {
+ if (addEntryResult.insertedEntry != null) {
+ insertedUnreadEntriesBySubscription
+ .computeIfAbsent(sub, k -> new ArrayList<>())
+ .add(addEntryResult.insertedEntry);
+ }
+ });
+ }
- if (inserted == 0) {
- feed.setMessage("No new entries found");
- } else if (inserted > 0) {
- feed.setMessage("Found %s new entries".formatted(inserted));
- }
- }
+ if (inserted == 0) {
+ feed.setMessage("No new entries found");
+ } else if (inserted > 0) {
+ feed.setMessage("Found %s new entries".formatted(inserted));
+ }
+ }
- if (!processed) {
- // requeue asap
- feed.setDisabledUntil(Models.MINIMUM_INSTANT);
- }
+ if (!processed) {
+ // requeue asap
+ feed.setDisabledUntil(Models.MINIMUM_INSTANT);
+ }
- if (inserted > 0) {
- feedUpdated.mark();
- }
+ if (inserted > 0) {
+ feedUpdated.mark();
+ }
- unitOfWork.run(() -> feedService.update(feed));
+ unitOfWork.run(() -> feedService.update(feed));
- return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription);
- }
+ return new FeedRefreshUpdaterResult(insertedUnreadEntriesBySubscription);
+ }
- private record AddEntryResult(boolean processed, FeedEntry insertedEntry, Set subscriptionsForWhichEntryIsUnread) {}
-
- public record FeedRefreshUpdaterResult(Map> insertedUnreadEntriesBySubscription) {}
+ private record AddEntryResult(
+ boolean processed,
+ FeedEntry insertedEntry,
+ Set subscriptionsForWhichEntryIsUnread) {}
+ public record FeedRefreshUpdaterResult(
+ Map> insertedUnreadEntriesBySubscription) {}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java
index 12348f59..c94fb01c 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java
@@ -1,15 +1,5 @@
package com.commafeed.backend.feed;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-import jakarta.inject.Singleton;
-
-import org.apache.commons.lang3.Strings;
-
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
@@ -18,108 +8,132 @@ import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed;
-
+import jakarta.inject.Singleton;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.Strings;
/**
- * Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link FeedRefreshUpdater} does that)
+ * Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link
+ * FeedRefreshUpdater} does that)
*/
@Slf4j
@Singleton
public class FeedRefreshWorker {
- private final FeedRefreshIntervalCalculator refreshIntervalCalculator;
- private final FeedFetcher fetcher;
- private final CommaFeedConfiguration config;
- private final Meter feedFetched;
+ private final FeedRefreshIntervalCalculator refreshIntervalCalculator;
+ private final FeedFetcher fetcher;
+ private final CommaFeedConfiguration config;
+ private final Meter feedFetched;
- public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config,
- MetricRegistry metrics) {
- this.refreshIntervalCalculator = refreshIntervalCalculator;
- this.fetcher = fetcher;
- this.config = config;
- this.feedFetched = metrics.meter(MetricRegistry.name(getClass(), "feedFetched"));
+ public FeedRefreshWorker(
+ FeedRefreshIntervalCalculator refreshIntervalCalculator,
+ FeedFetcher fetcher,
+ CommaFeedConfiguration config,
+ MetricRegistry metrics) {
+ this.refreshIntervalCalculator = refreshIntervalCalculator;
+ this.fetcher = fetcher;
+ this.config = config;
+ this.feedFetched = metrics.meter(MetricRegistry.name(getClass(), "feedFetched"));
+ }
- }
+ public FeedRefreshWorkerResult update(Feed feed) {
+ try {
+ String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
+ FeedFetcherResult result =
+ fetcher.fetch(
+ url,
+ false,
+ feed.getLastModifiedHeader(),
+ feed.getEtagHeader(),
+ feed.getLastPublishedDate(),
+ feed.getLastContentHash());
+ // stops here if NotModifiedException or any other exception is thrown
- public FeedRefreshWorkerResult update(Feed feed) {
- try {
- String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
- FeedFetcherResult result = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
- feed.getLastPublishedDate(), feed.getLastContentHash());
- // stops here if NotModifiedException or any other exception is thrown
+ List entries = result.feed().entries();
- List entries = result.feed().entries();
+ int maxFeedCapacity = config.database().cleanup().maxFeedCapacity();
+ if (maxFeedCapacity > 0) {
+ entries = entries.stream().limit(maxFeedCapacity).toList();
+ }
- int maxFeedCapacity = config.database().cleanup().maxFeedCapacity();
- if (maxFeedCapacity > 0) {
- entries = entries.stream().limit(maxFeedCapacity).toList();
- }
+ Duration entriesMaxAge = config.database().cleanup().entriesMaxAge();
+ if (!entriesMaxAge.isZero()) {
+ Instant threshold = Instant.now().minus(entriesMaxAge);
+ entries =
+ entries.stream()
+ .filter(entry -> entry.published().isAfter(threshold))
+ .toList();
+ }
- Duration entriesMaxAge = config.database().cleanup().entriesMaxAge();
- if (!entriesMaxAge.isZero()) {
- Instant threshold = Instant.now().minus(entriesMaxAge);
- entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList();
- }
+ String urlAfterRedirect = result.urlAfterRedirect();
+ if (Strings.CS.equals(url, urlAfterRedirect)) {
+ urlAfterRedirect = null;
+ }
- String urlAfterRedirect = result.urlAfterRedirect();
- if (Strings.CS.equals(url, urlAfterRedirect)) {
- urlAfterRedirect = null;
- }
+ feed.setUrlAfterRedirect(urlAfterRedirect);
+ feed.setLink(result.feed().link());
+ feed.setIconUrl(result.feed().iconUrl());
+ feed.setLastModifiedHeader(result.lastModifiedHeader());
+ feed.setEtagHeader(result.lastETagHeader());
+ feed.setLastContentHash(result.contentHash());
+ feed.setLastPublishedDate(result.feed().lastPublishedDate());
+ feed.setAverageEntryInterval(result.feed().averageEntryInterval());
+ feed.setLastEntryDate(result.feed().lastEntryDate());
- feed.setUrlAfterRedirect(urlAfterRedirect);
- feed.setLink(result.feed().link());
- feed.setIconUrl(result.feed().iconUrl());
- feed.setLastModifiedHeader(result.lastModifiedHeader());
- feed.setEtagHeader(result.lastETagHeader());
- feed.setLastContentHash(result.contentHash());
- feed.setLastPublishedDate(result.feed().lastPublishedDate());
- feed.setAverageEntryInterval(result.feed().averageEntryInterval());
- feed.setLastEntryDate(result.feed().lastEntryDate());
+ feed.setErrorCount(0);
+ feed.setMessage(null);
+ feed.setDisabledUntil(
+ refreshIntervalCalculator.onFetchSuccess(
+ result.feed().lastPublishedDate(),
+ result.feed().averageEntryInterval(),
+ result.validFor()));
- feed.setErrorCount(0);
- feed.setMessage(null);
- feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(),
- result.feed().averageEntryInterval(), result.validFor()));
+ return new FeedRefreshWorkerResult(feed, entries);
+ } catch (NotModifiedException e) {
+ log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
- return new FeedRefreshWorkerResult(feed, entries);
- } catch (NotModifiedException e) {
- log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
+ feed.setErrorCount(0);
+ feed.setMessage(e.getMessage());
+ feed.setDisabledUntil(
+ refreshIntervalCalculator.onFeedNotModified(
+ feed.getLastPublishedDate(), feed.getAverageEntryInterval()));
- feed.setErrorCount(0);
- feed.setMessage(e.getMessage());
- feed.setDisabledUntil(refreshIntervalCalculator.onFeedNotModified(feed.getLastPublishedDate(), feed.getAverageEntryInterval()));
+ if (e.getNewLastModifiedHeader() != null) {
+ feed.setLastModifiedHeader(e.getNewLastModifiedHeader());
+ }
- if (e.getNewLastModifiedHeader() != null) {
- feed.setLastModifiedHeader(e.getNewLastModifiedHeader());
- }
+ if (e.getNewEtagHeader() != null) {
+ feed.setEtagHeader(e.getNewEtagHeader());
+ }
- if (e.getNewEtagHeader() != null) {
- feed.setEtagHeader(e.getNewEtagHeader());
- }
+ return new FeedRefreshWorkerResult(feed, Collections.emptyList());
+ } catch (TooManyRequestsException e) {
+ log.debug("Too many requests : {}", feed.getUrl());
- return new FeedRefreshWorkerResult(feed, Collections.emptyList());
- } catch (TooManyRequestsException e) {
- log.debug("Too many requests : {}", feed.getUrl());
+ feed.setErrorCount(feed.getErrorCount() + 1);
+ feed.setMessage("Server indicated that we are sending too many requests");
+ feed.setDisabledUntil(
+ refreshIntervalCalculator.onTooManyRequests(
+ e.getRetryAfter(), feed.getErrorCount()));
- feed.setErrorCount(feed.getErrorCount() + 1);
- feed.setMessage("Server indicated that we are sending too many requests");
- feed.setDisabledUntil(refreshIntervalCalculator.onTooManyRequests(e.getRetryAfter(), feed.getErrorCount()));
+ return new FeedRefreshWorkerResult(feed, Collections.emptyList());
+ } catch (Exception e) {
+ log.debug("unable to refresh feed {}", feed.getUrl(), e);
- return new FeedRefreshWorkerResult(feed, Collections.emptyList());
- } catch (Exception e) {
- log.debug("unable to refresh feed {}", feed.getUrl(), e);
+ feed.setErrorCount(feed.getErrorCount() + 1);
+ feed.setMessage("Unable to refresh feed : " + e.getMessage());
+ feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed.getErrorCount()));
- feed.setErrorCount(feed.getErrorCount() + 1);
- feed.setMessage("Unable to refresh feed : " + e.getMessage());
- feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed.getErrorCount()));
-
- return new FeedRefreshWorkerResult(feed, Collections.emptyList());
- } finally {
- feedFetched.mark();
- }
- }
-
- public record FeedRefreshWorkerResult(Feed feed, List entries) {}
+ return new FeedRefreshWorkerResult(feed, Collections.emptyList());
+ } finally {
+ feedFetched.mark();
+ }
+ }
+ public record FeedRefreshWorkerResult(Feed feed, List entries) {}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java
index a5567bc2..f4be5c88 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUpdateNotifier.java
@@ -1,9 +1,5 @@
package com.commafeed.backend.feed;
-import java.util.List;
-
-import jakarta.inject.Singleton;
-
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO;
@@ -13,7 +9,8 @@ import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.PushNotificationService;
import com.commafeed.frontend.ws.WebSocketMessageBuilder;
import com.commafeed.frontend.ws.WebSocketSessions;
-
+import jakarta.inject.Singleton;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -22,29 +19,33 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class FeedUpdateNotifier {
- private final CommaFeedConfiguration config;
- private final UnitOfWork unitOfWork;
- private final UserSettingsDAO userSettingsDAO;
- private final WebSocketSessions webSocketSessions;
- private final PushNotificationService pushNotificationService;
+ private final CommaFeedConfiguration config;
+ private final UnitOfWork unitOfWork;
+ private final UserSettingsDAO userSettingsDAO;
+ private final WebSocketSessions webSocketSessions;
+ private final PushNotificationService pushNotificationService;
- public void notifyOverWebsocket(FeedSubscription sub, List entries) {
- if (!entries.isEmpty()) {
- webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
- }
- }
+ public void notifyOverWebsocket(FeedSubscription sub, List entries) {
+ if (!entries.isEmpty()) {
+ webSocketSessions.sendMessage(
+ sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, entries.size()));
+ }
+ }
- public void sendPushNotifications(FeedSubscription sub, List entries) {
- if (!config.pushNotifications().enabled() || !sub.isPushNotificationsEnabled() || entries.isEmpty()) {
- return;
- }
-
- UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(sub.getUser()));
- if (settings != null && settings.getPushNotifications() != null && settings.getPushNotifications().getType() != null) {
- for (FeedEntry entry : entries) {
- pushNotificationService.notify(settings.getPushNotifications(), sub, entry);
- }
- }
- }
+ public void sendPushNotifications(FeedSubscription sub, List entries) {
+ if (!config.pushNotifications().enabled()
+ || !sub.isPushNotificationsEnabled()
+ || entries.isEmpty()) {
+ return;
+ }
+ UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(sub.getUser()));
+ if (settings != null
+ && settings.getPushNotifications() != null
+ && settings.getPushNotifications().getType() != null) {
+ for (FeedEntry entry : entries) {
+ pushNotificationService.notify(settings.getPushNotifications(), sub, entry);
+ }
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUtils.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUtils.java
index 6babc0c0..2e002343 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUtils.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedUtils.java
@@ -1,14 +1,5 @@
package com.commafeed.backend.feed;
-import java.util.Collections;
-import java.util.Date;
-
-import org.apache.commons.lang3.StringUtils;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
import com.commafeed.backend.feed.parser.TextDirectionDetector;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.frontend.model.Entry;
@@ -16,87 +7,90 @@ import com.rometools.rome.feed.synd.SyndContentImpl;
import com.rometools.rome.feed.synd.SyndEnclosureImpl;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl;
-
+import java.util.Collections;
+import java.util.Date;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
-/**
- * Utility methods related to feed handling
- *
- */
+/** Utility methods related to feed handling */
@UtilityClass
@Slf4j
public class FeedUtils {
- public static String truncate(String string, int length) {
- return StringUtils.truncate(string, length);
- }
+ public static String truncate(String string, int length) {
+ return StringUtils.truncate(string, length);
+ }
- public static boolean isRTL(String title, String content) {
- String text = StringUtils.isNotBlank(content) ? content : title;
- if (StringUtils.isBlank(text)) {
- return false;
- }
+ public static boolean isRTL(String title, String content) {
+ String text = StringUtils.isNotBlank(content) ? content : title;
+ if (StringUtils.isBlank(text)) {
+ return false;
+ }
- String stripped = Jsoup.parse(text).text();
- if (StringUtils.isBlank(stripped)) {
- return false;
- }
+ String stripped = Jsoup.parse(text).text();
+ if (StringUtils.isBlank(stripped)) {
+ return false;
+ }
- return TextDirectionDetector.detect(stripped) == TextDirectionDetector.Direction.RIGHT_TO_LEFT;
- }
+ return TextDirectionDetector.detect(stripped)
+ == TextDirectionDetector.Direction.RIGHT_TO_LEFT;
+ }
- public static String getFaviconUrl(FeedSubscription subscription) {
- return "rest/feed/favicon/" + subscription.getId();
- }
+ public static String getFaviconUrl(FeedSubscription subscription) {
+ return "rest/feed/favicon/" + subscription.getId();
+ }
- public static String proxyImages(String content) {
- if (StringUtils.isBlank(content)) {
- return content;
- }
+ public static String proxyImages(String content) {
+ if (StringUtils.isBlank(content)) {
+ return content;
+ }
- Document doc = Jsoup.parse(content);
- Elements elements = doc.select("img");
- for (Element element : elements) {
- String href = element.attr("src");
- if (StringUtils.isNotBlank(href)) {
- String proxy = proxyImage(href);
- element.attr("src", proxy);
- }
- }
+ Document doc = Jsoup.parse(content);
+ Elements elements = doc.select("img");
+ for (Element element : elements) {
+ String href = element.attr("src");
+ if (StringUtils.isNotBlank(href)) {
+ String proxy = proxyImage(href);
+ element.attr("src", proxy);
+ }
+ }
- return doc.body().html();
- }
+ return doc.body().html();
+ }
- public static String proxyImage(String url) {
- if (StringUtils.isBlank(url)) {
- return url;
- }
+ public static String proxyImage(String url) {
+ if (StringUtils.isBlank(url)) {
+ return url;
+ }
- return "rest/server/proxy?u=" + ImageProxyUrl.encode(url);
- }
+ return "rest/server/proxy?u=" + ImageProxyUrl.encode(url);
+ }
- public static SyndEntry asRss(Entry entry) {
- SyndEntry e = new SyndEntryImpl();
+ public static SyndEntry asRss(Entry entry) {
+ SyndEntry e = new SyndEntryImpl();
- e.setUri(entry.getGuid());
- e.setTitle(entry.getTitle());
- e.setAuthor(entry.getAuthor());
+ e.setUri(entry.getGuid());
+ e.setTitle(entry.getTitle());
+ e.setAuthor(entry.getAuthor());
- SyndContentImpl c = new SyndContentImpl();
- c.setValue(entry.getContent());
- e.setContents(Collections.singletonList(c));
+ SyndContentImpl c = new SyndContentImpl();
+ c.setValue(entry.getContent());
+ e.setContents(Collections.singletonList(c));
- if (entry.getEnclosureUrl() != null) {
- SyndEnclosureImpl enclosure = new SyndEnclosureImpl();
- enclosure.setType(entry.getEnclosureType());
- enclosure.setUrl(entry.getEnclosureUrl());
- e.setEnclosures(Collections.singletonList(enclosure));
- }
-
- e.setLink(entry.getUrl());
- e.setPublishedDate(entry.getDate() == null ? null : Date.from(entry.getDate()));
- return e;
- }
+ if (entry.getEnclosureUrl() != null) {
+ SyndEnclosureImpl enclosure = new SyndEnclosureImpl();
+ enclosure.setType(entry.getEnclosureType());
+ enclosure.setUrl(entry.getEnclosureUrl());
+ e.setEnclosures(Collections.singletonList(enclosure));
+ }
+ e.setLink(entry.getUrl());
+ e.setPublishedDate(entry.getDate() == null ? null : Date.from(entry.getDate()));
+ return e;
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/ImageProxyUrl.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/ImageProxyUrl.java
index d36cfbc1..dacee7bb 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/ImageProxyUrl.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/ImageProxyUrl.java
@@ -1,70 +1,65 @@
package com.commafeed.backend.feed;
+import com.google.common.primitives.Bytes;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
-
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
-
-import org.apache.commons.lang3.RandomUtils;
-
-import com.google.common.primitives.Bytes;
-
import lombok.experimental.UtilityClass;
+import org.apache.commons.lang3.RandomUtils;
@UtilityClass
public class ImageProxyUrl {
- private static final int GCM_IV_LENGTH = 12;
- private static final int GCM_TAG_LENGTH = 128;
+ private static final int GCM_IV_LENGTH = 12;
+ private static final int GCM_TAG_LENGTH = 128;
- private static SecretKey key;
+ private static SecretKey key;
- public static void generateKey() {
- key = new SecretKeySpec(RandomUtils.secure().randomBytes(32), "AES");
- }
+ public static void generateKey() {
+ key = new SecretKeySpec(RandomUtils.secure().randomBytes(32), "AES");
+ }
- public static String encode(String url) {
- if (key == null) {
- throw new IllegalStateException("Key not initialized");
- }
+ public static String encode(String url) {
+ if (key == null) {
+ throw new IllegalStateException("Key not initialized");
+ }
- try {
- byte[] iv = RandomUtils.secure().randomBytes(GCM_IV_LENGTH);
+ try {
+ byte[] iv = RandomUtils.secure().randomBytes(GCM_IV_LENGTH);
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
- cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
- byte[] encrypted = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
+ byte[] encrypted = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
- byte[] combined = Bytes.concat(iv, encrypted);
- return Base64.getUrlEncoder().withoutPadding().encodeToString(combined);
- } catch (Exception e) {
- throw new IllegalStateException("Failed to encode URL", e);
- }
- }
+ byte[] combined = Bytes.concat(iv, encrypted);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(combined);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to encode URL", e);
+ }
+ }
- public static String decode(String code) {
- if (key == null) {
- throw new IllegalStateException("Key not initialized");
- }
+ public static String decode(String code) {
+ if (key == null) {
+ throw new IllegalStateException("Key not initialized");
+ }
- try {
- byte[] combined = Base64.getUrlDecoder().decode(code);
+ try {
+ byte[] combined = Base64.getUrlDecoder().decode(code);
- byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
- byte[] encrypted = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
+ byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
+ byte[] encrypted = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
- cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
- byte[] decrypted = cipher.doFinal(encrypted);
-
- return new String(decrypted, StandardCharsets.UTF_8);
- } catch (Exception e) {
- throw new IllegalStateException("Failed to decode URL", e);
- }
- }
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
+ byte[] decrypted = cipher.doFinal(encrypted);
+ return new String(decrypted, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to decode URL", e);
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/EncodingDetector.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/EncodingDetector.java
index c6b46d28..e2f727f5 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/EncodingDetector.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/EncodingDetector.java
@@ -1,70 +1,61 @@
package com.commafeed.backend.feed.parser;
-import java.nio.charset.Charset;
-
-import jakarta.inject.Singleton;
-
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.Strings;
-
import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch;
+import jakarta.inject.Singleton;
+import java.nio.charset.Charset;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.Strings;
@Singleton
public class EncodingDetector {
- /**
- * Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
- * feed
- *
- */
- public Charset getEncoding(byte[] bytes) {
- String extracted = extractDeclaredEncoding(bytes);
- if (Strings.CI.startsWith(extracted, "iso-8859-")) {
- if (!Strings.CS.endsWith(extracted, "1")) {
- return Charset.forName(extracted);
- }
- } else if (Strings.CI.startsWith(extracted, "windows-")) {
- return Charset.forName(extracted);
- }
- return detectEncoding(bytes);
- }
+ /**
+ * Detect feed encoding by using the declared encoding in the xml processing instruction and by
+ * detecting the characters used in the feed
+ */
+ public Charset getEncoding(byte[] bytes) {
+ String extracted = extractDeclaredEncoding(bytes);
+ if (Strings.CI.startsWith(extracted, "iso-8859-")) {
+ if (!Strings.CS.endsWith(extracted, "1")) {
+ return Charset.forName(extracted);
+ }
+ } else if (Strings.CI.startsWith(extracted, "windows-")) {
+ return Charset.forName(extracted);
+ }
+ return detectEncoding(bytes);
+ }
- /**
- * Extract the declared encoding from the xml
- */
- public String extractDeclaredEncoding(byte[] bytes) {
- int index = ArrayUtils.indexOf(bytes, (byte) '>');
- if (index == -1) {
- return null;
- }
+ /** Extract the declared encoding from the xml */
+ public String extractDeclaredEncoding(byte[] bytes) {
+ int index = ArrayUtils.indexOf(bytes, (byte) '>');
+ if (index == -1) {
+ return null;
+ }
- String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)).replace('\'', '"');
- index = Strings.CS.indexOf(pi, "encoding=\"");
- if (index == -1) {
- return null;
- }
- String encoding = pi.substring(index + 10);
- encoding = encoding.substring(0, encoding.indexOf('"'));
- return encoding;
- }
+ String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)).replace('\'', '"');
+ index = Strings.CS.indexOf(pi, "encoding=\"");
+ if (index == -1) {
+ return null;
+ }
+ String encoding = pi.substring(index + 10);
+ encoding = encoding.substring(0, encoding.indexOf('"'));
+ return encoding;
+ }
- /**
- * Detect encoding by analyzing characters in the array
- */
- private Charset detectEncoding(byte[] bytes) {
- String encoding = "UTF-8";
-
- CharsetDetector detector = new CharsetDetector();
- detector.setText(bytes);
- CharsetMatch match = detector.detect();
- if (match != null) {
- encoding = match.getName();
- }
- if (encoding.equalsIgnoreCase("ISO-8859-1")) {
- encoding = "windows-1252";
- }
- return Charset.forName(encoding);
- }
+ /** Detect encoding by analyzing characters in the array */
+ private Charset detectEncoding(byte[] bytes) {
+ String encoding = "UTF-8";
+ CharsetDetector detector = new CharsetDetector();
+ detector.setText(bytes);
+ CharsetMatch match = detector.detect();
+ if (match != null) {
+ encoding = match.getName();
+ }
+ if (encoding.equalsIgnoreCase("ISO-8859-1")) {
+ encoding = "windows-1252";
+ }
+ return Charset.forName(encoding);
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java
index ac626296..9304a180 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java
@@ -1,25 +1,5 @@
package com.commafeed.backend.feed.parser;
-import java.io.StringReader;
-import java.nio.charset.Charset;
-import java.text.DateFormat;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import jakarta.inject.Singleton;
-
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.SystemProperties;
-import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
-import org.jdom2.Element;
-import org.jdom2.Namespace;
-import org.xml.sax.InputSource;
-
import com.commafeed.backend.Urls;
import com.commafeed.backend.feed.parser.FeedParserResult.Content;
import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure;
@@ -38,258 +18,290 @@ import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.feed.synd.SyndLink;
import com.rometools.rome.feed.synd.SyndLinkImpl;
import com.rometools.rome.io.SyndFeedInput;
+import jakarta.inject.Singleton;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.SystemProperties;
+import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
+import org.jdom2.Element;
+import org.jdom2.Namespace;
+import org.xml.sax.InputSource;
-/**
- * Parses raw xml into a FeedParserResult object
- */
+/** Parses raw xml into a FeedParserResult object */
@Singleton
public class FeedParser {
- private static final Namespace ATOM_10_NS = Namespace.getNamespace("http://www.w3.org/2005/Atom");
+ private static final Namespace ATOM_10_NS =
+ Namespace.getNamespace("http://www.w3.org/2005/Atom");
- private static final Instant START = Instant.ofEpochMilli(86400000);
- private static final Instant END = Instant.ofEpochMilli(1000L * Integer.MAX_VALUE - 86400000);
+ private static final Instant START = Instant.ofEpochMilli(86400000);
+ private static final Instant END = Instant.ofEpochMilli(1000L * Integer.MAX_VALUE - 86400000);
- private static final Comparator ENTRY_COMPARATOR = Comparator.comparing(Entry::published).reversed();
+ private static final Comparator ENTRY_COMPARATOR =
+ Comparator.comparing(Entry::published).reversed();
- private final EncodingDetector encodingDetector;
- private final XMLCleaner xmlCleaner;
+ private final EncodingDetector encodingDetector;
+ private final XMLCleaner xmlCleaner;
- public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) {
- this.encodingDetector = encodingDetector;
- this.xmlCleaner = xmlCleaner;
+ public FeedParser(EncodingDetector encodingDetector, XMLCleaner xmlCleaner) {
+ this.encodingDetector = encodingDetector;
+ this.xmlCleaner = xmlCleaner;
- // disable entity expansion limits added in JDK24+ (#1961)
- // we already strip doctype declarations in XMLCleaner to prevent xxe attacks
- // we also already limit the size of feeds we download in HttpGetter
- System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
- System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
- }
+ // disable entity expansion limits added in JDK24+ (#1961)
+ // we already strip doctype declarations in XMLCleaner to prevent xxe attacks
+ // we also already limit the size of feeds we download in HttpGetter
+ System.setProperty(SystemProperties.JDK_XML_MAX_GENERAL_ENTITY_SIZE_LIMIT, "0");
+ System.setProperty(SystemProperties.JDK_XML_TOTAL_ENTITY_SIZE_LIMIT, "0");
+ }
- public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedParsingException {
- try {
- Charset encoding = encodingDetector.getEncoding(xml);
+ public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedParsingException {
+ try {
+ Charset encoding = encodingDetector.getEncoding(xml);
- String xmlString = xmlCleaner.clean(new String(xml, encoding));
- if (xmlString == null) {
- throw new FeedParsingException("Input string is empty for url " + feedUrl);
- }
+ String xmlString = xmlCleaner.clean(new String(xml, encoding));
+ if (xmlString == null) {
+ throw new FeedParsingException("Input string is empty for url " + feedUrl);
+ }
- InputSource source = new InputSource(new StringReader(xmlString));
- SyndFeed feed = new SyndFeedInput().build(source);
- handleForeignMarkup(feed);
+ InputSource source = new InputSource(new StringReader(xmlString));
+ SyndFeed feed = new SyndFeedInput().build(source);
+ handleForeignMarkup(feed);
- String title = feed.getTitle();
- String link = feed.getLink();
- String iconUrl = feed.getIcon() != null ? feed.getIcon().getUrl() : null;
- List entries = buildEntries(feed, feedUrl);
- Instant lastEntryDate = entries.stream().findFirst().map(Entry::published).orElse(null);
- Instant lastPublishedDate = toValidInstant(feed.getPublishedDate(), false);
- if (lastPublishedDate == null || lastEntryDate != null && lastPublishedDate.isBefore(lastEntryDate)) {
- lastPublishedDate = lastEntryDate;
- }
- Long averageEntryInterval = averageTimeBetweenEntries(entries);
+ String title = feed.getTitle();
+ String link = feed.getLink();
+ String iconUrl = feed.getIcon() != null ? feed.getIcon().getUrl() : null;
+ List entries = buildEntries(feed, feedUrl);
+ Instant lastEntryDate = entries.stream().findFirst().map(Entry::published).orElse(null);
+ Instant lastPublishedDate = toValidInstant(feed.getPublishedDate(), false);
+ if (lastPublishedDate == null
+ || lastEntryDate != null && lastPublishedDate.isBefore(lastEntryDate)) {
+ lastPublishedDate = lastEntryDate;
+ }
+ Long averageEntryInterval = averageTimeBetweenEntries(entries);
- return new FeedParserResult(title, link, iconUrl, lastPublishedDate, averageEntryInterval, lastEntryDate, entries);
- } catch (FeedParsingException e) {
- throw e;
- } catch (Exception e) {
- throw new FeedParsingException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
- }
- }
+ return new FeedParserResult(
+ title,
+ link,
+ iconUrl,
+ lastPublishedDate,
+ averageEntryInterval,
+ lastEntryDate,
+ entries);
+ } catch (FeedParsingException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new FeedParsingException(
+ String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
+ }
+ }
- /**
- * Adds atom links for rss feeds
- */
- private void handleForeignMarkup(SyndFeed feed) {
- List foreignMarkup = feed.getForeignMarkup();
- if (foreignMarkup == null) {
- return;
- }
- for (Element element : foreignMarkup) {
- if ("link".equals(element.getName()) && ATOM_10_NS.equals(element.getNamespace())) {
- SyndLink link = new SyndLinkImpl();
- link.setRel(element.getAttributeValue("rel"));
- link.setHref(element.getAttributeValue("href"));
- feed.getLinks().add(link);
- }
- }
- }
+ /** Adds atom links for rss feeds */
+ private void handleForeignMarkup(SyndFeed feed) {
+ List foreignMarkup = feed.getForeignMarkup();
+ if (foreignMarkup == null) {
+ return;
+ }
+ for (Element element : foreignMarkup) {
+ if ("link".equals(element.getName()) && ATOM_10_NS.equals(element.getNamespace())) {
+ SyndLink link = new SyndLinkImpl();
+ link.setRel(element.getAttributeValue("rel"));
+ link.setHref(element.getAttributeValue("href"));
+ feed.getLinks().add(link);
+ }
+ }
+ }
- private List buildEntries(SyndFeed feed, String feedUrl) {
- List entries = new ArrayList<>();
+ private List buildEntries(SyndFeed feed, String feedUrl) {
+ List entries = new ArrayList<>();
- for (SyndEntry item : feed.getEntries()) {
- String guid = item.getUri();
- if (StringUtils.isBlank(guid)) {
- guid = item.getLink();
- }
- if (StringUtils.isBlank(guid)) {
- // no guid and no link, skip entry
- continue;
- }
+ for (SyndEntry item : feed.getEntries()) {
+ String guid = item.getUri();
+ if (StringUtils.isBlank(guid)) {
+ guid = item.getLink();
+ }
+ if (StringUtils.isBlank(guid)) {
+ // no guid and no link, skip entry
+ continue;
+ }
- String url = buildEntryUrl(feed, feedUrl, item);
- if (StringUtils.isBlank(url) && Urls.isAbsolute(guid)) {
- // if link is empty but guid is used as url, use guid
- url = guid;
- }
+ String url = buildEntryUrl(feed, feedUrl, item);
+ if (StringUtils.isBlank(url) && Urls.isAbsolute(guid)) {
+ // if link is empty but guid is used as url, use guid
+ url = guid;
+ }
- Instant publishedDate = buildEntryPublishedDate(item);
- Content content = buildContent(item);
+ Instant publishedDate = buildEntryPublishedDate(item);
+ Content content = buildContent(item);
- entries.add(new Entry(guid, url, publishedDate, content));
- }
+ entries.add(new Entry(guid, url, publishedDate, content));
+ }
- entries.sort(ENTRY_COMPARATOR);
- return entries;
- }
+ entries.sort(ENTRY_COMPARATOR);
+ return entries;
+ }
- private Content buildContent(SyndEntry item) {
- String title = getTitle(item);
- String content = getContent(item);
- String author = StringUtils.trimToNull(item.getAuthor());
- String categories = StringUtils
- .trimToNull(item.getCategories().stream().map(SyndCategory::getName).collect(Collectors.joining(", ")));
+ private Content buildContent(SyndEntry item) {
+ String title = getTitle(item);
+ String content = getContent(item);
+ String author = StringUtils.trimToNull(item.getAuthor());
+ String categories =
+ StringUtils.trimToNull(
+ item.getCategories().stream()
+ .map(SyndCategory::getName)
+ .collect(Collectors.joining(", ")));
- Enclosure enclosure = buildEnclosure(item);
- Media media = buildMedia(item);
- return new Content(title, content, author, categories, enclosure, media);
- }
+ Enclosure enclosure = buildEnclosure(item);
+ Media media = buildMedia(item);
+ return new Content(title, content, author, categories, enclosure, media);
+ }
- private Enclosure buildEnclosure(SyndEntry item) {
- SyndEnclosure enclosure = item.getEnclosures().stream().findFirst().orElse(null);
- if (enclosure == null) {
- return null;
- }
+ private Enclosure buildEnclosure(SyndEntry item) {
+ SyndEnclosure enclosure = item.getEnclosures().stream().findFirst().orElse(null);
+ if (enclosure == null) {
+ return null;
+ }
- return new Enclosure(enclosure.getUrl(), enclosure.getType());
- }
+ return new Enclosure(enclosure.getUrl(), enclosure.getType());
+ }
- private Instant buildEntryPublishedDate(SyndEntry item) {
- Date date = item.getPublishedDate();
- if (date == null) {
- date = item.getUpdatedDate();
- }
- return toValidInstant(date, true);
- }
+ private Instant buildEntryPublishedDate(SyndEntry item) {
+ Date date = item.getPublishedDate();
+ if (date == null) {
+ date = item.getUpdatedDate();
+ }
+ return toValidInstant(date, true);
+ }
- private String buildEntryUrl(SyndFeed feed, String feedUrl, SyndEntry item) {
- String url = StringUtils.trimToNull(StringUtils.normalizeSpace(item.getLink()));
- if (url == null || Urls.isAbsolute(url)) {
- // url is absolute, nothing to do
- return url;
- }
+ private String buildEntryUrl(SyndFeed feed, String feedUrl, SyndEntry item) {
+ String url = StringUtils.trimToNull(StringUtils.normalizeSpace(item.getLink()));
+ if (url == null || Urls.isAbsolute(url)) {
+ // url is absolute, nothing to do
+ return url;
+ }
- // url is relative, trying to resolve it
- String feedLink = StringUtils.trimToNull(StringUtils.normalizeSpace(feed.getLink()));
- return Urls.toAbsolute(url, feedLink, feedUrl);
- }
+ // url is relative, trying to resolve it
+ String feedLink = StringUtils.trimToNull(StringUtils.normalizeSpace(feed.getLink()));
+ return Urls.toAbsolute(url, feedLink, feedUrl);
+ }
- private Instant toValidInstant(Date date, boolean nullToNow) {
- Instant now = Instant.now();
- if (date == null) {
- return nullToNow ? now : null;
- }
+ private Instant toValidInstant(Date date, boolean nullToNow) {
+ Instant now = Instant.now();
+ if (date == null) {
+ return nullToNow ? now : null;
+ }
- Instant instant = date.toInstant();
- if (instant.isBefore(START) || instant.isAfter(END)) {
- return now;
- }
+ Instant instant = date.toInstant();
+ if (instant.isBefore(START) || instant.isAfter(END)) {
+ return now;
+ }
- if (instant.isAfter(now)) {
- return now;
- }
- return instant;
- }
+ if (instant.isAfter(now)) {
+ return now;
+ }
+ return instant;
+ }
- private String getContent(SyndEntry item) {
- String content;
- if (item.getContents().isEmpty()) {
- content = item.getDescription() == null ? null : item.getDescription().getValue();
- } else {
- content = item.getContents().stream().map(SyndContent::getValue).collect(Collectors.joining(System.lineSeparator()));
- }
- return StringUtils.trimToNull(content);
- }
+ private String getContent(SyndEntry item) {
+ String content;
+ if (item.getContents().isEmpty()) {
+ content = item.getDescription() == null ? null : item.getDescription().getValue();
+ } else {
+ content =
+ item.getContents().stream()
+ .map(SyndContent::getValue)
+ .collect(Collectors.joining(System.lineSeparator()));
+ }
+ return StringUtils.trimToNull(content);
+ }
- private String getTitle(SyndEntry item) {
- String title = item.getTitle();
- if (StringUtils.isBlank(title)) {
- Date date = item.getPublishedDate();
- if (date != null) {
- title = DateFormat.getInstance().format(date);
- } else {
- title = "(no title)";
- }
- }
- return StringUtils.trimToNull(title);
- }
+ private String getTitle(SyndEntry item) {
+ String title = item.getTitle();
+ if (StringUtils.isBlank(title)) {
+ Date date = item.getPublishedDate();
+ if (date != null) {
+ title = DateFormat.getInstance().format(date);
+ } else {
+ title = "(no title)";
+ }
+ }
+ return StringUtils.trimToNull(title);
+ }
- private Media buildMedia(SyndEntry item) {
- MediaEntryModule module = (MediaEntryModule) item.getModule(MediaModule.URI);
- if (module == null) {
- return null;
- }
+ private Media buildMedia(SyndEntry item) {
+ MediaEntryModule module = (MediaEntryModule) item.getModule(MediaModule.URI);
+ if (module == null) {
+ return null;
+ }
- Media media = buildMedia(module.getMetadata());
- if (media == null && ArrayUtils.isNotEmpty(module.getMediaGroups())) {
- MediaGroup group = module.getMediaGroups()[0];
- media = buildMedia(group.getMetadata());
- }
+ Media media = buildMedia(module.getMetadata());
+ if (media == null && ArrayUtils.isNotEmpty(module.getMediaGroups())) {
+ MediaGroup group = module.getMediaGroups()[0];
+ media = buildMedia(group.getMetadata());
+ }
- return media;
- }
+ return media;
+ }
- private Media buildMedia(Metadata metadata) {
- if (metadata == null) {
- return null;
- }
+ private Media buildMedia(Metadata metadata) {
+ if (metadata == null) {
+ return null;
+ }
- String description = metadata.getDescription();
+ String description = metadata.getDescription();
- String thumbnailUrl = null;
- Integer thumbnailWidth = null;
- Integer thumbnailHeight = null;
- if (ArrayUtils.isNotEmpty(metadata.getThumbnail())) {
- Thumbnail thumbnail = metadata.getThumbnail()[0];
- thumbnailWidth = thumbnail.getWidth();
- thumbnailHeight = thumbnail.getHeight();
- if (thumbnail.getUrl() != null) {
- thumbnailUrl = thumbnail.getUrl().toString();
- }
- }
+ String thumbnailUrl = null;
+ Integer thumbnailWidth = null;
+ Integer thumbnailHeight = null;
+ if (ArrayUtils.isNotEmpty(metadata.getThumbnail())) {
+ Thumbnail thumbnail = metadata.getThumbnail()[0];
+ thumbnailWidth = thumbnail.getWidth();
+ thumbnailHeight = thumbnail.getHeight();
+ if (thumbnail.getUrl() != null) {
+ thumbnailUrl = thumbnail.getUrl().toString();
+ }
+ }
- if (description == null && thumbnailUrl == null) {
- return null;
- }
+ if (description == null && thumbnailUrl == null) {
+ return null;
+ }
- return new Media(description, thumbnailUrl, thumbnailWidth, thumbnailHeight);
- }
+ return new Media(description, thumbnailUrl, thumbnailWidth, thumbnailHeight);
+ }
- private Long averageTimeBetweenEntries(List entries) {
- if (entries.isEmpty() || entries.size() == 1) {
- return null;
- }
+ private Long averageTimeBetweenEntries(List entries) {
+ if (entries.isEmpty() || entries.size() == 1) {
+ return null;
+ }
- SummaryStatistics stats = new SummaryStatistics();
- for (int i = 0; i < entries.size() - 1; i++) {
- long diff = Math.abs(entries.get(i).published().toEpochMilli() - entries.get(i + 1).published().toEpochMilli());
- stats.addValue(diff);
- }
- return (long) stats.getMean();
- }
+ SummaryStatistics stats = new SummaryStatistics();
+ for (int i = 0; i < entries.size() - 1; i++) {
+ long diff =
+ Math.abs(
+ entries.get(i).published().toEpochMilli()
+ - entries.get(i + 1).published().toEpochMilli());
+ stats.addValue(diff);
+ }
+ return (long) stats.getMean();
+ }
- public static class FeedParsingException extends Exception {
- private static final long serialVersionUID = 1L;
+ public static class FeedParsingException extends Exception {
+ private static final long serialVersionUID = 1L;
- public FeedParsingException(String message) {
- super(message);
- }
-
- public FeedParsingException(String message, Throwable cause) {
- super(message, cause);
- }
- }
+ public FeedParsingException(String message) {
+ super(message);
+ }
+ public FeedParsingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParserResult.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParserResult.java
index 32980590..cb343da1 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParserResult.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParserResult.java
@@ -3,13 +3,29 @@ package com.commafeed.backend.feed.parser;
import java.time.Instant;
import java.util.List;
-public record FeedParserResult(String title, String link, String iconUrl, Instant lastPublishedDate, Long averageEntryInterval,
- Instant lastEntryDate, List