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 entries) { - public record Entry(String guid, String url, Instant published, Content content) {} +public record FeedParserResult( + String title, + String link, + String iconUrl, + Instant lastPublishedDate, + Long averageEntryInterval, + Instant lastEntryDate, + List entries) { + public record Entry(String guid, String url, Instant published, Content content) {} - public record Content(String title, String content, String author, String categories, Enclosure enclosure, Media media) {} + public record Content( + String title, + String content, + String author, + String categories, + Enclosure enclosure, + Media media) {} - public record Enclosure(String url, String type) {} + public record Enclosure(String url, String type) {} - public record Media(String description, String thumbnailUrl, Integer thumbnailWidth, Integer thumbnailHeight) {} + public record Media( + String description, + String thumbnailUrl, + Integer thumbnailWidth, + Integer thumbnailHeight) {} } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/HtmlEntities.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/HtmlEntities.java index b04c0ed2..7447134d 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/HtmlEntities.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/HtmlEntities.java @@ -4,268 +4,267 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - import lombok.experimental.UtilityClass; @UtilityClass class HtmlEntities { - public static final Map HTML_TO_NUMERIC_MAP; - public static final List HTML_ENTITIES; + public static final Map HTML_TO_NUMERIC_MAP; + public static final List HTML_ENTITIES; - static { - Map map = new LinkedHashMap<>(); - map.put("Á", "Á"); - map.put("á", "á"); - map.put("Â", "Â"); - map.put("â", "â"); - map.put("´", "´"); - map.put("Æ", "Æ"); - map.put("æ", "æ"); - map.put("À", "À"); - map.put("à", "à"); - map.put("ℵ", "ℵ"); - map.put("Α", "Α"); - map.put("α", "α"); - map.put("&", "&"); - map.put("∧", "∧"); - map.put("∠", "∠"); - map.put("Å", "Å"); - map.put("å", "å"); - map.put("≈", "≈"); - map.put("Ã", "Ã"); - map.put("ã", "ã"); - map.put("Ä", "Ä"); - map.put("ä", "ä"); - map.put("„", "„"); - map.put("Β", "Β"); - map.put("β", "β"); - map.put("¦", "¦"); - map.put("•", "•"); - map.put("∩", "∩"); - map.put("Ç", "Ç"); - map.put("ç", "ç"); - map.put("¸", "¸"); - map.put("¢", "¢"); - map.put("Χ", "Χ"); - map.put("χ", "χ"); - map.put("ˆ", "ˆ"); - map.put("♣", "♣"); - map.put("≅", "≅"); - map.put("©", "©"); - map.put("↵", "↵"); - map.put("∪", "∪"); - map.put("¤", "¤"); - map.put("†", "†"); - map.put("‡", "‡"); - map.put("↓", "↓"); - map.put("⇓", "⇓"); - map.put("°", "°"); - map.put("Δ", "Δ"); - map.put("δ", "δ"); - map.put("♦", "♦"); - map.put("÷", "÷"); - map.put("É", "É"); - map.put("é", "é"); - map.put("Ê", "Ê"); - map.put("ê", "ê"); - map.put("È", "È"); - map.put("è", "è"); - map.put("∅", "∅"); - map.put(" ", " "); - map.put(" ", " "); - map.put("Ε", "Ε"); - map.put("ε", "ε"); - map.put("≡", "≡"); - map.put("Η", "Η"); - map.put("η", "η"); - map.put("Ð", "Ð"); - map.put("ð", "ð"); - map.put("Ë", "Ë"); - map.put("ë", "ë"); - map.put("€", "€"); - map.put("∃", "∃"); - map.put("ƒ", "ƒ"); - map.put("∀", "∀"); - map.put("½", "½"); - map.put("¼", "¼"); - map.put("¾", "¾"); - map.put("⁄", "⁄"); - map.put("Γ", "Γ"); - map.put("γ", "γ"); - map.put("≥", "≥"); - map.put("↔", "↔"); - map.put("⇔", "⇔"); - map.put("♥", "♥"); - map.put("…", "…"); - map.put("Í", "Í"); - map.put("í", "í"); - map.put("Î", "Î"); - map.put("î", "î"); - map.put("¡", "¡"); - map.put("Ì", "Ì"); - map.put("ì", "ì"); - map.put("ℑ", "ℑ"); - map.put("∞", "∞"); - map.put("∫", "∫"); - map.put("Ι", "Ι"); - map.put("ι", "ι"); - map.put("¿", "¿"); - map.put("∈", "∈"); - map.put("Ï", "Ï"); - map.put("ï", "ï"); - map.put("Κ", "Κ"); - map.put("κ", "κ"); - map.put("Λ", "Λ"); - map.put("λ", "λ"); - map.put("⟨", "〈"); - map.put("«", "«"); - map.put("←", "←"); - map.put("⇐", "⇐"); - map.put("⌈", "⌈"); - map.put("“", "“"); - map.put("≤", "≤"); - map.put("⌊", "⌊"); - map.put("∗", "∗"); - map.put("◊", "◊"); - map.put("‎", "‎"); - map.put("‹", "‹"); - map.put("‘", "‘"); - map.put("¯", "¯"); - map.put("—", "—"); - map.put("µ", "µ"); - map.put("·", "·"); - map.put("−", "−"); - map.put("Μ", "Μ"); - map.put("μ", "μ"); - map.put("∇", "∇"); - map.put(" ", " "); - map.put("–", "–"); - map.put("≠", "≠"); - map.put("∋", "∋"); - map.put("¬", "¬"); - map.put("∉", "∉"); - map.put("⊄", "⊄"); - map.put("Ñ", "Ñ"); - map.put("ñ", "ñ"); - map.put("Ν", "Ν"); - map.put("ν", "ν"); - map.put("Ó", "Ó"); - map.put("ó", "ó"); - map.put("Ô", "Ô"); - map.put("ô", "ô"); - map.put("Œ", "Œ"); - map.put("œ", "œ"); - map.put("Ò", "Ò"); - map.put("ò", "ò"); - map.put("‾", "‾"); - map.put("Ω", "Ω"); - map.put("ω", "ω"); - map.put("Ο", "Ο"); - map.put("ο", "ο"); - map.put("⊕", "⊕"); - map.put("∨", "∨"); - map.put("ª", "ª"); - map.put("º", "º"); - map.put("Ø", "Ø"); - map.put("ø", "ø"); - map.put("Õ", "Õ"); - map.put("õ", "õ"); - map.put("⊗", "⊗"); - map.put("Ö", "Ö"); - map.put("ö", "ö"); - map.put("¶", "¶"); - map.put("∂", "∂"); - map.put("‰", "‰"); - map.put("⊥", "⊥"); - map.put("Φ", "Φ"); - map.put("φ", "φ"); - map.put("Π", "Π"); - map.put("π", "π"); - map.put("ϖ", "ϖ"); - map.put("±", "±"); - map.put("£", "£"); - map.put("′", "′"); - map.put("″", "″"); - map.put("∏", "∏"); - map.put("∝", "∝"); - map.put("Ψ", "Ψ"); - map.put("ψ", "ψ"); - map.put(""", """); - map.put("√", "√"); - map.put("⟩", "〉"); - map.put("»", "»"); - map.put("→", "→"); - map.put("⇒", "⇒"); - map.put("⌉", "⌉"); - map.put("”", "”"); - map.put("ℜ", "ℜ"); - map.put("®", "®"); - map.put("⌋", "⌋"); - map.put("Ρ", "Ρ"); - map.put("ρ", "ρ"); - map.put("‏", "‏"); - map.put("›", "›"); - map.put("’", "’"); - map.put("‚", "‚"); - map.put("Š", "Š"); - map.put("š", "š"); - map.put("⋅", "⋅"); - map.put("§", "§"); - map.put("­", "­"); - map.put("Σ", "Σ"); - map.put("σ", "σ"); - map.put("ς", "ς"); - map.put("∼", "∼"); - map.put("♠", "♠"); - map.put("⊂", "⊂"); - map.put("⊆", "⊆"); - map.put("∑", "∑"); - map.put("¹", "¹"); - map.put("²", "²"); - map.put("³", "³"); - map.put("⊃", "⊃"); - map.put("⊇", "⊇"); - map.put("ß", "ß"); - map.put("Τ", "Τ"); - map.put("τ", "τ"); - map.put("∴", "∴"); - map.put("Θ", "Θ"); - map.put("θ", "θ"); - map.put("ϑ", "ϑ"); - map.put(" ", " "); - map.put("Þ", "Þ"); - map.put("þ", "þ"); - map.put("˜", "˜"); - map.put("×", "×"); - map.put("™", "™"); - map.put("Ú", "Ú"); - map.put("ú", "ú"); - map.put("↑", "↑"); - map.put("⇑", "⇑"); - map.put("Û", "Û"); - map.put("û", "û"); - map.put("Ù", "Ù"); - map.put("ù", "ù"); - map.put("¨", "¨"); - map.put("ϒ", "ϒ"); - map.put("Υ", "Υ"); - map.put("υ", "υ"); - map.put("Ü", "Ü"); - map.put("ü", "ü"); - map.put("℘", "℘"); - map.put("Ξ", "Ξ"); - map.put("ξ", "ξ"); - map.put("Ý", "Ý"); - map.put("ý", "ý"); - map.put("¥", "¥"); - map.put("ÿ", "ÿ"); - map.put("Ÿ", "Ÿ"); - map.put("Ζ", "Ζ"); - map.put("ζ", "ζ"); - map.put("‍", "‍"); - map.put("‌", "‌"); + static { + Map map = new LinkedHashMap<>(); + map.put("Á", "Á"); + map.put("á", "á"); + map.put("Â", "Â"); + map.put("â", "â"); + map.put("´", "´"); + map.put("Æ", "Æ"); + map.put("æ", "æ"); + map.put("À", "À"); + map.put("à", "à"); + map.put("ℵ", "ℵ"); + map.put("Α", "Α"); + map.put("α", "α"); + map.put("&", "&"); + map.put("∧", "∧"); + map.put("∠", "∠"); + map.put("Å", "Å"); + map.put("å", "å"); + map.put("≈", "≈"); + map.put("Ã", "Ã"); + map.put("ã", "ã"); + map.put("Ä", "Ä"); + map.put("ä", "ä"); + map.put("„", "„"); + map.put("Β", "Β"); + map.put("β", "β"); + map.put("¦", "¦"); + map.put("•", "•"); + map.put("∩", "∩"); + map.put("Ç", "Ç"); + map.put("ç", "ç"); + map.put("¸", "¸"); + map.put("¢", "¢"); + map.put("Χ", "Χ"); + map.put("χ", "χ"); + map.put("ˆ", "ˆ"); + map.put("♣", "♣"); + map.put("≅", "≅"); + map.put("©", "©"); + map.put("↵", "↵"); + map.put("∪", "∪"); + map.put("¤", "¤"); + map.put("†", "†"); + map.put("‡", "‡"); + map.put("↓", "↓"); + map.put("⇓", "⇓"); + map.put("°", "°"); + map.put("Δ", "Δ"); + map.put("δ", "δ"); + map.put("♦", "♦"); + map.put("÷", "÷"); + map.put("É", "É"); + map.put("é", "é"); + map.put("Ê", "Ê"); + map.put("ê", "ê"); + map.put("È", "È"); + map.put("è", "è"); + map.put("∅", "∅"); + map.put(" ", " "); + map.put(" ", " "); + map.put("Ε", "Ε"); + map.put("ε", "ε"); + map.put("≡", "≡"); + map.put("Η", "Η"); + map.put("η", "η"); + map.put("Ð", "Ð"); + map.put("ð", "ð"); + map.put("Ë", "Ë"); + map.put("ë", "ë"); + map.put("€", "€"); + map.put("∃", "∃"); + map.put("ƒ", "ƒ"); + map.put("∀", "∀"); + map.put("½", "½"); + map.put("¼", "¼"); + map.put("¾", "¾"); + map.put("⁄", "⁄"); + map.put("Γ", "Γ"); + map.put("γ", "γ"); + map.put("≥", "≥"); + map.put("↔", "↔"); + map.put("⇔", "⇔"); + map.put("♥", "♥"); + map.put("…", "…"); + map.put("Í", "Í"); + map.put("í", "í"); + map.put("Î", "Î"); + map.put("î", "î"); + map.put("¡", "¡"); + map.put("Ì", "Ì"); + map.put("ì", "ì"); + map.put("ℑ", "ℑ"); + map.put("∞", "∞"); + map.put("∫", "∫"); + map.put("Ι", "Ι"); + map.put("ι", "ι"); + map.put("¿", "¿"); + map.put("∈", "∈"); + map.put("Ï", "Ï"); + map.put("ï", "ï"); + map.put("Κ", "Κ"); + map.put("κ", "κ"); + map.put("Λ", "Λ"); + map.put("λ", "λ"); + map.put("⟨", "〈"); + map.put("«", "«"); + map.put("←", "←"); + map.put("⇐", "⇐"); + map.put("⌈", "⌈"); + map.put("“", "“"); + map.put("≤", "≤"); + map.put("⌊", "⌊"); + map.put("∗", "∗"); + map.put("◊", "◊"); + map.put("‎", "‎"); + map.put("‹", "‹"); + map.put("‘", "‘"); + map.put("¯", "¯"); + map.put("—", "—"); + map.put("µ", "µ"); + map.put("·", "·"); + map.put("−", "−"); + map.put("Μ", "Μ"); + map.put("μ", "μ"); + map.put("∇", "∇"); + map.put(" ", " "); + map.put("–", "–"); + map.put("≠", "≠"); + map.put("∋", "∋"); + map.put("¬", "¬"); + map.put("∉", "∉"); + map.put("⊄", "⊄"); + map.put("Ñ", "Ñ"); + map.put("ñ", "ñ"); + map.put("Ν", "Ν"); + map.put("ν", "ν"); + map.put("Ó", "Ó"); + map.put("ó", "ó"); + map.put("Ô", "Ô"); + map.put("ô", "ô"); + map.put("Œ", "Œ"); + map.put("œ", "œ"); + map.put("Ò", "Ò"); + map.put("ò", "ò"); + map.put("‾", "‾"); + map.put("Ω", "Ω"); + map.put("ω", "ω"); + map.put("Ο", "Ο"); + map.put("ο", "ο"); + map.put("⊕", "⊕"); + map.put("∨", "∨"); + map.put("ª", "ª"); + map.put("º", "º"); + map.put("Ø", "Ø"); + map.put("ø", "ø"); + map.put("Õ", "Õ"); + map.put("õ", "õ"); + map.put("⊗", "⊗"); + map.put("Ö", "Ö"); + map.put("ö", "ö"); + map.put("¶", "¶"); + map.put("∂", "∂"); + map.put("‰", "‰"); + map.put("⊥", "⊥"); + map.put("Φ", "Φ"); + map.put("φ", "φ"); + map.put("Π", "Π"); + map.put("π", "π"); + map.put("ϖ", "ϖ"); + map.put("±", "±"); + map.put("£", "£"); + map.put("′", "′"); + map.put("″", "″"); + map.put("∏", "∏"); + map.put("∝", "∝"); + map.put("Ψ", "Ψ"); + map.put("ψ", "ψ"); + map.put(""", """); + map.put("√", "√"); + map.put("⟩", "〉"); + map.put("»", "»"); + map.put("→", "→"); + map.put("⇒", "⇒"); + map.put("⌉", "⌉"); + map.put("”", "”"); + map.put("ℜ", "ℜ"); + map.put("®", "®"); + map.put("⌋", "⌋"); + map.put("Ρ", "Ρ"); + map.put("ρ", "ρ"); + map.put("‏", "‏"); + map.put("›", "›"); + map.put("’", "’"); + map.put("‚", "‚"); + map.put("Š", "Š"); + map.put("š", "š"); + map.put("⋅", "⋅"); + map.put("§", "§"); + map.put("­", "­"); + map.put("Σ", "Σ"); + map.put("σ", "σ"); + map.put("ς", "ς"); + map.put("∼", "∼"); + map.put("♠", "♠"); + map.put("⊂", "⊂"); + map.put("⊆", "⊆"); + map.put("∑", "∑"); + map.put("¹", "¹"); + map.put("²", "²"); + map.put("³", "³"); + map.put("⊃", "⊃"); + map.put("⊇", "⊇"); + map.put("ß", "ß"); + map.put("Τ", "Τ"); + map.put("τ", "τ"); + map.put("∴", "∴"); + map.put("Θ", "Θ"); + map.put("θ", "θ"); + map.put("ϑ", "ϑ"); + map.put(" ", " "); + map.put("Þ", "Þ"); + map.put("þ", "þ"); + map.put("˜", "˜"); + map.put("×", "×"); + map.put("™", "™"); + map.put("Ú", "Ú"); + map.put("ú", "ú"); + map.put("↑", "↑"); + map.put("⇑", "⇑"); + map.put("Û", "Û"); + map.put("û", "û"); + map.put("Ù", "Ù"); + map.put("ù", "ù"); + map.put("¨", "¨"); + map.put("ϒ", "ϒ"); + map.put("Υ", "Υ"); + map.put("υ", "υ"); + map.put("Ü", "Ü"); + map.put("ü", "ü"); + map.put("℘", "℘"); + map.put("Ξ", "Ξ"); + map.put("ξ", "ξ"); + map.put("Ý", "Ý"); + map.put("ý", "ý"); + map.put("¥", "¥"); + map.put("ÿ", "ÿ"); + map.put("Ÿ", "Ÿ"); + map.put("Ζ", "Ζ"); + map.put("ζ", "ζ"); + map.put("‍", "‍"); + map.put("‌", "‌"); - HTML_TO_NUMERIC_MAP = Collections.unmodifiableMap(map); - HTML_ENTITIES = List.copyOf(map.keySet()); - } + HTML_TO_NUMERIC_MAP = Collections.unmodifiableMap(map); + HTML_ENTITIES = List.copyOf(map.keySet()); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/TextDirectionDetector.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/TextDirectionDetector.java index 157b0048..3205151e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/TextDirectionDetector.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/TextDirectionDetector.java @@ -2,55 +2,54 @@ package com.commafeed.backend.feed.parser; import java.text.Bidi; import java.util.regex.Pattern; - import org.apache.commons.lang3.math.NumberUtils; public class TextDirectionDetector { - private static final Pattern WORDS_PATTERN = Pattern.compile("\\s+"); - private static final Pattern URL_PATTERN = Pattern.compile("^https?://.*"); + private static final Pattern WORDS_PATTERN = Pattern.compile("\\s+"); + private static final Pattern URL_PATTERN = Pattern.compile("^https?://.*"); - private static final double RTL_THRESHOLD = 0.4D; + private static final double RTL_THRESHOLD = 0.4D; - public enum Direction { - LEFT_TO_RIGHT, RIGHT_TO_LEFT - } + public enum Direction { + LEFT_TO_RIGHT, + RIGHT_TO_LEFT + } - public static Direction detect(String input) { - if (input == null || input.isBlank()) { - return Direction.LEFT_TO_RIGHT; - } + public static Direction detect(String input) { + if (input == null || input.isBlank()) { + return Direction.LEFT_TO_RIGHT; + } - long rtl = 0; - long total = 0; - for (String token : WORDS_PATTERN.split(input)) { - // skip urls - if (URL_PATTERN.matcher(token).matches()) { - continue; - } + long rtl = 0; + long total = 0; + for (String token : WORDS_PATTERN.split(input)) { + // skip urls + if (URL_PATTERN.matcher(token).matches()) { + continue; + } - // skip numbers - if (NumberUtils.isCreatable(token)) { - continue; - } + // skip numbers + if (NumberUtils.isCreatable(token)) { + continue; + } - boolean requiresBidi = Bidi.requiresBidi(token.toCharArray(), 0, token.length()); - if (requiresBidi) { - Bidi bidi = new Bidi(token, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); - if (bidi.getBaseLevel() == 1) { - rtl++; - } - } + boolean requiresBidi = Bidi.requiresBidi(token.toCharArray(), 0, token.length()); + if (requiresBidi) { + Bidi bidi = new Bidi(token, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT); + if (bidi.getBaseLevel() == 1) { + rtl++; + } + } - total++; - } + total++; + } - if (total == 0) { - return Direction.LEFT_TO_RIGHT; - } - - double ratio = (double) rtl / total; - return ratio > RTL_THRESHOLD ? Direction.RIGHT_TO_LEFT : Direction.LEFT_TO_RIGHT; - } + if (total == 0) { + return Direction.LEFT_TO_RIGHT; + } + double ratio = (double) rtl / total; + return ratio > RTL_THRESHOLD ? Direction.RIGHT_TO_LEFT : Direction.LEFT_TO_RIGHT; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/XMLCleaner.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/XMLCleaner.java index 358deea9..8ca46a2f 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/XMLCleaner.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/XMLCleaner.java @@ -1,10 +1,8 @@ package com.commafeed.backend.feed.parser; +import jakarta.inject.Singleton; import java.util.Collection; import java.util.regex.Pattern; - -import jakarta.inject.Singleton; - import org.ahocorasick.trie.Emit; import org.ahocorasick.trie.Trie; import org.apache.commons.lang3.StringUtils; @@ -13,70 +11,71 @@ import org.jdom2.Verifier; @Singleton public class XMLCleaner { - private static final Pattern DOCTYPE_PATTERN = Pattern.compile("]*>", Pattern.CASE_INSENSITIVE); + private static final Pattern DOCTYPE_PATTERN = + Pattern.compile("]*>", Pattern.CASE_INSENSITIVE); - private final Trie trie = Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build(); + private final Trie trie = + Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build(); - public String clean(String xml) { - xml = removeCharactersBeforeFirstXmlTag(xml); - xml = removeInvalidXmlCharacters(xml); - xml = replaceHtmlEntitiesWithNumericEntities(xml); - xml = removeDoctypeDeclarations(xml); - return xml; - } + public String clean(String xml) { + xml = removeCharactersBeforeFirstXmlTag(xml); + xml = removeInvalidXmlCharacters(xml); + xml = replaceHtmlEntitiesWithNumericEntities(xml); + xml = removeDoctypeDeclarations(xml); + return xml; + } - String removeCharactersBeforeFirstXmlTag(String xml) { - if (StringUtils.isBlank(xml)) { - return null; - } + String removeCharactersBeforeFirstXmlTag(String xml) { + if (StringUtils.isBlank(xml)) { + return null; + } - int pos = xml.indexOf('<'); - return pos < 0 ? null : xml.substring(pos); - } + int pos = xml.indexOf('<'); + return pos < 0 ? null : xml.substring(pos); + } - String removeInvalidXmlCharacters(String xml) { - if (StringUtils.isBlank(xml)) { - return null; - } + String removeInvalidXmlCharacters(String xml) { + if (StringUtils.isBlank(xml)) { + return null; + } - return xml.codePoints() - .filter(Verifier::isXMLCharacter) - .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) - .toString(); - } + return xml.codePoints() + .filter(Verifier::isXMLCharacter) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } - // https://stackoverflow.com/a/40836618 - String replaceHtmlEntitiesWithNumericEntities(String source) { - if (StringUtils.isBlank(source)) { - return null; - } + // https://stackoverflow.com/a/40836618 + String replaceHtmlEntitiesWithNumericEntities(String source) { + if (StringUtils.isBlank(source)) { + return null; + } - // Create a buffer sufficiently large that re-allocations are minimized. - StringBuilder sb = new StringBuilder(source.length() << 1); + // Create a buffer sufficiently large that re-allocations are minimized. + StringBuilder sb = new StringBuilder(source.length() << 1); - Collection emits = trie.parseText(source); + Collection emits = trie.parseText(source); - int prevIndex = 0; - for (Emit emit : emits) { - int matchIndex = emit.getStart(); + int prevIndex = 0; + for (Emit emit : emits) { + int matchIndex = emit.getStart(); - sb.append(source, prevIndex, matchIndex); - sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword())); - prevIndex = emit.getEnd() + 1; - } + sb.append(source, prevIndex, matchIndex); + sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword())); + prevIndex = emit.getEnd() + 1; + } - // Add the remainder of the string (contains no more matches). - sb.append(source.substring(prevIndex)); + // Add the remainder of the string (contains no more matches). + sb.append(source.substring(prevIndex)); - return sb.toString(); - } + return sb.toString(); + } - String removeDoctypeDeclarations(String xml) { - if (StringUtils.isBlank(xml)) { - return null; - } - - return DOCTYPE_PATTERN.matcher(xml).replaceAll(""); - } + String removeDoctypeDeclarations(String xml) { + if (StringUtils.isBlank(xml)) { + return null; + } + return DOCTYPE_PATTERN.matcher(xml).replaceAll(""); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/AbstractModel.java b/commafeed-server/src/main/java/com/commafeed/backend/model/AbstractModel.java index 7ca6a9c0..76eed405 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/AbstractModel.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/AbstractModel.java @@ -1,33 +1,28 @@ package com.commafeed.backend.model; -import java.io.Serializable; - import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.TableGenerator; - +import java.io.Serializable; import lombok.Getter; import lombok.Setter; -/** - * Abstract model for all entities, defining id and table generator - * - */ +/** Abstract model for all entities, defining id and table generator */ @SuppressWarnings("serial") @MappedSuperclass @Getter @Setter public abstract class AbstractModel implements Serializable { - @Id - @GeneratedValue(strategy = GenerationType.TABLE, generator = "gen") - @TableGenerator( - name = "gen", - table = "hibernate_sequences", - pkColumnName = "sequence_name", - valueColumnName = "sequence_next_hi_value", - allocationSize = 1000) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.TABLE, generator = "gen") + @TableGenerator( + name = "gen", + table = "hibernate_sequences", + pkColumnName = "sequence_name", + valueColumnName = "sequence_next_hi_value", + allocationSize = 1000) + private Long id; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/Feed.java b/commafeed-server/src/main/java/com/commafeed/backend/model/Feed.java index 438247ec..34e3d112 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/Feed.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/Feed.java @@ -1,17 +1,14 @@ package com.commafeed.backend.model; -import java.sql.Types; -import java.time.Instant; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Lob; import jakarta.persistence.Table; - -import org.hibernate.annotations.JdbcTypeCode; - +import java.sql.Types; +import java.time.Instant; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; @Entity @Table(name = "FEEDS") @@ -20,97 +17,66 @@ import lombok.Setter; @Setter public class Feed extends AbstractModel { - /** - * The url of the feed - */ - @Lob - @Column(length = Integer.MAX_VALUE, nullable = false) - @JdbcTypeCode(Types.LONGVARCHAR) - private String url; + /** The url of the feed */ + @Lob + @Column(length = Integer.MAX_VALUE, nullable = false) + @JdbcTypeCode(Types.LONGVARCHAR) + private String url; - /** - * cache the url after potential http 30x redirects - */ - @Column(name = "url_after_redirect", length = 2048, nullable = false) - private String urlAfterRedirect; + /** cache the url after potential http 30x redirects */ + @Column(name = "url_after_redirect", length = 2048, nullable = false) + private String urlAfterRedirect; - @Column(length = 2048, nullable = false) - private String normalizedUrl; + @Column(length = 2048, nullable = false) + private String normalizedUrl; - @Column(length = 40, nullable = false) - private String normalizedUrlHash; + @Column(length = 40, nullable = false) + private String normalizedUrlHash; - /** - * The url of the website, extracted from the feed - */ - @Lob - @Column(length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String link; + /** The url of the website, extracted from the feed */ + @Lob + @Column(length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String link; - @Lob - @Column(name = "icon_url", length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String iconUrl; + @Lob + @Column(name = "icon_url", length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String iconUrl; - /** - * Last time we tried to fetch the feed - */ - @Column - private Instant lastUpdated; + /** Last time we tried to fetch the feed */ + @Column private Instant lastUpdated; - /** - * Last publishedDate value in the feed - */ - @Column - private Instant lastPublishedDate; + /** Last publishedDate value in the feed */ + @Column private Instant lastPublishedDate; - /** - * date of the last entry of the feed - */ - @Column - private Instant lastEntryDate; + /** date of the last entry of the feed */ + @Column private Instant lastEntryDate; - /** - * error message while retrieving the feed - */ - @Lob - @Column(length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String message; + /** error message while retrieving the feed */ + @Lob + @Column(length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String message; - /** - * times we failed to retrieve the feed - */ - private int errorCount; + /** times we failed to retrieve the feed */ + private int errorCount; - /** - * feed refresh is disabled until this date - */ - @Column - private Instant disabledUntil; + /** feed refresh is disabled until this date */ + @Column private Instant disabledUntil; - /** - * http header returned by the feed - */ - @Column(length = 64) - private String lastModifiedHeader; + /** http header returned by the feed */ + @Column(length = 64) + private String lastModifiedHeader; - /** - * http header returned by the feed - */ - @Column(length = 255) - private String etagHeader; + /** http header returned by the feed */ + @Column(length = 255) + private String etagHeader; - /** - * average time between entries in the feed - */ - private Long averageEntryInterval; - - /** - * last hash of the content of the feed xml - */ - @Column(length = 40) - private String lastContentHash; + /** average time between entries in the feed */ + private Long averageEntryInterval; + /** last hash of the content of the feed xml */ + @Column(length = 40) + private String lastContentHash; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedCategory.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedCategory.java index d1769013..d530a998 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedCategory.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedCategory.java @@ -6,7 +6,6 @@ import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; - import lombok.Getter; import lombok.Setter; @@ -17,18 +16,17 @@ import lombok.Setter; @Setter public class FeedCategory extends AbstractModel { - @Column(length = 128, nullable = false) - private String name; + @Column(length = 128, nullable = false) + private String name; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) - private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private User user; - @ManyToOne(fetch = FetchType.LAZY) - private FeedCategory parent; + @ManyToOne(fetch = FetchType.LAZY) + private FeedCategory parent; - private boolean collapsed; - - private int position; + private boolean collapsed; + private int position; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntry.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntry.java index 12ddc98e..982f194f 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntry.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntry.java @@ -1,8 +1,5 @@ package com.commafeed.backend.model; -import java.time.Instant; -import java.util.Set; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -11,7 +8,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; - +import java.time.Instant; +import java.util.Set; import lombok.Getter; import lombok.Setter; @@ -22,39 +20,32 @@ import lombok.Setter; @Setter public class FeedEntry extends AbstractModel { - @Column(length = 2048, nullable = false) - private String guid; + @Column(length = 2048, nullable = false) + private String guid; - @Column(length = 40, nullable = false) - private String guidHash; + @Column(length = 40, nullable = false) + private String guidHash; - @ManyToOne(fetch = FetchType.LAZY) - private Feed feed; + @ManyToOne(fetch = FetchType.LAZY) + private Feed feed; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(nullable = false, updatable = false) - private FeedEntryContent content; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(nullable = false, updatable = false) + private FeedEntryContent content; - @Column(length = 2048) - private String url; + @Column(length = 2048) + private String url; - /** - * the moment the entry was inserted in the database - */ - @Column - private Instant inserted; + /** the moment the entry was inserted in the database */ + @Column private Instant inserted; - /** - * the moment the entry was published in the feed - * - */ - @Column(name = "updated") - private Instant published; + /** the moment the entry was published in the feed */ + @Column(name = "updated") + private Instant published; - @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) - private Set statuses; - - @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) - private Set tags; + @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) + private Set statuses; + @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE) + private Set tags; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryContent.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryContent.java index 195164b7..9c5bdd92 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryContent.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryContent.java @@ -1,21 +1,17 @@ package com.commafeed.backend.model; -import java.sql.Types; - +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Lob; import jakarta.persistence.Table; - -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.hibernate.annotations.JdbcTypeCode; - -import com.fasterxml.jackson.annotation.JsonProperty; - +import java.sql.Types; import lombok.Getter; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.hibernate.annotations.JdbcTypeCode; @Entity @Table(name = "FEEDENTRYCONTENTS") @@ -24,73 +20,74 @@ import lombok.Setter; @Setter public class FeedEntryContent extends AbstractModel { - public enum Direction { - @JsonProperty("ltr") - LTR, + public enum Direction { + @JsonProperty("ltr") + LTR, - @JsonProperty("rtl") - RTL, + @JsonProperty("rtl") + RTL, - @JsonProperty("unknown") - UNKNOWN - } + @JsonProperty("unknown") + UNKNOWN + } - @Column(length = 2048) - private String title; + @Column(length = 2048) + private String title; - @Column(length = 40) - private String titleHash; + @Column(length = 40) + private String titleHash; - @Lob - @Column(length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String content; + @Lob + @Column(length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String content; - @Column(length = 40) - private String contentHash; + @Column(length = 40) + private String contentHash; - @Column(name = "author", length = 128) - private String author; + @Column(name = "author", length = 128) + private String author; - @Column(length = 2048) - private String enclosureUrl; + @Column(length = 2048) + private String enclosureUrl; - @Column(length = 255) - private String enclosureType; + @Column(length = 255) + private String enclosureType; - @Lob - @Column(length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String mediaDescription; + @Lob + @Column(length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String mediaDescription; - @Column(length = 2048) - private String mediaThumbnailUrl; + @Column(length = 2048) + private String mediaThumbnailUrl; - private Integer mediaThumbnailWidth; - private Integer mediaThumbnailHeight; + private Integer mediaThumbnailWidth; + private Integer mediaThumbnailHeight; - @Column(length = 4096) - private String categories; + @Column(length = 4096) + private String categories; - @Column - @Enumerated(EnumType.STRING) - private Direction direction = Direction.UNKNOWN; + @Column + @Enumerated(EnumType.STRING) + private Direction direction = Direction.UNKNOWN; - public boolean equivalentTo(FeedEntryContent c) { - if (c == null) { - return false; - } + public boolean equivalentTo(FeedEntryContent c) { + if (c == null) { + return false; + } - return new EqualsBuilder().append(title, c.title) - .append(content, c.content) - .append(author, c.author) - .append(categories, c.categories) - .append(enclosureUrl, c.enclosureUrl) - .append(enclosureType, c.enclosureType) - .append(mediaDescription, c.mediaDescription) - .append(mediaThumbnailUrl, c.mediaThumbnailUrl) - .append(mediaThumbnailWidth, c.mediaThumbnailWidth) - .append(mediaThumbnailHeight, c.mediaThumbnailHeight) - .build(); - } + return new EqualsBuilder() + .append(title, c.title) + .append(content, c.content) + .append(author, c.author) + .append(categories, c.categories) + .append(enclosureUrl, c.enclosureUrl) + .append(enclosureType, c.enclosureType) + .append(mediaDescription, c.mediaDescription) + .append(mediaThumbnailUrl, c.mediaThumbnailUrl) + .append(mediaThumbnailWidth, c.mediaThumbnailWidth) + .append(mediaThumbnailHeight, c.mediaThumbnailHeight) + .build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java index 98423e21..c94bcf73 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryStatus.java @@ -1,9 +1,5 @@ package com.commafeed.backend.model; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -11,7 +7,9 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; - +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import lombok.Getter; import lombok.Setter; @@ -22,48 +20,40 @@ import lombok.Setter; @Setter public class FeedEntryStatus extends AbstractModel { - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) - private FeedSubscription subscription; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private FeedSubscription subscription; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) - private FeedEntry entry; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private FeedEntry entry; - @Column(name = "read_status") - private boolean read; - private boolean starred; + @Column(name = "read_status") + private boolean read; - @Transient - private boolean markable; + private boolean starred; - @Transient - private List tags = new ArrayList<>(); + @Transient private boolean markable; - /** - * Denormalization starts here - */ + @Transient private List tags = new ArrayList<>(); - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) - private User user; + /** Denormalization starts here */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private User user; - @Column - private Instant entryInserted; + @Column private Instant entryInserted; - @Column(name = "entryUpdated") - private Instant entryPublished; + @Column(name = "entryUpdated") + private Instant entryPublished; - public FeedEntryStatus() { - - } - - public FeedEntryStatus(User user, FeedSubscription subscription, FeedEntry entry) { - this.user = user; - this.subscription = subscription; - this.entry = entry; - this.entryInserted = entry.getInserted(); - this.entryPublished = entry.getPublished(); - } + public FeedEntryStatus() {} + public FeedEntryStatus(User user, FeedSubscription subscription, FeedEntry entry) { + this.user = user; + this.subscription = subscription; + this.entry = entry; + this.entryInserted = entry.getInserted(); + this.entryPublished = entry.getPublished(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryTag.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryTag.java index 8ca204a5..ffb3fd44 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryTag.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedEntryTag.java @@ -6,7 +6,6 @@ import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; - import lombok.Getter; import lombok.Setter; @@ -17,24 +16,22 @@ import lombok.Setter; @Setter public class FeedEntryTag extends AbstractModel { - @JoinColumn(name = "user_id") - @ManyToOne(fetch = FetchType.LAZY) - private User user; + @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY) + private User user; - @JoinColumn(name = "entry_id") - @ManyToOne(fetch = FetchType.LAZY) - private FeedEntry entry; + @JoinColumn(name = "entry_id") + @ManyToOne(fetch = FetchType.LAZY) + private FeedEntry entry; - @Column(name = "name", length = 40) - private String name; + @Column(name = "name", length = 40) + private String name; - public FeedEntryTag() { - } - - public FeedEntryTag(User user, FeedEntry entry, String name) { - this.name = name; - this.entry = entry; - this.user = user; - } + public FeedEntryTag() {} + public FeedEntryTag(User user, FeedEntry entry, String name) { + this.name = name; + this.entry = entry; + this.user = user; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java index 2dec0d2a..b0a08a4a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/FeedSubscription.java @@ -1,7 +1,5 @@ package com.commafeed.backend.model; -import java.util.Set; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -10,7 +8,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; - +import java.util.Set; import lombok.Getter; import lombok.Setter; @@ -21,35 +19,34 @@ import lombok.Setter; @Setter public class FeedSubscription extends AbstractModel { - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) - private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private User user; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(nullable = false) - private Feed feed; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Feed feed; - @Column(length = 128, nullable = false) - private String title; + @Column(length = 128, nullable = false) + private String title; - @ManyToOne(fetch = FetchType.LAZY) - private FeedCategory category; + @ManyToOne(fetch = FetchType.LAZY) + private FeedCategory category; - @OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE) - private Set statuses; + @OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE) + private Set statuses; - private int position; + private int position; - @Column(name = "filtering_expression", length = 4096) - private String filter; + @Column(name = "filtering_expression", length = 4096) + private String filter; - @Column(name = "filtering_expression_legacy", length = 4096) - private String filterLegacy; + @Column(name = "filtering_expression_legacy", length = 4096) + private String filterLegacy; - @Column(name = "push_notifications_enabled") - private boolean pushNotificationsEnabled; - - @Column(name = "auto_mark_as_read_after_days") - private Integer autoMarkAsReadAfterDays; + @Column(name = "push_notifications_enabled") + private boolean pushNotificationsEnabled; + @Column(name = "auto_mark_as_read_after_days") + private Integer autoMarkAsReadAfterDays; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/Models.java b/commafeed-server/src/main/java/com/commafeed/backend/model/Models.java index 950c0a18..e0ad99d7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/Models.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/Models.java @@ -2,40 +2,35 @@ package com.commafeed.backend.model; import java.time.Duration; import java.time.Instant; - +import lombok.experimental.UtilityClass; import org.hibernate.Hibernate; import org.hibernate.HibernateException; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; -import lombok.experimental.UtilityClass; - @UtilityClass public class Models { - public static final Instant MINIMUM_INSTANT = Instant.EPOCH - // mariadb timestamp range starts at 1970-01-01 00:00:01 - .plusSeconds(1) - // make sure the timestamp fits for all timezones - .plus(Duration.ofHours(24)); + public static final Instant MINIMUM_INSTANT = + Instant.EPOCH + // mariadb timestamp range starts at 1970-01-01 00:00:01 + .plusSeconds(1) + // make sure the timestamp fits for all timezones + .plus(Duration.ofHours(24)); - /** - * initialize a proxy - */ - public static void initialize(Object proxy) throws HibernateException { - Hibernate.initialize(proxy); - } + /** initialize a proxy */ + public static void initialize(Object proxy) throws HibernateException { + Hibernate.initialize(proxy); + } - /** - * extract the id from the proxy without initializing it - */ - public static Long getId(AbstractModel model) { - if (model instanceof HibernateProxy proxy) { - LazyInitializer lazyInitializer = proxy.getHibernateLazyInitializer(); - if (lazyInitializer.isUninitialized()) { - return (Long) lazyInitializer.getIdentifier(); - } - } - return model.getId(); - } + /** extract the id from the proxy without initializing it */ + public static Long getId(AbstractModel model) { + if (model instanceof HibernateProxy proxy) { + LazyInitializer lazyInitializer = proxy.getHibernateLazyInitializer(); + if (lazyInitializer.isUninitialized()) { + return (Long) lazyInitializer.getIdentifier(); + } + } + return model.getId(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/User.java b/commafeed-server/src/main/java/com/commafeed/backend/model/User.java index 59e39d9f..ebecc611 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/User.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/User.java @@ -1,17 +1,14 @@ package com.commafeed.backend.model; -import java.sql.Types; -import java.time.Instant; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Lob; import jakarta.persistence.Table; - -import org.hibernate.annotations.JdbcTypeCode; - +import java.sql.Types; +import java.time.Instant; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; @Entity @Table(name = "USERS") @@ -20,40 +17,36 @@ import lombok.Setter; @Setter public class User extends AbstractModel { - @Column(length = 32, nullable = false, unique = true) - private String name; + @Column(length = 32, nullable = false, unique = true) + private String name; - @Column(length = 255, unique = true) - private String email; + @Column(length = 255, unique = true) + private String email; - @Lob - @Column(length = Integer.MAX_VALUE, nullable = false) - @JdbcTypeCode(Types.LONGVARBINARY) - private byte[] password; + @Lob + @Column(length = Integer.MAX_VALUE, nullable = false) + @JdbcTypeCode(Types.LONGVARBINARY) + private byte[] password; - @Column(length = 40, unique = true) - private String apiKey; + @Column(length = 40, unique = true) + private String apiKey; - @Lob - @Column(length = Integer.MAX_VALUE, nullable = false) - @JdbcTypeCode(Types.LONGVARBINARY) - private byte[] salt; + @Lob + @Column(length = Integer.MAX_VALUE, nullable = false) + @JdbcTypeCode(Types.LONGVARBINARY) + private byte[] salt; - @Column(nullable = false) - private boolean disabled; + @Column(nullable = false) + private boolean disabled; - @Column - private Instant lastLogin; + @Column private Instant lastLogin; - @Column - private Instant created; + @Column private Instant created; - @Column(length = 40) - private String recoverPasswordToken; + @Column(length = 40) + private String recoverPasswordToken; - @Column - private Instant recoverPasswordTokenDate; + @Column private Instant recoverPasswordTokenDate; - @Column - private Instant lastForceRefresh; + @Column private Instant lastForceRefresh; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/UserRole.java b/commafeed-server/src/main/java/com/commafeed/backend/model/UserRole.java index a5b46a3b..222fac28 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/UserRole.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/UserRole.java @@ -8,7 +8,6 @@ import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; - import lombok.Getter; import lombok.Setter; @@ -19,25 +18,23 @@ import lombok.Setter; @Setter public class UserRole extends AbstractModel { - public enum Role { - USER, ADMIN - } + public enum Role { + USER, + ADMIN + } - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; - @Column(name = "roleName", nullable = false) - @Enumerated(EnumType.STRING) - private Role role; + @Column(name = "roleName", nullable = false) + @Enumerated(EnumType.STRING) + private Role role; - public UserRole() { - - } - - public UserRole(User user, Role role) { - this.user = user; - this.role = role; - } + public UserRole() {} + public UserRole(User user, Role role) { + this.user = user; + this.role = role; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java b/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java index 83c60645..7dae8bf0 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/model/UserSettings.java @@ -1,8 +1,6 @@ package com.commafeed.backend.model; -import java.io.Serializable; -import java.sql.Types; - +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import jakarta.persistence.Embedded; @@ -14,13 +12,11 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.Lob; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; - -import org.hibernate.annotations.JdbcTypeCode; - -import com.fasterxml.jackson.annotation.JsonProperty; - +import java.io.Serializable; +import java.sql.Types; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; @Entity @Table(name = "USERSETTINGS") @@ -29,155 +25,154 @@ import lombok.Setter; @Setter public class UserSettings extends AbstractModel { - public enum ReadingMode { - @JsonProperty("all") - ALL, + public enum ReadingMode { + @JsonProperty("all") + ALL, - @JsonProperty("unread") - UNREAD; + @JsonProperty("unread") + UNREAD; - // method called for query parameters - public static ReadingMode fromString(final String s) { - return ReadingMode.valueOf(s.toUpperCase()); - } - } + // method called for query parameters + public static ReadingMode fromString(final String s) { + return ReadingMode.valueOf(s.toUpperCase()); + } + } - public enum ReadingOrder { - @JsonProperty("asc") - ASC, + public enum ReadingOrder { + @JsonProperty("asc") + ASC, - @JsonProperty("desc") - DESC; + @JsonProperty("desc") + DESC; - // method called for query parameters - public static ReadingOrder fromString(final String s) { - return ReadingOrder.valueOf(s.toUpperCase()); - } - } + // method called for query parameters + public static ReadingOrder fromString(final String s) { + return ReadingOrder.valueOf(s.toUpperCase()); + } + } - public enum ScrollMode { - @JsonProperty("always") - ALWAYS, + public enum ScrollMode { + @JsonProperty("always") + ALWAYS, - @JsonProperty("never") - NEVER, + @JsonProperty("never") + NEVER, - @JsonProperty("if_needed") - IF_NEEDED - } + @JsonProperty("if_needed") + IF_NEEDED + } - public enum IconDisplayMode { - @JsonProperty("always") - ALWAYS, + public enum IconDisplayMode { + @JsonProperty("always") + ALWAYS, - @JsonProperty("never") - NEVER, + @JsonProperty("never") + NEVER, - @JsonProperty("on_desktop") - ON_DESKTOP, + @JsonProperty("on_desktop") + ON_DESKTOP, - @JsonProperty("on_mobile") - ON_MOBILE - } + @JsonProperty("on_mobile") + ON_MOBILE + } - public enum PushNotificationType { - @JsonProperty("ntfy") - NTFY, + public enum PushNotificationType { + @JsonProperty("ntfy") + NTFY, - @JsonProperty("gotify") - GOTIFY, + @JsonProperty("gotify") + GOTIFY, - @JsonProperty("pushover") - PUSHOVER - } + @JsonProperty("pushover") + PUSHOVER + } - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false, unique = true) - private User user; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private ReadingMode readingMode; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReadingMode readingMode; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private ReadingOrder readingOrder; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReadingOrder readingOrder; - @Column(name = "user_lang", length = 4) - private String language; + @Column(name = "user_lang", length = 4) + private String language; - private boolean showRead; - private boolean scrollMarks; + private boolean showRead; + private boolean scrollMarks; - @Lob - @Column(length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String customCss; + @Lob + @Column(length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String customCss; - @Lob - @Column(length = Integer.MAX_VALUE) - @JdbcTypeCode(Types.LONGVARCHAR) - private String customJs; + @Lob + @Column(length = Integer.MAX_VALUE) + @JdbcTypeCode(Types.LONGVARCHAR) + private String customJs; - @Column(name = "scroll_speed") - private int scrollSpeed; + @Column(name = "scroll_speed") + private int scrollSpeed; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private ScrollMode scrollMode; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ScrollMode scrollMode; - private int entriesToKeepOnTopWhenScrolling; + private int entriesToKeepOnTopWhenScrolling; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private IconDisplayMode starIconDisplayMode; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private IconDisplayMode starIconDisplayMode; - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private IconDisplayMode externalLinkIconDisplayMode; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private IconDisplayMode externalLinkIconDisplayMode; - @Column(name = "primary_color", length = 32) - private String primaryColor; + @Column(name = "primary_color", length = 32) + private String primaryColor; - private boolean markAllAsReadConfirmation; - private boolean markAllAsReadNavigateToNextUnread; - private boolean customContextMenu; - private boolean mobileFooter; - private boolean unreadCountTitle; - private boolean unreadCountFavicon; - private boolean disablePullToRefresh; + private boolean markAllAsReadConfirmation; + private boolean markAllAsReadNavigateToNextUnread; + private boolean customContextMenu; + private boolean mobileFooter; + private boolean unreadCountTitle; + private boolean unreadCountFavicon; + private boolean disablePullToRefresh; - private boolean email; - private boolean gmail; - private boolean facebook; - private boolean twitter; - private boolean tumblr; - private boolean instapaper; - private boolean buffer; + private boolean email; + private boolean gmail; + private boolean facebook; + private boolean twitter; + private boolean tumblr; + private boolean instapaper; + private boolean buffer; - @Embedded - private PushNotificationUserSettings pushNotifications = new PushNotificationUserSettings(); + @Embedded + private PushNotificationUserSettings pushNotifications = new PushNotificationUserSettings(); - @Embeddable - @SuppressWarnings("serial") - @Getter - @Setter - public static class PushNotificationUserSettings implements Serializable { - @Enumerated(EnumType.STRING) - @Column(name = "push_notification_type", length = 16) - private PushNotificationType type; + @Embeddable + @SuppressWarnings("serial") + @Getter + @Setter + public static class PushNotificationUserSettings implements Serializable { + @Enumerated(EnumType.STRING) + @Column(name = "push_notification_type", length = 16) + private PushNotificationType type; - @Column(name = "push_notification_server_url", length = 1024) - private String serverUrl; + @Column(name = "push_notification_server_url", length = 1024) + private String serverUrl; - @Column(name = "push_notification_user_id", length = 512) - private String userId; + @Column(name = "push_notification_user_id", length = 512) + private String userId; - @Column(name = "push_notification_user_secret", length = 512) - private String userSecret; - - @Column(name = "push_notification_topic", length = 256) - private String topic; - } + @Column(name = "push_notification_user_secret", length = 512) + private String userSecret; + @Column(name = "push_notification_topic", length = 256) + private String topic; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java index fad5a625..47cad10f 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java @@ -1,13 +1,5 @@ package com.commafeed.backend.opml; -import java.util.Comparator; -import java.util.Date; -import java.util.List; - -import jakarta.inject.Singleton; - -import org.apache.commons.lang3.ObjectUtils; - import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.model.FeedCategory; @@ -16,75 +8,88 @@ import com.commafeed.backend.model.User; import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Outline; - +import jakarta.inject.Singleton; +import java.util.Comparator; +import java.util.Date; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.ObjectUtils; @RequiredArgsConstructor @Singleton public class OPMLExporter { - private static final Comparator CATEGORY_COMPARATOR = Comparator - .comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0)); - private static final Comparator SUBSCRIPTION_COMPARATOR = Comparator - .comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0)); + private static final Comparator CATEGORY_COMPARATOR = + Comparator.comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0)); + private static final Comparator SUBSCRIPTION_COMPARATOR = + Comparator.comparingInt(e -> ObjectUtils.firstNonNull(e.getPosition(), 0)); - private final FeedCategoryDAO feedCategoryDAO; - private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; - public Opml export(User user) { - Opml opml = new Opml(); - opml.setFeedType("opml_1.0"); - opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName())); - opml.setCreated(new Date()); + public Opml export(User user) { + Opml opml = new Opml(); + opml.setFeedType("opml_1.0"); + opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName())); + opml.setCreated(new Date()); - List categories = feedCategoryDAO.findAll(user); - categories.sort(CATEGORY_COMPARATOR); + List categories = feedCategoryDAO.findAll(user); + categories.sort(CATEGORY_COMPARATOR); - List subscriptions = feedSubscriptionDAO.findAll(user); - subscriptions.sort(SUBSCRIPTION_COMPARATOR); + List subscriptions = feedSubscriptionDAO.findAll(user); + subscriptions.sort(SUBSCRIPTION_COMPARATOR); - // export root categories - for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).toList()) { - opml.getOutlines().add(buildCategoryOutline(cat, categories, subscriptions)); - } + // export root categories + for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).toList()) { + opml.getOutlines().add(buildCategoryOutline(cat, categories, subscriptions)); + } - // export root subscriptions - for (FeedSubscription sub : subscriptions.stream().filter(s -> s.getCategory() == null).toList()) { - opml.getOutlines().add(buildSubscriptionOutline(sub)); - } + // export root subscriptions + for (FeedSubscription sub : + subscriptions.stream().filter(s -> s.getCategory() == null).toList()) { + opml.getOutlines().add(buildSubscriptionOutline(sub)); + } - return opml; + return opml; + } - } + private Outline buildCategoryOutline( + FeedCategory cat, List categories, List subscriptions) { + Outline outline = new Outline(); + outline.setText(cat.getName()); + outline.setTitle(cat.getName()); - private Outline buildCategoryOutline(FeedCategory cat, List categories, List subscriptions) { - Outline outline = new Outline(); - outline.setText(cat.getName()); - outline.setTitle(cat.getName()); + for (FeedCategory child : + categories.stream() + .filter( + c -> + c.getParent() != null + && c.getParent().getId().equals(cat.getId())) + .toList()) { + outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions)); + } - for (FeedCategory child : categories.stream() - .filter(c -> c.getParent() != null && c.getParent().getId().equals(cat.getId())) - .toList()) { - outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions)); - } + for (FeedSubscription sub : + subscriptions.stream() + .filter( + s -> + s.getCategory() != null + && s.getCategory().getId().equals(cat.getId())) + .toList()) { + outline.getChildren().add(buildSubscriptionOutline(sub)); + } + return outline; + } - for (FeedSubscription sub : subscriptions.stream() - .filter(s -> s.getCategory() != null && s.getCategory().getId().equals(cat.getId())) - .toList()) { - outline.getChildren().add(buildSubscriptionOutline(sub)); - } - return outline; - } - - private Outline buildSubscriptionOutline(FeedSubscription sub) { - Outline outline = new Outline(); - outline.setText(sub.getTitle()); - outline.setTitle(sub.getTitle()); - outline.setType("rss"); - outline.getAttributes().add(new Attribute("xmlUrl", sub.getFeed().getUrl())); - if (sub.getFeed().getLink() != null) { - outline.getAttributes().add(new Attribute("htmlUrl", sub.getFeed().getLink())); - } - return outline; - } + private Outline buildSubscriptionOutline(FeedSubscription sub) { + Outline outline = new Outline(); + outline.setText(sub.getTitle()); + outline.setTitle(sub.getTitle()); + outline.setType("rss"); + outline.getAttributes().add(new Attribute("xmlUrl", sub.getFeed().getUrl())); + if (sub.getFeed().getLink() != null) { + outline.getAttributes().add(new Attribute("htmlUrl", sub.getFeed().getLink())); + } + return outline; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java index 8953a9a0..72584f4a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java @@ -1,13 +1,5 @@ package com.commafeed.backend.opml; -import java.io.StringReader; -import java.util.List; - -import jakarta.inject.Singleton; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; - import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.parser.XMLCleaner; @@ -18,71 +10,76 @@ import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Outline; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.WireFeedInput; - +import jakarta.inject.Singleton; +import java.io.StringReader; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; @Slf4j @RequiredArgsConstructor @Singleton public class OPMLImporter { - private final XMLCleaner xmlCleaner; - private final FeedCategoryDAO feedCategoryDAO; - private final FeedSubscriptionService feedSubscriptionService; + private final XMLCleaner xmlCleaner; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedSubscriptionService feedSubscriptionService; - public void importOpml(User user, String xml) throws IllegalArgumentException, FeedException { - xml = xmlCleaner.clean(xml); - if (xml == null) { - throw new IllegalArgumentException("Invalid OPML"); - } + public void importOpml(User user, String xml) throws IllegalArgumentException, FeedException { + xml = xmlCleaner.clean(xml); + if (xml == null) { + throw new IllegalArgumentException("Invalid OPML"); + } - WireFeedInput input = new WireFeedInput(); - Opml feed = (Opml) input.build(new StringReader(xml)); - List outlines = feed.getOutlines(); - for (int i = 0; i < outlines.size(); i++) { - handleOutline(user, outlines.get(i), null, i); - } - } + WireFeedInput input = new WireFeedInput(); + Opml feed = (Opml) input.build(new StringReader(xml)); + List outlines = feed.getOutlines(); + for (int i = 0; i < outlines.size(); i++) { + handleOutline(user, outlines.get(i), null, i); + } + } - private void handleOutline(User user, Outline outline, FeedCategory parent, int position) { - List children = outline.getChildren(); - if (CollectionUtils.isNotEmpty(children)) { - String name = FeedUtils.truncate(outline.getText(), 128); - if (name == null) { - name = FeedUtils.truncate(outline.getTitle(), 128); - } - FeedCategory category = feedCategoryDAO.findByName(user, name, parent); - if (category == null) { - if (StringUtils.isBlank(name)) { - name = "Unnamed category"; - } + private void handleOutline(User user, Outline outline, FeedCategory parent, int position) { + List children = outline.getChildren(); + if (CollectionUtils.isNotEmpty(children)) { + String name = FeedUtils.truncate(outline.getText(), 128); + if (name == null) { + name = FeedUtils.truncate(outline.getTitle(), 128); + } + FeedCategory category = feedCategoryDAO.findByName(user, name, parent); + if (category == null) { + if (StringUtils.isBlank(name)) { + name = "Unnamed category"; + } - category = new FeedCategory(); - category.setName(name); - category.setParent(parent); - category.setUser(user); - category.setPosition(position); - feedCategoryDAO.persist(category); - } + category = new FeedCategory(); + category.setName(name); + category.setParent(parent); + category.setUser(user); + category.setPosition(position); + feedCategoryDAO.persist(category); + } - for (int i = 0; i < children.size(); i++) { - handleOutline(user, children.get(i), category, i); - } - } else { - String name = FeedUtils.truncate(outline.getText(), 128); - if (name == null) { - name = FeedUtils.truncate(outline.getTitle(), 128); - } - if (StringUtils.isBlank(name)) { - name = "Unnamed subscription"; - } - // make sure we continue with the import process even if a feed failed - try { - feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position); - } catch (Exception e) { - log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage()); - } - } - } + for (int i = 0; i < children.size(); i++) { + handleOutline(user, children.get(i), category, i); + } + } else { + String name = FeedUtils.truncate(outline.getText(), 128); + if (name == null) { + name = FeedUtils.truncate(outline.getTitle(), 128); + } + if (StringUtils.isBlank(name)) { + name = "Unnamed subscription"; + } + // make sure we continue with the import process even if a feed failed + try { + feedSubscriptionService.subscribe( + user, outline.getXmlUrl(), name, parent, position); + } catch (Exception e) { + log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage()); + } + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java index 254d1a05..7149cd68 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java @@ -1,38 +1,32 @@ package com.commafeed.backend.rome; -import java.util.Locale; - -import org.jdom2.Document; -import org.jdom2.Element; - import com.rometools.opml.io.impl.OPML10Parser; import com.rometools.rome.feed.WireFeed; import com.rometools.rome.io.FeedException; - import io.quarkus.runtime.annotations.RegisterForReflection; +import java.util.Locale; +import org.jdom2.Document; +import org.jdom2.Element; -/** - * Support for OPML 1.1 parsing - * - */ +/** Support for OPML 1.1 parsing */ @RegisterForReflection public class OPML11Parser extends OPML10Parser { - public OPML11Parser() { - super("opml_1.1"); - } + public OPML11Parser() { + super("opml_1.1"); + } - @Override - public boolean isMyType(Document document) { - Element e = document.getRootElement(); + @Override + public boolean isMyType(Document document) { + Element e = document.getRootElement(); - return e.getName().equals("opml"); + return e.getName().equals("opml"); + } - } - - @Override - public WireFeed parse(Document document, boolean validate, Locale locale) throws IllegalArgumentException, FeedException { - document.getRootElement().getChildren().add(new Element("head")); - return super.parse(document, validate, locale); - } + @Override + public WireFeed parse(Document document, boolean validate, Locale locale) + throws IllegalArgumentException, FeedException { + document.getRootElement().getChildren().add(new Element("head")); + return super.parse(document, validate, locale); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java index 80fc60c7..1791762c 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java @@ -1,12 +1,14 @@ package com.commafeed.backend.service; +import com.steadystate.css.parser.CSSOMParser; +import com.steadystate.css.parser.SACParserCSS21; +import jakarta.inject.Singleton; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - -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; @@ -21,155 +23,212 @@ import org.w3c.css.sac.ErrorHandler; import org.w3c.css.sac.InputSource; import org.w3c.dom.css.CSSStyleDeclaration; -import com.steadystate.css.parser.CSSOMParser; -import com.steadystate.css.parser.SACParserCSS21; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - @RequiredArgsConstructor @Slf4j @Singleton public class FeedEntryContentCleaningService { - private static final Safelist HTML_WHITELIST = buildWhiteList(); - private static final List ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border"); - private static final List ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height"); - private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' }; + private static final Safelist HTML_WHITELIST = buildWhiteList(); + private static final List ALLOWED_IFRAME_CSS_RULES = + Arrays.asList("height", "width", "border"); + private static final List ALLOWED_IMG_CSS_RULES = + Arrays.asList("display", "width", "height"); + private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] {'(', ')'}; - public String clean(String content, String baseUri, boolean keepTextOnly) { - if (StringUtils.isNotBlank(content)) { - baseUri = StringUtils.trimToEmpty(baseUri); + public String clean(String content, String baseUri, boolean keepTextOnly) { + if (StringUtils.isNotBlank(content)) { + baseUri = StringUtils.trimToEmpty(baseUri); - Document dirty = Jsoup.parseBodyFragment(content, baseUri); - Cleaner cleaner = new Cleaner(HTML_WHITELIST); - Document clean = cleaner.clean(dirty); + Document dirty = Jsoup.parseBodyFragment(content, baseUri); + Cleaner cleaner = new Cleaner(HTML_WHITELIST); + Document clean = cleaner.clean(dirty); - for (Element e : clean.select("iframe[style]")) { - String style = e.attr("style"); - String escaped = escapeIFrameCss(style); - e.attr("style", escaped); - } + for (Element e : clean.select("iframe[style]")) { + String style = e.attr("style"); + String escaped = escapeIFrameCss(style); + e.attr("style", escaped); + } - for (Element e : clean.select("img[style]")) { - String style = e.attr("style"); - String escaped = escapeImgCss(style); - e.attr("style", escaped); - } + for (Element e : clean.select("img[style]")) { + String style = e.attr("style"); + String escaped = escapeImgCss(style); + e.attr("style", escaped); + } - clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false)); - Element body = clean.body(); - if (keepTextOnly) { - content = body.text(); - } else { - content = body.html(); - } - } - return content; - } + clean.outputSettings( + new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false)); + Element body = clean.body(); + if (keepTextOnly) { + content = body.text(); + } else { + content = body.html(); + } + } + return content; + } - private static Safelist buildWhiteList() { - Safelist whitelist = new Safelist(); - whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1", - "h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup", - "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul"); + private static Safelist buildWhiteList() { + Safelist whitelist = new Safelist(); + whitelist.addTags( + "a", + "b", + "blockquote", + "br", + "caption", + "cite", + "code", + "col", + "colgroup", + "dd", + "div", + "dl", + "dt", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "i", + "iframe", + "img", + "li", + "ol", + "p", + "pre", + "q", + "small", + "strike", + "strong", + "sub", + "sup", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "tr", + "u", + "ul"); - whitelist.addAttributes("div", "dir"); - whitelist.addAttributes("pre", "dir"); - whitelist.addAttributes("code", "dir"); - whitelist.addAttributes("table", "dir"); - whitelist.addAttributes("p", "dir"); - whitelist.addAttributes("a", "href", "title"); - whitelist.addAttributes("blockquote", "cite"); - whitelist.addAttributes("col", "span", "width"); - whitelist.addAttributes("colgroup", "span", "width"); - whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style"); - whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style"); - whitelist.addAttributes("ol", "start", "type"); - whitelist.addAttributes("q", "cite"); - whitelist.addAttributes("table", "border", "bordercolor", "summary", "width"); - whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width"); - whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width"); - whitelist.addAttributes("ul", "type"); + whitelist.addAttributes("div", "dir"); + whitelist.addAttributes("pre", "dir"); + whitelist.addAttributes("code", "dir"); + whitelist.addAttributes("table", "dir"); + whitelist.addAttributes("p", "dir"); + whitelist.addAttributes("a", "href", "title"); + whitelist.addAttributes("blockquote", "cite"); + whitelist.addAttributes("col", "span", "width"); + whitelist.addAttributes("colgroup", "span", "width"); + whitelist.addAttributes( + "iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style"); + whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style"); + whitelist.addAttributes("ol", "start", "type"); + whitelist.addAttributes("q", "cite"); + whitelist.addAttributes("table", "border", "bordercolor", "summary", "width"); + whitelist.addAttributes( + "td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width"); + whitelist.addAttributes( + "th", + "border", + "bordercolor", + "abbr", + "axis", + "colspan", + "rowspan", + "scope", + "width"); + whitelist.addAttributes("ul", "type"); - whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto"); - whitelist.addProtocols("blockquote", "cite", "http", "https"); - whitelist.addProtocols("img", "src", "http", "https"); - whitelist.addProtocols("q", "cite", "http", "https"); + whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto"); + whitelist.addProtocols("blockquote", "cite", "http", "https"); + whitelist.addProtocols("img", "src", "http", "https"); + whitelist.addProtocols("q", "cite", "http", "https"); - whitelist.addEnforcedAttribute("a", "target", "_blank"); - whitelist.addEnforcedAttribute("a", "rel", "noreferrer"); - return whitelist; - } + whitelist.addEnforcedAttribute("a", "target", "_blank"); + whitelist.addEnforcedAttribute("a", "rel", "noreferrer"); + return whitelist; + } - private String escapeIFrameCss(String orig) { - String rule = ""; - try { - List rules = new ArrayList<>(); - CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig))); + private String escapeIFrameCss(String orig) { + String rule = ""; + try { + List rules = new ArrayList<>(); + CSSStyleDeclaration decl = + buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig))); - for (int i = 0; i < decl.getLength(); i++) { - String property = decl.item(i); - String value = decl.getPropertyValue(property); - if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) { - continue; - } + for (int i = 0; i < decl.getLength(); i++) { + String property = decl.item(i); + String value = decl.getPropertyValue(property); + if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) { + continue; + } - if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) { - rules.add(property + ":" + decl.getPropertyValue(property) + ";"); - } - } - rule = StringUtils.join(rules, ""); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - return rule; - } + if (ALLOWED_IFRAME_CSS_RULES.contains(property) + && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) { + rules.add(property + ":" + decl.getPropertyValue(property) + ";"); + } + } + rule = StringUtils.join(rules, ""); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return rule; + } - private String escapeImgCss(String orig) { - String rule = ""; - try { - List rules = new ArrayList<>(); - CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig))); + private String escapeImgCss(String orig) { + String rule = ""; + try { + List rules = new ArrayList<>(); + CSSStyleDeclaration decl = + buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig))); - for (int i = 0; i < decl.getLength(); i++) { - String property = decl.item(i); - String value = decl.getPropertyValue(property); - if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) { - continue; - } + for (int i = 0; i < decl.getLength(); i++) { + String property = decl.item(i); + String value = decl.getPropertyValue(property); + if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) { + continue; + } - if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) { - rules.add(property + ":" + decl.getPropertyValue(property) + ";"); - } - } - rule = StringUtils.join(rules, ""); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - return rule; - } + if (ALLOWED_IMG_CSS_RULES.contains(property) + && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) { + rules.add(property + ":" + decl.getPropertyValue(property) + ";"); + } + } + rule = StringUtils.join(rules, ""); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return rule; + } - private CSSOMParser buildCssParser() { - CSSOMParser parser = new CSSOMParser(new SACParserCSS21()); + private CSSOMParser buildCssParser() { + CSSOMParser parser = new CSSOMParser(new SACParserCSS21()); - parser.setErrorHandler(new ErrorHandler() { - @Override - public void warning(CSSParseException exception) throws CSSException { - log.debug("warning while parsing css: {}", exception.getMessage(), exception); - } + parser.setErrorHandler( + new ErrorHandler() { + @Override + public void warning(CSSParseException exception) throws CSSException { + log.debug( + "warning while parsing css: {}", exception.getMessage(), exception); + } - @Override - public void error(CSSParseException exception) throws CSSException { - log.debug("error while parsing css: {}", exception.getMessage(), exception); - } + @Override + public void error(CSSParseException exception) throws CSSException { + log.debug("error while parsing css: {}", exception.getMessage(), exception); + } - @Override - public void fatalError(CSSParseException exception) throws CSSException { - log.debug("fatal error while parsing css: {}", exception.getMessage(), exception); - } - }); + @Override + public void fatalError(CSSParseException exception) throws CSSException { + log.debug( + "fatal error while parsing css: {}", + exception.getMessage(), + exception); + } + }); - return parser; - } + return parser; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java index 8ae1f861..ef723461 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java @@ -1,11 +1,5 @@ package com.commafeed.backend.service; -import java.util.Optional; - -import jakarta.inject.Singleton; - -import org.apache.commons.lang3.StringUtils; - import com.commafeed.backend.Digests; import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.feed.FeedUtils; @@ -13,59 +7,65 @@ import com.commafeed.backend.feed.parser.FeedParserResult.Content; import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure; import com.commafeed.backend.feed.parser.FeedParserResult.Media; import com.commafeed.backend.model.FeedEntryContent; - +import jakarta.inject.Singleton; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; @RequiredArgsConstructor @Singleton public class FeedEntryContentService { - private final FeedEntryContentDAO feedEntryContentDAO; - private final FeedEntryContentCleaningService cleaningService; + private final FeedEntryContentDAO feedEntryContentDAO; + private final FeedEntryContentCleaningService cleaningService; - /** - * this is NOT thread-safe - */ - public FeedEntryContent findOrCreate(Content content, String baseUrl) { - FeedEntryContent entryContent = buildContent(content, baseUrl); - Optional existing = feedEntryContentDAO.findExisting(entryContent.getContentHash(), entryContent.getTitleHash()) - .stream() - .filter(entryContent::equivalentTo) - .findFirst(); - if (existing.isPresent()) { - return existing.get(); - } else { - feedEntryContentDAO.persist(entryContent); - return entryContent; - } - } + /** this is NOT thread-safe */ + public FeedEntryContent findOrCreate(Content content, String baseUrl) { + FeedEntryContent entryContent = buildContent(content, baseUrl); + Optional existing = + feedEntryContentDAO + .findExisting(entryContent.getContentHash(), entryContent.getTitleHash()) + .stream() + .filter(entryContent::equivalentTo) + .findFirst(); + if (existing.isPresent()) { + return existing.get(); + } else { + feedEntryContentDAO.persist(entryContent); + return entryContent; + } + } - private FeedEntryContent buildContent(Content content, String baseUrl) { - FeedEntryContent entryContent = new FeedEntryContent(); - entryContent.setTitleHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.title()))); - entryContent.setContentHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.content()))); - entryContent.setTitle(FeedUtils.truncate(cleaningService.clean(content.title(), baseUrl, true), 2048)); - entryContent.setContent(cleaningService.clean(content.content(), baseUrl, false)); - entryContent.setAuthor(FeedUtils.truncate(cleaningService.clean(content.author(), baseUrl, true), 128)); - entryContent.setCategories(FeedUtils.truncate(content.categories(), 4096)); - entryContent.setDirection( - FeedUtils.isRTL(content.title(), content.content()) ? FeedEntryContent.Direction.RTL : FeedEntryContent.Direction.LTR); + private FeedEntryContent buildContent(Content content, String baseUrl) { + FeedEntryContent entryContent = new FeedEntryContent(); + entryContent.setTitleHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.title()))); + entryContent.setContentHash(Digests.sha1Hex(StringUtils.trimToEmpty(content.content()))); + entryContent.setTitle( + FeedUtils.truncate(cleaningService.clean(content.title(), baseUrl, true), 2048)); + entryContent.setContent(cleaningService.clean(content.content(), baseUrl, false)); + entryContent.setAuthor( + FeedUtils.truncate(cleaningService.clean(content.author(), baseUrl, true), 128)); + entryContent.setCategories(FeedUtils.truncate(content.categories(), 4096)); + entryContent.setDirection( + FeedUtils.isRTL(content.title(), content.content()) + ? FeedEntryContent.Direction.RTL + : FeedEntryContent.Direction.LTR); - Enclosure enclosure = content.enclosure(); - if (enclosure != null) { - entryContent.setEnclosureUrl(FeedUtils.truncate(enclosure.url(), 2048)); - entryContent.setEnclosureType(enclosure.type()); - } + Enclosure enclosure = content.enclosure(); + if (enclosure != null) { + entryContent.setEnclosureUrl(FeedUtils.truncate(enclosure.url(), 2048)); + entryContent.setEnclosureType(enclosure.type()); + } - Media media = content.media(); - if (media != null) { - entryContent.setMediaDescription(cleaningService.clean(media.description(), baseUrl, false)); - entryContent.setMediaThumbnailUrl(FeedUtils.truncate(media.thumbnailUrl(), 2048)); - entryContent.setMediaThumbnailWidth(media.thumbnailWidth()); - entryContent.setMediaThumbnailHeight(media.thumbnailHeight()); - } - - return entryContent; - } + Media media = content.media(); + if (media != null) { + entryContent.setMediaDescription( + cleaningService.clean(media.description(), baseUrl, false)); + entryContent.setMediaThumbnailUrl(FeedUtils.truncate(media.thumbnailUrl(), 2048)); + entryContent.setMediaThumbnailWidth(media.thumbnailWidth()); + entryContent.setMediaThumbnailHeight(media.thumbnailHeight()); + } + return entryContent; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java index 52c04552..93986083 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java @@ -1,22 +1,7 @@ package com.commafeed.backend.service; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import jakarta.inject.Singleton; - -import org.apache.commons.lang3.StringUtils; -import org.jsoup.Jsoup; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.model.FeedEntry; - import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelValidationException; import dev.cel.common.types.SimpleType; @@ -25,83 +10,112 @@ import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; +import jakarta.inject.Singleton; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; @RequiredArgsConstructor @Singleton public class FeedEntryFilteringService { - private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() - .addVar("title", SimpleType.STRING) - .addVar("titleLower", SimpleType.STRING) - .addVar("author", SimpleType.STRING) - .addVar("authorLower", SimpleType.STRING) - .addVar("content", SimpleType.STRING) - .addVar("contentLower", SimpleType.STRING) - .addVar("url", SimpleType.STRING) - .addVar("urlLower", SimpleType.STRING) - .addVar("categories", SimpleType.STRING) - .addVar("categoriesLower", SimpleType.STRING) - .build(); - private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("title", SimpleType.STRING) + .addVar("titleLower", SimpleType.STRING) + .addVar("author", SimpleType.STRING) + .addVar("authorLower", SimpleType.STRING) + .addVar("content", SimpleType.STRING) + .addVar("contentLower", SimpleType.STRING) + .addVar("url", SimpleType.STRING) + .addVar("urlLower", SimpleType.STRING) + .addVar("categories", SimpleType.STRING) + .addVar("categoriesLower", SimpleType.STRING) + .build(); + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder().build(); - private final ExecutorService executor = Executors.newCachedThreadPool(); - private final CommaFeedConfiguration config; + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final CommaFeedConfiguration config; - public boolean filterMatchesEntry(String filter, FeedEntry entry) throws FeedEntryFilterException { - if (StringUtils.isBlank(filter)) { - return true; - } + public boolean filterMatchesEntry(String filter, FeedEntry entry) + throws FeedEntryFilterException { + if (StringUtils.isBlank(filter)) { + return true; + } - String title = entry.getContent().getTitle() == null ? "" : Jsoup.parse(entry.getContent().getTitle()).text(); - String author = entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor(); - String content = entry.getContent().getContent() == null ? "" : Jsoup.parse(entry.getContent().getContent()).text(); - String url = entry.getUrl() == null ? "" : entry.getUrl(); - String categories = entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories(); + String title = + entry.getContent().getTitle() == null + ? "" + : Jsoup.parse(entry.getContent().getTitle()).text(); + String author = + entry.getContent().getAuthor() == null ? "" : entry.getContent().getAuthor(); + String content = + entry.getContent().getContent() == null + ? "" + : Jsoup.parse(entry.getContent().getContent()).text(); + String url = entry.getUrl() == null ? "" : entry.getUrl(); + String categories = + entry.getContent().getCategories() == null + ? "" + : entry.getContent().getCategories(); - Map data = new HashMap<>(); - data.put("title", title); - data.put("titleLower", title.toLowerCase()); + Map data = new HashMap<>(); + data.put("title", title); + data.put("titleLower", title.toLowerCase()); - data.put("author", author); - data.put("authorLower", author.toLowerCase()); + data.put("author", author); + data.put("authorLower", author.toLowerCase()); - data.put("content", content); - data.put("contentLower", content.toLowerCase()); + data.put("content", content); + data.put("contentLower", content.toLowerCase()); - data.put("url", url); - data.put("urlLower", url.toLowerCase()); + data.put("url", url); + data.put("urlLower", url.toLowerCase()); - data.put("categories", categories); - data.put("categoriesLower", categories.toLowerCase()); + data.put("categories", categories); + data.put("categoriesLower", categories.toLowerCase()); - Future future = executor.submit(() -> evaluateCelExpression(filter, data)); - Object result; - try { - result = future.get(config.feedRefresh().filteringExpressionEvaluationTimeout().toMillis(), TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e); - } catch (ExecutionException e) { - throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e); - } catch (TimeoutException e) { - throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e); - } + Future future = executor.submit(() -> evaluateCelExpression(filter, data)); + Object result; + try { + result = + future.get( + config.feedRefresh().filteringExpressionEvaluationTimeout().toMillis(), + TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FeedEntryFilterException( + "interrupted while evaluating expression " + filter, e); + } catch (ExecutionException e) { + throw new FeedEntryFilterException( + "Exception while evaluating expression " + filter, e); + } catch (TimeoutException e) { + throw new FeedEntryFilterException("Took too long evaluating expression " + filter, e); + } - return Boolean.TRUE.equals(result); - } + return Boolean.TRUE.equals(result); + } - private Object evaluateCelExpression(String expression, Map data) - throws CelValidationException, CelEvaluationException { - CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); - CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); - return program.eval(data); - } + private Object evaluateCelExpression(String expression, Map data) + throws CelValidationException, CelEvaluationException { + CelAbstractSyntaxTree ast = CEL_COMPILER.compile(expression).getAst(); + CelRuntime.Program program = CEL_RUNTIME.createProgram(ast); + return program.eval(data); + } - @SuppressWarnings("serial") - public static class FeedEntryFilterException extends Exception { - public FeedEntryFilterException(String message, Throwable t) { - super(message, t); - } - } + @SuppressWarnings("serial") + public static class FeedEntryFilterException extends Exception { + public FeedEntryFilterException(String message, Throwable t) { + super(message, t); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java index ac5b1d59..55ae282e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java @@ -1,12 +1,5 @@ package com.commafeed.backend.service; -import java.time.Instant; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import jakarta.inject.Singleton; - import com.commafeed.backend.Digests; import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -20,7 +13,11 @@ import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; - +import jakarta.inject.Singleton; +import java.time.Instant; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,113 +26,146 @@ import lombok.extern.slf4j.Slf4j; @Singleton public class FeedEntryService { - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final FeedEntryDAO feedEntryDAO; - private final FeedEntryStatusDAO feedEntryStatusDAO; - private final FeedEntryContentService feedEntryContentService; - private final FeedEntryFilteringService feedEntryFilteringService; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedEntryDAO feedEntryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + private final FeedEntryContentService feedEntryContentService; + private final FeedEntryFilteringService feedEntryFilteringService; - public FeedEntry find(Feed feed, Entry entry) { - String guidHash = Digests.sha1Hex(entry.guid()); - return feedEntryDAO.findExisting(guidHash, feed); - } + public FeedEntry find(Feed feed, Entry entry) { + String guidHash = Digests.sha1Hex(entry.guid()); + return feedEntryDAO.findExisting(guidHash, feed); + } - public FeedEntry create(Feed feed, Entry entry) { - FeedEntry feedEntry = new FeedEntry(); - feedEntry.setGuid(FeedUtils.truncate(entry.guid(), 2048)); - feedEntry.setGuidHash(Digests.sha1Hex(entry.guid())); - feedEntry.setUrl(FeedUtils.truncate(entry.url(), 2048)); - feedEntry.setPublished(entry.published()); - feedEntry.setInserted(Instant.now()); - feedEntry.setFeed(feed); - feedEntry.setContent(feedEntryContentService.findOrCreate(entry.content(), feed.getLink())); + public FeedEntry create(Feed feed, Entry entry) { + FeedEntry feedEntry = new FeedEntry(); + feedEntry.setGuid(FeedUtils.truncate(entry.guid(), 2048)); + feedEntry.setGuidHash(Digests.sha1Hex(entry.guid())); + feedEntry.setUrl(FeedUtils.truncate(entry.url(), 2048)); + feedEntry.setPublished(entry.published()); + feedEntry.setInserted(Instant.now()); + feedEntry.setFeed(feed); + feedEntry.setContent(feedEntryContentService.findOrCreate(entry.content(), feed.getLink())); - feedEntryDAO.persist(feedEntry); - return feedEntry; - } + feedEntryDAO.persist(feedEntry); + return feedEntry; + } - public List removeExistingEntries(Feed feed, List entries) { - Set guidHashes = entries.stream().map(e -> Digests.sha1Hex(e.guid())).collect(Collectors.toSet()); - Set existingGuidHashes = feedEntryDAO.findExistingGuidHashes(guidHashes, feed); - return entries.stream().filter(e -> !existingGuidHashes.contains(Digests.sha1Hex(e.guid()))).toList(); - } + public List removeExistingEntries(Feed feed, List entries) { + Set guidHashes = + entries.stream().map(e -> Digests.sha1Hex(e.guid())).collect(Collectors.toSet()); + Set existingGuidHashes = feedEntryDAO.findExistingGuidHashes(guidHashes, feed); + return entries.stream() + .filter(e -> !existingGuidHashes.contains(Digests.sha1Hex(e.guid()))) + .toList(); + } - public boolean applyFilter(FeedSubscription sub, FeedEntry entry) { - boolean matches = true; - try { - matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry); - } catch (FeedEntryFilterException e) { - log.error("could not evaluate filter {}", sub.getFilter(), e); - } + public boolean applyFilter(FeedSubscription sub, FeedEntry entry) { + boolean matches = true; + try { + matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry); + } catch (FeedEntryFilterException e) { + log.error("could not evaluate filter {}", sub.getFilter(), e); + } - if (!matches) { - FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry); - status.setRead(true); - feedEntryStatusDAO.persist(status); - } + if (!matches) { + FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry); + status.setRead(true); + feedEntryStatusDAO.persist(status); + } - return matches; - } + return matches; + } - public void markEntry(User user, Long entryId, boolean read) { - FeedEntry entry = feedEntryDAO.findById(entryId); - if (entry == null) { - return; - } + public void markEntry(User user, Long entryId, boolean read) { + FeedEntry entry = feedEntryDAO.findById(entryId); + if (entry == null) { + return; + } - FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed()); - if (sub == null) { - return; - } + FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed()); + if (sub == null) { + return; + } - FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); - if (status.isMarkable()) { - status.setRead(read); - feedEntryStatusDAO.merge(status); - } - } + FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); + if (status.isMarkable()) { + status.setRead(read); + feedEntryStatusDAO.merge(status); + } + } - public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) { + public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) { - FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId); - if (sub == null) { - return; - } + FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId); + if (sub == null) { + return; + } - FeedEntry entry = feedEntryDAO.findById(entryId); - if (entry == null) { - return; - } + FeedEntry entry = feedEntryDAO.findById(entryId); + if (entry == null) { + return; + } - FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); - status.setStarred(starred); - feedEntryStatusDAO.merge(status); - } + FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); + status.setStarred(starred); + feedEntryStatusDAO.merge(status); + } - public void markSubscriptionEntries(User user, List subscriptions, Instant olderThan, Instant insertedBefore, - List keywords) { - List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, - false, null, null, null); - markList(statuses, olderThan, insertedBefore); - } + public void markSubscriptionEntries( + User user, + List subscriptions, + Instant olderThan, + Instant insertedBefore, + List keywords) { + List statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subscriptions, + true, + keywords, + null, + -1, + -1, + null, + false, + null, + null, + null); + markList(statuses, olderThan, insertedBefore); + } - public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) { - List statuses = feedEntryStatusDAO.findStarred(user, null, null, -1, -1, null, false); - markList(statuses, olderThan, insertedBefore); - } + public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) { + List statuses = + feedEntryStatusDAO.findStarred(user, null, null, -1, -1, null, false); + markList(statuses, olderThan, insertedBefore); + } - private void markList(List statuses, Instant olderThan, Instant insertedBefore) { - List statusesToMark = statuses.stream().filter(FeedEntryStatus::isMarkable).filter(s -> { - Instant entryDate = s.getEntry().getPublished(); - return olderThan == null || entryDate == null || entryDate.isBefore(olderThan); - }).filter(s -> { - Instant insertedDate = s.getEntry().getInserted(); - return insertedBefore == null || insertedDate == null || insertedDate.isBefore(insertedBefore); - }).toList(); + private void markList( + List statuses, Instant olderThan, Instant insertedBefore) { + List statusesToMark = + statuses.stream() + .filter(FeedEntryStatus::isMarkable) + .filter( + s -> { + Instant entryDate = s.getEntry().getPublished(); + return olderThan == null + || entryDate == null + || entryDate.isBefore(olderThan); + }) + .filter( + s -> { + Instant insertedDate = s.getEntry().getInserted(); + return insertedBefore == null + || insertedDate == null + || insertedDate.isBefore(insertedBefore); + }) + .toList(); - statusesToMark.forEach(s -> { - s.setRead(true); - feedEntryStatusDAO.merge(s); - }); - } + statusesToMark.forEach( + s -> { + s.setRead(true); + feedEntryStatusDAO.merge(s); + }); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java index 742e68be..2a428fab 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java @@ -1,43 +1,42 @@ package com.commafeed.backend.service; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import jakarta.inject.Singleton; - import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.User; - +import jakarta.inject.Singleton; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class FeedEntryTagService { - private final FeedEntryDAO feedEntryDAO; - private final FeedEntryTagDAO feedEntryTagDAO; + private final FeedEntryDAO feedEntryDAO; + private final FeedEntryTagDAO feedEntryTagDAO; - public void updateTags(User user, Long entryId, List tagNames) { - FeedEntry entry = feedEntryDAO.findById(entryId); - if (entry == null) { - return; - } + public void updateTags(User user, Long entryId, List tagNames) { + FeedEntry entry = feedEntryDAO.findById(entryId); + if (entry == null) { + return; + } - List existingTags = feedEntryTagDAO.findByEntry(user, entry); - Set existingTagNames = existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet()); + List existingTags = feedEntryTagDAO.findByEntry(user, entry); + Set existingTagNames = + existingTags.stream().map(FeedEntryTag::getName).collect(Collectors.toSet()); - List addList = tagNames.stream() - .filter(name -> !existingTagNames.contains(name)) - .map(name -> new FeedEntryTag(user, entry, name)) - .toList(); - List removeList = existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).toList(); - - addList.forEach(feedEntryTagDAO::persist); - feedEntryTagDAO.delete(removeList); - } + List addList = + tagNames.stream() + .filter(name -> !existingTagNames.contains(name)) + .map(name -> new FeedEntryTag(user, entry, name)) + .toList(); + List removeList = + existingTags.stream().filter(tag -> !tagNames.contains(tag.getName())).toList(); + addList.forEach(feedEntryTagDAO::persist); + feedEntryTagDAO.delete(removeList); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedFaviconService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedFaviconService.java index e7859e1f..536e3d4a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedFaviconService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedFaviconService.java @@ -1,71 +1,72 @@ package com.commafeed.backend.service; -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import jakarta.inject.Singleton; -import jakarta.ws.rs.core.MediaType; - -import org.apache.commons.lang3.ArrayUtils; - import com.commafeed.backend.favicon.Favicon; import com.commafeed.backend.favicon.FaviconFetcher; import com.commafeed.backend.model.Feed; import com.google.common.io.Resources; - import io.quarkus.arc.All; +import jakarta.inject.Singleton; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Set; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; @Singleton @Slf4j public class FeedFaviconService { - private static final Set ICON_MIMETYPE_BLACKLIST = Set.of(MediaType.APPLICATION_XML_TYPE, MediaType.TEXT_HTML_TYPE); - private static final long MIN_ICON_LENGTH = 100; - private static final long MAX_ICON_LENGTH = 100000; + private static final Set ICON_MIMETYPE_BLACKLIST = + Set.of(MediaType.APPLICATION_XML_TYPE, MediaType.TEXT_HTML_TYPE); + private static final long MIN_ICON_LENGTH = 100; + private static final long MAX_ICON_LENGTH = 100000; - private final List faviconFetchers; - private final Favicon defaultFavicon; + private final List faviconFetchers; + private final Favicon defaultFavicon; - public FeedFaviconService(@All List faviconFetchers) throws IOException { - this.faviconFetchers = faviconFetchers; - this.defaultFavicon = new Favicon( - Resources.toByteArray(Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif"))), "image/gif"); - } + public FeedFaviconService(@All List faviconFetchers) throws IOException { + this.faviconFetchers = faviconFetchers; + this.defaultFavicon = + new Favicon( + Resources.toByteArray( + Objects.requireNonNull( + getClass().getResource("/images/default_favicon.gif"))), + "image/gif"); + } - public Favicon fetchFavicon(Feed feed) { - for (FaviconFetcher faviconFetcher : faviconFetchers) { - Favicon icon = faviconFetcher.fetch(feed); - if (isFaviconValid(icon)) { - return icon; - } - } - return defaultFavicon; - } + public Favicon fetchFavicon(Feed feed) { + for (FaviconFetcher faviconFetcher : faviconFetchers) { + Favicon icon = faviconFetcher.fetch(feed); + if (isFaviconValid(icon)) { + return icon; + } + } + return defaultFavicon; + } - private static boolean isFaviconValid(Favicon favicon) { - if (favicon == null || ArrayUtils.isEmpty(favicon.icon())) { - return false; - } + private static boolean isFaviconValid(Favicon favicon) { + if (favicon == null || ArrayUtils.isEmpty(favicon.icon())) { + return false; + } - long length = favicon.icon().length; - if (length < MIN_ICON_LENGTH) { - log.debug("Length {} below MIN_ICON_LENGTH {}", length, MIN_ICON_LENGTH); - return false; - } + long length = favicon.icon().length; + if (length < MIN_ICON_LENGTH) { + log.debug("Length {} below MIN_ICON_LENGTH {}", length, MIN_ICON_LENGTH); + return false; + } - if (length > MAX_ICON_LENGTH) { - log.debug("Length {} greater than MAX_ICON_LENGTH {}", length, MAX_ICON_LENGTH); - return false; - } + if (length > MAX_ICON_LENGTH) { + log.debug("Length {} greater than MAX_ICON_LENGTH {}", length, MAX_ICON_LENGTH); + return false; + } - if (ICON_MIMETYPE_BLACKLIST.stream().anyMatch(bl -> bl.isCompatible(favicon.mediaType()))) { - log.debug("Content-Type {} is blacklisted", favicon.mediaType()); - return false; - } + if (ICON_MIMETYPE_BLACKLIST.stream().anyMatch(bl -> bl.isCompatible(favicon.mediaType()))) { + log.debug("Content-Type {} is blacklisted", favicon.mediaType()); + return false; + } - return true; - } + return true; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java index e76e4f7b..3f76d735 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java @@ -1,46 +1,42 @@ package com.commafeed.backend.service; -import java.time.Instant; - -import jakarta.inject.Singleton; - import com.commafeed.backend.Digests; import com.commafeed.backend.Urls; import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Models; - +import jakarta.inject.Singleton; +import java.time.Instant; import lombok.RequiredArgsConstructor; @Singleton @RequiredArgsConstructor public class FeedService { - private final FeedDAO feedDAO; + private final FeedDAO feedDAO; - public synchronized Feed findOrCreate(String url) { - String normalizedUrl = Urls.normalize(url); - String normalizedUrlHash = Digests.sha1Hex(normalizedUrl); - Feed feed = feedDAO.findByUrl(normalizedUrl, normalizedUrlHash); - if (feed == null) { - feed = new Feed(); - feed.setUrl(url); - feed.setNormalizedUrl(normalizedUrl); - feed.setNormalizedUrlHash(normalizedUrlHash); - feed.setDisabledUntil(Models.MINIMUM_INSTANT); - feedDAO.persist(feed); - } - return feed; - } - - public void update(Feed feed) { - String normalized = Urls.normalize(feed.getUrl()); - feed.setNormalizedUrl(normalized); - feed.setNormalizedUrlHash(Digests.sha1Hex(normalized)); - feed.setLastUpdated(Instant.now()); - feed.setEtagHeader(FeedUtils.truncate(feed.getEtagHeader(), 255)); - feedDAO.merge(feed); - } + public synchronized Feed findOrCreate(String url) { + String normalizedUrl = Urls.normalize(url); + String normalizedUrlHash = Digests.sha1Hex(normalizedUrl); + Feed feed = feedDAO.findByUrl(normalizedUrl, normalizedUrlHash); + if (feed == null) { + feed = new Feed(); + feed.setUrl(url); + feed.setNormalizedUrl(normalizedUrl); + feed.setNormalizedUrlHash(normalizedUrlHash); + feed.setDisabledUntil(Models.MINIMUM_INSTANT); + feedDAO.persist(feed); + } + return feed; + } + public void update(Feed feed) { + String normalized = Urls.normalize(feed.getUrl()); + feed.setNormalizedUrl(normalized); + feed.setNormalizedUrlHash(Digests.sha1Hex(normalized)); + feed.setLastUpdated(Instant.now()); + feed.setEtagHeader(FeedUtils.truncate(feed.getEtagHeader(), 255)); + feedDAO.merge(feed); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java index 4a5d1e48..14f5c0da 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java @@ -1,12 +1,5 @@ package com.commafeed.backend.service; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import jakarta.inject.Singleton; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.Urls; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -18,107 +11,125 @@ import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UnreadCount; - +import jakarta.inject.Singleton; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class FeedSubscriptionService { - private final FeedEntryStatusDAO feedEntryStatusDAO; - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final FeedService feedService; - private final FeedRefreshEngine feedRefreshEngine; - private final CommaFeedConfiguration config; + private final FeedEntryStatusDAO feedEntryStatusDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedService feedService; + private final FeedRefreshEngine feedRefreshEngine; + private final CommaFeedConfiguration config; - public FeedSubscriptionService(FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO, FeedService feedService, - FeedRefreshEngine feedRefreshEngine, CommaFeedConfiguration config) { - this.feedEntryStatusDAO = feedEntryStatusDAO; - this.feedSubscriptionDAO = feedSubscriptionDAO; - this.feedService = feedService; - this.feedRefreshEngine = feedRefreshEngine; - this.config = config; + public FeedSubscriptionService( + FeedEntryStatusDAO feedEntryStatusDAO, + FeedSubscriptionDAO feedSubscriptionDAO, + FeedService feedService, + FeedRefreshEngine feedRefreshEngine, + CommaFeedConfiguration config) { + this.feedEntryStatusDAO = feedEntryStatusDAO; + this.feedSubscriptionDAO = feedSubscriptionDAO; + this.feedService = feedService; + this.feedRefreshEngine = feedRefreshEngine; + this.config = config; - // automatically refresh new feeds after they are subscribed to - // we need to use this hook because the feed needs to have been persisted before being processed by the feed engine - feedSubscriptionDAO.onPostCommitInsert(sub -> { - Feed feed = sub.getFeed(); - if (feed.getDisabledUntil() == null || feed.getDisabledUntil().isBefore(Instant.now())) { - feedRefreshEngine.refreshImmediately(feed); - } - }); - } + // automatically refresh new feeds after they are subscribed to + // we need to use this hook because the feed needs to have been persisted before being + // processed + // by the feed engine + feedSubscriptionDAO.onPostCommitInsert( + sub -> { + Feed feed = sub.getFeed(); + if (feed.getDisabledUntil() == null + || feed.getDisabledUntil().isBefore(Instant.now())) { + feedRefreshEngine.refreshImmediately(feed); + } + }); + } - public long subscribe(User user, String url, String title, FeedCategory category, int position) { - Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser(); - if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) { - String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)", - maxFeedsPerUser); - throw new FeedSubscriptionException(message); - } + public long subscribe( + User user, String url, String title, FeedCategory category, int position) { + Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser(); + if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) { + String message = + String.format( + "You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)", + maxFeedsPerUser); + throw new FeedSubscriptionException(message); + } - Feed feed = feedService.findOrCreate(url); + Feed feed = feedService.findOrCreate(url); - // upgrade feed to https if it was using http - if (Urls.isHttp(feed.getUrl()) && Urls.isHttps(url)) { - feed.setUrl(url); - } + // upgrade feed to https if it was using http + if (Urls.isHttp(feed.getUrl()) && Urls.isHttps(url)) { + feed.setUrl(url); + } - FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed); - if (sub == null) { - sub = new FeedSubscription(); - sub.setFeed(feed); - sub.setUser(user); - } - sub.setCategory(category); - sub.setPosition(position); - sub.setTitle(FeedUtils.truncate(title, 128)); - return feedSubscriptionDAO.merge(sub).getId(); - } + FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed); + if (sub == null) { + sub = new FeedSubscription(); + sub.setFeed(feed); + sub.setUser(user); + } + sub.setCategory(category); + sub.setPosition(position); + sub.setTitle(FeedUtils.truncate(title, 128)); + return feedSubscriptionDAO.merge(sub).getId(); + } - public boolean unsubscribe(User user, Long subId) { - FeedSubscription sub = feedSubscriptionDAO.findById(user, subId); - if (sub != null) { - feedSubscriptionDAO.delete(sub); - return true; - } else { - return false; - } - } + public boolean unsubscribe(User user, Long subId) { + FeedSubscription sub = feedSubscriptionDAO.findById(user, subId); + if (sub != null) { + feedSubscriptionDAO.delete(sub); + return true; + } else { + return false; + } + } - public void refreshAll(User user) throws ForceFeedRefreshTooSoonException { - Instant lastForceRefresh = user.getLastForceRefresh(); - if (lastForceRefresh != null && lastForceRefresh.plus(config.feedRefresh().forceRefreshCooldownDuration()).isAfter(Instant.now())) { - throw new ForceFeedRefreshTooSoonException(); - } + public void refreshAll(User user) throws ForceFeedRefreshTooSoonException { + Instant lastForceRefresh = user.getLastForceRefresh(); + if (lastForceRefresh != null + && lastForceRefresh + .plus(config.feedRefresh().forceRefreshCooldownDuration()) + .isAfter(Instant.now())) { + throw new ForceFeedRefreshTooSoonException(); + } - List subs = feedSubscriptionDAO.findAll(user); - for (FeedSubscription sub : subs) { - Feed feed = sub.getFeed(); - feedRefreshEngine.refreshImmediately(feed); - } + List subs = feedSubscriptionDAO.findAll(user); + for (FeedSubscription sub : subs) { + Feed feed = sub.getFeed(); + feedRefreshEngine.refreshImmediately(feed); + } - user.setLastForceRefresh(Instant.now()); - } + user.setLastForceRefresh(Instant.now()); + } - public Map getUnreadCount(User user) { - return feedSubscriptionDAO.findAll(user) - .stream() - .collect(Collectors.toMap(FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount)); - } + public Map getUnreadCount(User user) { + return feedSubscriptionDAO.findAll(user).stream() + .collect( + Collectors.toMap( + FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount)); + } - @SuppressWarnings("serial") - public static class FeedSubscriptionException extends RuntimeException { - private FeedSubscriptionException(String msg) { - super(msg); - } - } - - @SuppressWarnings("serial") - public static class ForceFeedRefreshTooSoonException extends Exception { - private ForceFeedRefreshTooSoonException() { - super(); - } - } + @SuppressWarnings("serial") + public static class FeedSubscriptionException extends RuntimeException { + private FeedSubscriptionException(String msg) { + super(msg); + } + } + @SuppressWarnings("serial") + public static class ForceFeedRefreshTooSoonException extends Exception { + private ForceFeedRefreshTooSoonException() { + super(); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java index 24147d4b..825902b6 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java @@ -1,21 +1,19 @@ package com.commafeed.backend.service; -import jakarta.inject.Singleton; - import com.commafeed.backend.model.User; - import io.quarkus.mailer.Mail; import io.quarkus.mailer.Mailer; +import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class MailService { - private final Mailer mailer; + private final Mailer mailer; - public void sendMail(User user, String subject, String content) { - Mail mail = Mail.withHtml(user.getEmail(), "CommaFeed - " + subject, content); - mailer.send(mail); - } + public void sendMail(User user, String subject, String content) { + Mail mail = Mail.withHtml(user.getEmail(), "CommaFeed - " + subject, content); + mailer.send(mail); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java index 2e473da3..0ab8cd25 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java @@ -1,20 +1,16 @@ package com.commafeed.backend.service; +import jakarta.inject.Singleton; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.KeySpec; - import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; - -import jakarta.inject.Singleton; - -import org.apache.commons.lang3.StringUtils; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html @Slf4j @@ -22,72 +18,71 @@ import lombok.extern.slf4j.Slf4j; @Singleton public class PasswordEncryptionService { - public boolean authenticate(String attemptedPassword, byte[] encryptedPassword, byte[] salt) { - if (StringUtils.isBlank(attemptedPassword)) { - return false; - } - // Encrypt the clear-text password using the same salt that was used to - // encrypt the original password - byte[] encryptedAttemptedPassword = null; - try { - encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt); - } catch (Exception e) { - // should never happen - log.error(e.getMessage(), e); - } + public boolean authenticate(String attemptedPassword, byte[] encryptedPassword, byte[] salt) { + if (StringUtils.isBlank(attemptedPassword)) { + return false; + } + // Encrypt the clear-text password using the same salt that was used to + // encrypt the original password + byte[] encryptedAttemptedPassword = null; + try { + encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt); + } catch (Exception e) { + // should never happen + log.error(e.getMessage(), e); + } - if (encryptedAttemptedPassword == null) { - return false; - } + if (encryptedAttemptedPassword == null) { + return false; + } - // Authentication succeeds if encrypted password that the user entered - // is equal to the stored hash - return MessageDigest.isEqual(encryptedPassword, encryptedAttemptedPassword); - } + // Authentication succeeds if encrypted password that the user entered + // is equal to the stored hash + return MessageDigest.isEqual(encryptedPassword, encryptedAttemptedPassword); + } - public byte[] getEncryptedPassword(String password, byte[] salt) { - // PBKDF2 with SHA-1 as the hashing algorithm. Note that the NIST - // specifically names SHA-1 as an acceptable hashing algorithm for - // PBKDF2 - String algorithm = "PBKDF2WithHmacSHA1"; - // SHA-1 generates 160 bit hashes, so that's what makes sense here - int derivedKeyLength = 160; - // Pick an iteration count that works for you. The NIST recommends at - // least 1,000 iterations: - // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf - // iOS 4.x reportedly uses 10,000: - // http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/ - int iterations = 20000; + public byte[] getEncryptedPassword(String password, byte[] salt) { + // PBKDF2 with SHA-1 as the hashing algorithm. Note that the NIST + // specifically names SHA-1 as an acceptable hashing algorithm for + // PBKDF2 + String algorithm = "PBKDF2WithHmacSHA1"; + // SHA-1 generates 160 bit hashes, so that's what makes sense here + int derivedKeyLength = 160; + // Pick an iteration count that works for you. The NIST recommends at + // least 1,000 iterations: + // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf + // iOS 4.x reportedly uses 10,000: + // http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/ + int iterations = 20000; - KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength); + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength); - byte[] bytes = null; - try { - SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm); - SecretKey key = f.generateSecret(spec); - bytes = key.getEncoded(); - } catch (Exception e) { - // should never happen - log.error(e.getMessage(), e); - } - return bytes; - } + byte[] bytes = null; + try { + SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm); + SecretKey key = f.generateSecret(spec); + bytes = key.getEncoded(); + } catch (Exception e) { + // should never happen + log.error(e.getMessage(), e); + } + return bytes; + } - public byte[] generateSalt() { - // VERY important to use SecureRandom instead of just Random + public byte[] generateSalt() { + // VERY important to use SecureRandom instead of just Random - byte[] salt = null; - try { - SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); - - // Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5 - salt = new byte[8]; - random.nextBytes(salt); - } catch (NoSuchAlgorithmException e) { - // should never happen - log.error(e.getMessage(), e); - } - return salt; - } + byte[] salt = null; + try { + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + // Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5 + salt = new byte[8]; + random.nextBytes(salt); + } catch (NoSuchAlgorithmException e) { + // should never happen + log.error(e.getMessage(), e); + } + return salt; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java index 2a96f55c..13233f5c 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/PushNotificationService.java @@ -1,12 +1,20 @@ package com.commafeed.backend.service; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.HttpClientFactory; +import com.commafeed.backend.Urls; +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings; +import io.vertx.core.json.JsonObject; +import jakarta.inject.Singleton; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; - -import jakarta.inject.Singleton; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.config.RequestConfig; @@ -18,163 +26,196 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.util.Timeout; -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.HttpClientFactory; -import com.commafeed.backend.Urls; -import com.commafeed.backend.model.FeedEntry; -import com.commafeed.backend.model.FeedSubscription; -import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings; - -import io.vertx.core.json.JsonObject; -import lombok.extern.slf4j.Slf4j; - @Singleton @Slf4j public class PushNotificationService { - private final CloseableHttpClient httpClient; - private final Meter meter; - private final CommaFeedConfiguration config; + private final CloseableHttpClient httpClient; + private final Meter meter; + private final CommaFeedConfiguration config; - public PushNotificationService(HttpClientFactory httpClientFactory, MetricRegistry metrics, CommaFeedConfiguration config) { - this.httpClient = httpClientFactory.newClient(config.pushNotifications().threads()); - this.meter = metrics.meter(MetricRegistry.name(getClass(), "notify")); - this.config = config; - } + public PushNotificationService( + HttpClientFactory httpClientFactory, + MetricRegistry metrics, + CommaFeedConfiguration config) { + this.httpClient = httpClientFactory.newClient(config.pushNotifications().threads()); + this.meter = metrics.meter(MetricRegistry.name(getClass(), "notify")); + this.config = config; + } - public void notify(PushNotificationUserSettings settings, FeedSubscription subscription, FeedEntry entry) { - if (!config.pushNotifications().enabled() || settings.getType() == null) { - return; - } + public void notify( + PushNotificationUserSettings settings, FeedSubscription subscription, FeedEntry entry) { + if (!config.pushNotifications().enabled() || settings.getType() == null) { + return; + } - log.debug("sending {} push notification for entry {} in feed {}", settings.getType(), entry.getId(), - subscription.getFeed().getId()); - String entryTitle = entry.getContent() != null ? entry.getContent().getTitle() : null; - String entryUrl = entry.getUrl(); - String feedTitle = subscription.getTitle(); + log.debug( + "sending {} push notification for entry {} in feed {}", + settings.getType(), + entry.getId(), + subscription.getFeed().getId()); + String entryTitle = entry.getContent() != null ? entry.getContent().getTitle() : null; + String entryUrl = entry.getUrl(); + String feedTitle = subscription.getTitle(); - if (StringUtils.isBlank(entryTitle)) { - entryTitle = "New entry"; - } + if (StringUtils.isBlank(entryTitle)) { + entryTitle = "New entry"; + } - try { - switch (settings.getType()) { - case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl); - case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl); - case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl); - default -> throw new IllegalStateException("unsupported notification type: " + settings.getType()); - } - } catch (IOException e) { - throw new PushNotificationException("Failed to send external notification", e); - } + try { + switch (settings.getType()) { + case NTFY -> sendNtfy(settings, feedTitle, entryTitle, entryUrl); + case GOTIFY -> sendGotify(settings, feedTitle, entryTitle, entryUrl); + case PUSHOVER -> sendPushover(settings, feedTitle, entryTitle, entryUrl); + default -> + throw new IllegalStateException( + "unsupported notification type: " + settings.getType()); + } + } catch (IOException e) { + throw new PushNotificationException("Failed to send external notification", e); + } - meter.mark(); - } + meter.mark(); + } - private void sendNtfy(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl) throws IOException { - String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl()); - String topic = settings.getTopic(); + private void sendNtfy( + PushNotificationUserSettings settings, + String feedTitle, + String entryTitle, + String entryUrl) + throws IOException { + String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl()); + String topic = settings.getTopic(); - if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(topic)) { - log.warn("ntfy notification skipped: missing server URL or topic"); - return; - } + if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(topic)) { + log.warn("ntfy notification skipped: missing server URL or topic"); + return; + } - HttpPost request = new HttpPost(serverUrl + "/" + topic); - request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); - request.addHeader("Title", feedTitle); - request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8)); + HttpPost request = new HttpPost(serverUrl + "/" + topic); + request.setConfig( + RequestConfig.custom() + .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) + .build()); + request.addHeader("Title", feedTitle); + request.setEntity(new StringEntity(entryTitle, StandardCharsets.UTF_8)); - if (StringUtils.isNotBlank(entryUrl)) { - request.addHeader("Click", entryUrl); - } + if (StringUtils.isNotBlank(entryUrl)) { + request.addHeader("Click", entryUrl); + } - if (StringUtils.isNotBlank(settings.getUserSecret())) { - request.addHeader("Authorization", "Bearer " + settings.getUserSecret()); - } + if (StringUtils.isNotBlank(settings.getUserSecret())) { + request.addHeader("Authorization", "Bearer " + settings.getUserSecret()); + } - httpClient.execute(request, response -> { - if (response.getCode() >= 400) { - throw new PushNotificationException("ntfy notification failed with status " + response.getCode()); - } - return null; - }); - } + httpClient.execute( + request, + response -> { + if (response.getCode() >= 400) { + throw new PushNotificationException( + "ntfy notification failed with status " + response.getCode()); + } + return null; + }); + } - private void sendGotify(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl) - throws IOException { - String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl()); - String token = settings.getUserSecret(); + private void sendGotify( + PushNotificationUserSettings settings, + String feedTitle, + String entryTitle, + String entryUrl) + throws IOException { + String serverUrl = Urls.removeTrailingSlash(settings.getServerUrl()); + String token = settings.getUserSecret(); - if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(token)) { - log.warn("gotify notification skipped: missing server URL or token"); - return; - } + if (StringUtils.isBlank(serverUrl) || StringUtils.isBlank(token)) { + log.warn("gotify notification skipped: missing server URL or token"); + return; + } - JsonObject json = new JsonObject(); - json.put("title", feedTitle); - json.put("message", entryTitle); - json.put("priority", 5); - if (StringUtils.isNotBlank(entryUrl)) { - json.put("extras", - new JsonObject().put("client::notification", new JsonObject().put("click", new JsonObject().put("url", entryUrl)))); - } + JsonObject json = new JsonObject(); + json.put("title", feedTitle); + json.put("message", entryTitle); + json.put("priority", 5); + if (StringUtils.isNotBlank(entryUrl)) { + json.put( + "extras", + new JsonObject() + .put( + "client::notification", + new JsonObject() + .put("click", new JsonObject().put("url", entryUrl)))); + } - HttpPost request = new HttpPost(serverUrl + "/message"); - request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); - request.addHeader("X-Gotify-Key", token); - request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON)); + HttpPost request = new HttpPost(serverUrl + "/message"); + request.setConfig( + RequestConfig.custom() + .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) + .build()); + request.addHeader("X-Gotify-Key", token); + request.setEntity(new StringEntity(json.toString(), ContentType.APPLICATION_JSON)); - httpClient.execute(request, response -> { - if (response.getCode() >= 400) { - throw new PushNotificationException("gotify notification failed with status " + response.getCode()); - } - return null; - }); - } + httpClient.execute( + request, + response -> { + if (response.getCode() >= 400) { + throw new PushNotificationException( + "gotify notification failed with status " + response.getCode()); + } + return null; + }); + } - private void sendPushover(PushNotificationUserSettings settings, String feedTitle, String entryTitle, String entryUrl) - throws IOException { - String token = settings.getUserSecret(); - String userKey = settings.getUserId(); + private void sendPushover( + PushNotificationUserSettings settings, + String feedTitle, + String entryTitle, + String entryUrl) + throws IOException { + String token = settings.getUserSecret(); + String userKey = settings.getUserId(); - if (StringUtils.isBlank(token) || StringUtils.isBlank(userKey)) { - log.warn("pushover notification skipped: missing token or user key"); - return; - } + if (StringUtils.isBlank(token) || StringUtils.isBlank(userKey)) { + log.warn("pushover notification skipped: missing token or user key"); + return; + } - List params = new ArrayList<>(); - params.add(new BasicNameValuePair("token", token)); - params.add(new BasicNameValuePair("user", userKey)); - params.add(new BasicNameValuePair("title", feedTitle)); - params.add(new BasicNameValuePair("message", entryTitle)); - if (StringUtils.isNotBlank(entryUrl)) { - params.add(new BasicNameValuePair("url", entryUrl)); - } + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("token", token)); + params.add(new BasicNameValuePair("user", userKey)); + params.add(new BasicNameValuePair("title", feedTitle)); + params.add(new BasicNameValuePair("message", entryTitle)); + if (StringUtils.isNotBlank(entryUrl)) { + params.add(new BasicNameValuePair("url", entryUrl)); + } - HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json"); - request.setConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); - request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); + HttpPost request = new HttpPost("https://api.pushover.net/1/messages.json"); + request.setConfig( + RequestConfig.custom() + .setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())) + .build()); + request.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); - httpClient.execute(request, response -> { - if (response.getCode() >= 400) { - throw new PushNotificationException("pushover notification failed with status " + response.getCode()); - } - return null; - }); - } + httpClient.execute( + request, + response -> { + if (response.getCode() >= 400) { + throw new PushNotificationException( + "pushover notification failed with status " + response.getCode()); + } + return null; + }); + } - public static class PushNotificationException extends RuntimeException { - private static final long serialVersionUID = -3392881821584833819L; + public static class PushNotificationException extends RuntimeException { + private static final long serialVersionUID = -3392881821584833819L; - public PushNotificationException(String message) { - super(message); - } + public PushNotificationException(String message) { + super(message); + } - public PushNotificationException(String message, Throwable cause) { - super(message, cause); - } - } + public PushNotificationException(String message, Throwable cause) { + super(message, cause); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java index 0d1c3f4a..dda4bf8a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java @@ -1,16 +1,5 @@ package com.commafeed.backend.service; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import jakarta.inject.Singleton; - -import org.apache.commons.lang3.StringUtils; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConstants; import com.commafeed.backend.Digests; @@ -24,138 +13,152 @@ import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.internal.PostLoginActivities; import com.google.common.base.Preconditions; - +import jakarta.inject.Singleton; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; @RequiredArgsConstructor @Singleton public class UserService { - private final FeedCategoryDAO feedCategoryDAO; - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final UserDAO userDAO; - private final UserRoleDAO userRoleDAO; - private final UserSettingsDAO userSettingsDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final UserDAO userDAO; + private final UserRoleDAO userRoleDAO; + private final UserSettingsDAO userSettingsDAO; - private final PasswordEncryptionService encryptionService; - private final CommaFeedConfiguration config; + private final PasswordEncryptionService encryptionService; + private final CommaFeedConfiguration config; - private final PostLoginActivities postLoginActivities; + private final PostLoginActivities postLoginActivities; - /** - * try to log in with given credentials - */ - public Optional login(String nameOrEmail, String password) { - if (nameOrEmail == null || password == null) { - return Optional.empty(); - } + /** try to log in with given credentials */ + public Optional login(String nameOrEmail, String password) { + if (nameOrEmail == null || password == null) { + return Optional.empty(); + } - User user = userDAO.findByName(nameOrEmail); - if (user == null) { - user = userDAO.findByEmail(nameOrEmail); - } - if (user != null && !user.isDisabled()) { - boolean authenticated = encryptionService.authenticate(password, user.getPassword(), user.getSalt()); - if (authenticated) { - performPostLoginActivities(user); - return Optional.of(user); - } - } - return Optional.empty(); - } + User user = userDAO.findByName(nameOrEmail); + if (user == null) { + user = userDAO.findByEmail(nameOrEmail); + } + if (user != null && !user.isDisabled()) { + boolean authenticated = + encryptionService.authenticate(password, user.getPassword(), user.getSalt()); + if (authenticated) { + performPostLoginActivities(user); + return Optional.of(user); + } + } + return Optional.empty(); + } - /** - * try to log in with given api key - */ - public Optional login(String apiKey) { - if (apiKey == null) { - return Optional.empty(); - } + /** try to log in with given api key */ + public Optional login(String apiKey) { + if (apiKey == null) { + return Optional.empty(); + } - User user = userDAO.findByApiKey(apiKey); - if (user != null && !user.isDisabled()) { - performPostLoginActivities(user); - return Optional.of(user); - } - return Optional.empty(); - } + User user = userDAO.findByApiKey(apiKey); + if (user != null && !user.isDisabled()) { + performPostLoginActivities(user); + return Optional.of(user); + } + return Optional.empty(); + } - /** - * try to log in with given fever api key - */ - public Optional login(long userId, String feverApiKey) { - if (feverApiKey == null) { - return Optional.empty(); - } + /** try to log in with given fever api key */ + public Optional login(long userId, String feverApiKey) { + if (feverApiKey == null) { + return Optional.empty(); + } - User user = userDAO.findById(userId); - if (user == null || user.isDisabled() || user.getApiKey() == null) { - return Optional.empty(); - } + User user = userDAO.findById(userId); + if (user == null || user.isDisabled() || user.getApiKey() == null) { + return Optional.empty(); + } - String computedFeverApiKey = Digests.md5Hex(user.getName() + ":" + user.getApiKey()); - if (!computedFeverApiKey.equalsIgnoreCase(feverApiKey)) { - return Optional.empty(); - } + String computedFeverApiKey = Digests.md5Hex(user.getName() + ":" + user.getApiKey()); + if (!computedFeverApiKey.equalsIgnoreCase(feverApiKey)) { + return Optional.empty(); + } - performPostLoginActivities(user); - return Optional.of(user); - } + performPostLoginActivities(user); + return Optional.of(user); + } - /** - * should triggers after successful login - */ - public void performPostLoginActivities(User user) { - postLoginActivities.executeFor(user); - } + /** should triggers after successful login */ + public void performPostLoginActivities(User user) { + postLoginActivities.executeFor(user); + } - public User register(String name, String password, String email, Collection roles) { - return register(name, password, email, roles, false); - } + public User register(String name, String password, String email, Collection roles) { + return register(name, password, email, roles, false); + } - public User register(String name, String password, String email, Collection roles, boolean forceRegistration) { + public User register( + String name, + String password, + String email, + Collection roles, + boolean forceRegistration) { - if (!forceRegistration) { - Preconditions.checkState(config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance"); - } + if (!forceRegistration) { + Preconditions.checkState( + config.users().allowRegistrations(), + "Registrations are closed on this CommaFeed instance"); + } - Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken"); - if (StringUtils.isNotBlank(email)) { - Preconditions.checkArgument(userDAO.findByEmail(email) == null, "Email already taken"); - } + Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken"); + if (StringUtils.isNotBlank(email)) { + Preconditions.checkArgument(userDAO.findByEmail(email) == null, "Email already taken"); + } - User user = new User(); - byte[] salt = encryptionService.generateSalt(); - user.setName(name); - user.setEmail(email); - user.setCreated(Instant.now()); - user.setSalt(salt); - user.setPassword(encryptionService.getEncryptedPassword(password, salt)); - userDAO.persist(user); - for (Role role : roles) { - userRoleDAO.persist(new UserRole(user, role)); - } - return user; - } + User user = new User(); + byte[] salt = encryptionService.generateSalt(); + user.setName(name); + user.setEmail(email); + user.setCreated(Instant.now()); + user.setSalt(salt); + user.setPassword(encryptionService.getEncryptedPassword(password, salt)); + userDAO.persist(user); + for (Role role : roles) { + userRoleDAO.persist(new UserRole(user, role)); + } + return user; + } - public void createDemoUser() { - register(CommaFeedConstants.USERNAME_DEMO, "demo", "demo@commafeed.com", Collections.singletonList(Role.USER), true); - } + public void createDemoUser() { + register( + CommaFeedConstants.USERNAME_DEMO, + "demo", + "demo@commafeed.com", + Collections.singletonList(Role.USER), + true); + } - public void unregister(User user) { - userSettingsDAO.delete(userSettingsDAO.findByUser(user)); - userRoleDAO.delete(userRoleDAO.findAll(user)); - feedSubscriptionDAO.delete(feedSubscriptionDAO.findAll(user)); - feedCategoryDAO.delete(feedCategoryDAO.findAll(user)); - userDAO.delete(user); - } + public void unregister(User user) { + userSettingsDAO.delete(userSettingsDAO.findByUser(user)); + userRoleDAO.delete(userRoleDAO.findAll(user)); + feedSubscriptionDAO.delete(feedSubscriptionDAO.findAll(user)); + feedCategoryDAO.delete(feedCategoryDAO.findAll(user)); + userDAO.delete(user); + } - public String generateApiKey(User user) { - byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt()); - return Digests.sha1Hex(key); - } + public String generateApiKey(User user) { + byte[] key = + encryptionService.getEncryptedPassword( + UUID.randomUUID().toString(), user.getSalt()); + return Digests.sha1Hex(key); + } - public Set getRoles(User user) { - return userRoleDAO.findRoles(user); - } + public Set getRoles(User user) { + return userRoleDAO.findRoles(user); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java index 822bbe02..83cb8407 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java @@ -1,10 +1,5 @@ package com.commafeed.backend.service.db; -import java.time.Instant; -import java.util.List; - -import jakarta.inject.Singleton; - import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; @@ -16,173 +11,202 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.Feed; - +import jakarta.inject.Singleton; +import java.time.Instant; +import java.util.List; import lombok.extern.slf4j.Slf4j; -/** - * Contains utility methods for cleaning the database - * - */ +/** Contains utility methods for cleaning the database */ @Slf4j @Singleton public class DatabaseCleaningService { - private final UnitOfWork unitOfWork; - private final FeedDAO feedDAO; - private final FeedEntryDAO feedEntryDAO; - private final FeedEntryContentDAO feedEntryContentDAO; - private final FeedEntryStatusDAO feedEntryStatusDAO; - private final int batchSize; - private final boolean keepStarredEntries; - private final Meter entriesDeletedMeter; + private final UnitOfWork unitOfWork; + private final FeedDAO feedDAO; + private final FeedEntryDAO feedEntryDAO; + private final FeedEntryContentDAO feedEntryContentDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + private final int batchSize; + private final boolean keepStarredEntries; + private final Meter entriesDeletedMeter; - public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO, - FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) { - this.unitOfWork = unitOfWork; - this.feedDAO = feedDAO; - this.feedEntryDAO = feedEntryDAO; - this.feedEntryContentDAO = feedEntryContentDAO; - this.feedEntryStatusDAO = feedEntryStatusDAO; - this.batchSize = config.database().cleanup().batchSize(); - this.keepStarredEntries = config.database().cleanup().keepStarredEntries(); - this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted")); - } + public DatabaseCleaningService( + CommaFeedConfiguration config, + UnitOfWork unitOfWork, + FeedDAO feedDAO, + FeedEntryDAO feedEntryDAO, + FeedEntryContentDAO feedEntryContentDAO, + FeedEntryStatusDAO feedEntryStatusDAO, + MetricRegistry metrics) { + this.unitOfWork = unitOfWork; + this.feedDAO = feedDAO; + this.feedEntryDAO = feedEntryDAO; + this.feedEntryContentDAO = feedEntryContentDAO; + this.feedEntryStatusDAO = feedEntryStatusDAO; + this.batchSize = config.database().cleanup().batchSize(); + this.keepStarredEntries = config.database().cleanup().keepStarredEntries(); + this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted")); + } - public void cleanFeedsWithoutSubscriptions() { - log.info("cleaning feeds without subscriptions"); - long total = 0; - int deleted; - long entriesTotal = 0; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of feeds without subscriptions"); - return; - } + public void cleanFeedsWithoutSubscriptions() { + log.info("cleaning feeds without subscriptions"); + long total = 0; + int deleted; + long entriesTotal = 0; + do { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping cleanup of feeds without subscriptions"); + return; + } - List feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1)); - for (Feed feed : feeds) { - long entriesDeleted; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of feeds without subscriptions"); - return; - } + List feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1)); + for (Feed feed : feeds) { + long entriesDeleted; + do { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping cleanup of feeds without subscriptions"); + return; + } - entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize)); - entriesDeletedMeter.mark(entriesDeleted); - entriesTotal += entriesDeleted; - log.debug("removed {} entries for feeds without subscriptions", entriesTotal); - } while (entriesDeleted > 0); - } - deleted = unitOfWork.call(() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList()))); - total += deleted; - log.debug("removed {} feeds without subscriptions", total); - } while (deleted != 0); - log.info("cleanup done: {} feeds without subscriptions deleted", total); - } + entriesDeleted = + unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize)); + entriesDeletedMeter.mark(entriesDeleted); + entriesTotal += entriesDeleted; + log.debug("removed {} entries for feeds without subscriptions", entriesTotal); + } while (entriesDeleted > 0); + } + deleted = + unitOfWork.call( + () -> + feedDAO.delete( + feedDAO.findByIds( + feeds.stream() + .map(AbstractModel::getId) + .toList()))); + total += deleted; + log.debug("removed {} feeds without subscriptions", total); + } while (deleted != 0); + log.info("cleanup done: {} feeds without subscriptions deleted", total); + } - public void cleanContentsWithoutEntries() { - log.info("cleaning contents without entries"); - long total = 0; - long deleted; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of contents without entries"); - return; - } + public void cleanContentsWithoutEntries() { + log.info("cleaning contents without entries"); + long total = 0; + long deleted; + do { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping cleanup of contents without entries"); + return; + } - deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize)); - total += deleted; - log.debug("removed {} contents without entries", total); - } while (deleted != 0); - log.info("cleanup done: {} contents without entries deleted", total); - } + deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize)); + total += deleted; + log.debug("removed {} contents without entries", total); + } while (deleted != 0); + log.info("cleanup done: {} contents without entries deleted", total); + } - public void cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) { - log.info("cleaning entries exceeding feed capacity"); - long total = 0; - while (true) { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of entries exceeding feed capacity"); - return; - } + public void cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) { + log.info("cleaning entries exceeding feed capacity"); + long total = 0; + while (true) { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping cleanup of entries exceeding feed capacity"); + return; + } - List feeds = unitOfWork - .call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries)); - if (feeds.isEmpty()) { - break; - } + List feeds = + unitOfWork.call( + () -> + feedEntryDAO.findFeedsExceedingCapacity( + maxFeedCapacity, batchSize, keepStarredEntries)); + if (feeds.isEmpty()) { + break; + } - for (final FeedCapacity feed : feeds) { - long remaining = feed.capacity() - maxFeedCapacity; - int deleted; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of entries exceeding feed capacity"); - return; - } + for (final FeedCapacity feed : feeds) { + long remaining = feed.capacity() - maxFeedCapacity; + int deleted; + do { + if (Thread.currentThread().isInterrupted()) { + log.info( + "interrupted, stopping cleanup of entries exceeding feed capacity"); + return; + } - final long rem = remaining; - deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries)); - entriesDeletedMeter.mark(deleted); - total += deleted; - remaining -= deleted; - log.debug("removed {} entries for feeds exceeding capacity", total); - } while (deleted > 0 && remaining > 0); - } - } - log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total); - } + final long rem = remaining; + deleted = + unitOfWork.call( + () -> + feedEntryDAO.deleteOldEntries( + feed.id(), + Math.min(batchSize, rem), + keepStarredEntries)); + entriesDeletedMeter.mark(deleted); + total += deleted; + remaining -= deleted; + log.debug("removed {} entries for feeds exceeding capacity", total); + } while (deleted > 0 && remaining > 0); + } + } + log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total); + } - public void cleanEntriesOlderThan(final Instant olderThan) { - log.info("cleaning old entries"); - long total = 0; - long deleted; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of old entries"); - return; - } + public void cleanEntriesOlderThan(final Instant olderThan) { + log.info("cleaning old entries"); + long total = 0; + long deleted; + do { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping cleanup of old entries"); + return; + } - deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries)); - entriesDeletedMeter.mark(deleted); - total += deleted; - log.debug("removed {} old entries", total); - } while (deleted != 0); - log.info("cleanup done: {} old entries deleted", total); - } + deleted = + unitOfWork.call( + () -> + feedEntryDAO.deleteEntriesOlderThan( + olderThan, batchSize, keepStarredEntries)); + entriesDeletedMeter.mark(deleted); + total += deleted; + log.debug("removed {} old entries", total); + } while (deleted != 0); + log.info("cleanup done: {} old entries deleted", total); + } - public void cleanStatusesOlderThan(final Instant olderThan) { - log.info("cleaning old read statuses"); - long total = 0; - long deleted; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping cleanup of old read statuses"); - return; - } + public void cleanStatusesOlderThan(final Instant olderThan) { + log.info("cleaning old read statuses"); + long total = 0; + long deleted; + do { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping cleanup of old read statuses"); + return; + } - deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize)); - total += deleted; - log.debug("removed {} old read statuses", total); - } while (deleted != 0); - log.info("cleanup done: {} old read statuses deleted", total); - } + deleted = + unitOfWork.call( + () -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize)); + total += deleted; + log.debug("removed {} old read statuses", total); + } while (deleted != 0); + log.info("cleanup done: {} old read statuses deleted", total); + } - public void autoMarkAsRead() { - log.info("marking entries as read based on autoMarkAsReadAfterDays"); - long total = 0; - long marked; - do { - if (Thread.currentThread().isInterrupted()) { - log.info("interrupted, stopping marking entries as read"); - return; - } + public void autoMarkAsRead() { + log.info("marking entries as read based on autoMarkAsReadAfterDays"); + long total = 0; + long marked; + do { + if (Thread.currentThread().isInterrupted()) { + log.info("interrupted, stopping marking entries as read"); + return; + } - marked = unitOfWork.call(() -> feedEntryStatusDAO.autoMarkAsRead(batchSize)); - total += marked; - log.debug("marked {} entries as read", total); - } while (marked != 0); - log.info("cleanup done: marked {} entries as read", total); - } + marked = unitOfWork.call(() -> feedEntryStatusDAO.autoMarkAsRead(batchSize)); + total += marked; + log.debug("marked {} entries as read", total); + } while (marked != 0); + log.info("cleanup done: marked {} entries as read", total); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java index 54792083..fce5af15 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java @@ -1,44 +1,42 @@ package com.commafeed.backend.service.db; -import jakarta.inject.Singleton; - -import org.kohsuke.MetaInfServices; - import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; - +import jakarta.inject.Singleton; import liquibase.database.Database; import liquibase.database.core.PostgresDatabase; import liquibase.structure.DatabaseObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.kohsuke.MetaInfServices; @Slf4j @RequiredArgsConstructor @Singleton public class DatabaseStartupService { - private final UnitOfWork unitOfWork; - private final UserDAO userDAO; + private final UnitOfWork unitOfWork; + private final UserDAO userDAO; - public boolean isInitialSetupRequired() { - return unitOfWork.call(userDAO::count) == 0; - } + public boolean isInitialSetupRequired() { + return unitOfWork.call(userDAO::count) == 0; + } - /** - * Register a postgresql database in liquibase that doesn't escape columns, so that we can use lower case columns - */ - @MetaInfServices(Database.class) - public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase { - @Override - public String escapeObjectName(String objectName, Class objectType) { - return objectName; - } - - @Override - public int getPriority() { - return super.getPriority() + 1; - } - } + /** + * Register a postgresql database in liquibase that doesn't escape columns, so that we can use + * lower case columns + */ + @MetaInfServices(Database.class) + public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase { + @Override + public String escapeObjectName( + String objectName, Class objectType) { + return objectName; + } + @Override + public int getPriority() { + return super.getPriority() + 1; + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java b/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java index 3a9df391..829a57a3 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java @@ -1,30 +1,29 @@ package com.commafeed.backend.service.internal; -import java.time.Instant; -import java.time.temporal.ChronoUnit; - -import jakarta.inject.Singleton; - import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; - +import jakarta.inject.Singleton; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class PostLoginActivities { - private final UserDAO userDAO; - private final UnitOfWork unitOfWork; + private final UserDAO userDAO; + private final UnitOfWork unitOfWork; - public void executeFor(User user) { - // only update lastLogin every once in a while in order to avoid invalidating the cache every time someone logs in - Instant now = Instant.now(); - Instant lastLogin = user.getLastLogin(); - if (lastLogin == null || ChronoUnit.MINUTES.between(lastLogin, now) >= 30) { - user.setLastLogin(now); - unitOfWork.run(() -> userDAO.merge(user)); - } - } + public void executeFor(User user) { + // only update lastLogin every once in a while in order to avoid invalidating the cache + // every + // time someone logs in + Instant now = Instant.now(); + Instant lastLogin = user.getLastLogin(); + if (lastLogin == null || ChronoUnit.MINUTES.between(lastLogin, now) >= 30) { + user.setLastLogin(now); + unitOfWork.run(() -> userDAO.merge(user)); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/AutoMarkAsReadTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/AutoMarkAsReadTask.java index e438c241..363844b6 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/AutoMarkAsReadTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/AutoMarkAsReadTask.java @@ -1,37 +1,33 @@ package com.commafeed.backend.task; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.backend.service.db.DatabaseCleaningService; - +import jakarta.inject.Singleton; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class AutoMarkAsReadTask extends ScheduledTask { - private final DatabaseCleaningService cleaner; + private final DatabaseCleaningService cleaner; - @Override - public void run() { - cleaner.autoMarkAsRead(); - } + @Override + public void run() { + cleaner.autoMarkAsRead(); + } - @Override - public long getInitialDelay() { - return 30; - } + @Override + public long getInitialDelay() { + return 30; + } - @Override - public long getPeriod() { - return 60; - } - - @Override - public TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + public long getPeriod() { + return 60; + } + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java index 5347619b..6140ffac 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java @@ -1,16 +1,13 @@ package com.commafeed.backend.task; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConstants; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.UserService; - +import jakarta.inject.Singleton; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,43 +16,42 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class DemoAccountCleanupTask extends ScheduledTask { - private final CommaFeedConfiguration config; - private final UnitOfWork unitOfWork; - private final UserDAO userDAO; - private final UserService userService; + private final CommaFeedConfiguration config; + private final UnitOfWork unitOfWork; + private final UserDAO userDAO; + private final UserService userService; - @Override - protected void run() { - if (!config.users().createDemoAccount()) { - return; - } + @Override + protected void run() { + if (!config.users().createDemoAccount()) { + return; + } - log.info("recreating demo user account"); - unitOfWork.run(() -> { - User demoUser = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO); - if (demoUser == null) { - return; - } + log.info("recreating demo user account"); + unitOfWork.run( + () -> { + User demoUser = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO); + if (demoUser == null) { + return; + } - userService.unregister(demoUser); - userService.createDemoUser(); - }); + userService.unregister(demoUser); + userService.createDemoUser(); + }); + } - } + @Override + protected long getInitialDelay() { + return 1; + } - @Override - protected long getInitialDelay() { - return 1; - } - - @Override - protected long getPeriod() { - return getTimeUnit().convert(24, TimeUnit.HOURS); - } - - @Override - protected TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + protected long getPeriod() { + return getTimeUnit().convert(24, TimeUnit.HOURS); + } + @Override + protected TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java index 0291f6fc..76e40f76 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java @@ -1,42 +1,38 @@ package com.commafeed.backend.task; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; - +import jakarta.inject.Singleton; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask { - private final CommaFeedConfiguration config; - private final DatabaseCleaningService cleaner; + private final CommaFeedConfiguration config; + private final DatabaseCleaningService cleaner; - @Override - public void run() { - int maxFeedCapacity = config.database().cleanup().maxFeedCapacity(); - if (maxFeedCapacity > 0) { - cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity); - } - } + @Override + public void run() { + int maxFeedCapacity = config.database().cleanup().maxFeedCapacity(); + if (maxFeedCapacity > 0) { + cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity); + } + } - @Override - public long getInitialDelay() { - return 10; - } + @Override + public long getInitialDelay() { + return 10; + } - @Override - public long getPeriod() { - return 60; - } - - @Override - public TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + public long getPeriod() { + return 60; + } + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java index d89545fb..78335223 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java @@ -1,45 +1,41 @@ package com.commafeed.backend.task; -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; - +import jakarta.inject.Singleton; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class OldEntriesCleanupTask extends ScheduledTask { - private final CommaFeedConfiguration config; - private final DatabaseCleaningService cleaner; + private final CommaFeedConfiguration config; + private final DatabaseCleaningService cleaner; - @Override - public void run() { - Duration entriesMaxAge = config.database().cleanup().entriesMaxAge(); - if (!entriesMaxAge.isZero()) { - Instant threshold = Instant.now().minus(entriesMaxAge); - cleaner.cleanEntriesOlderThan(threshold); - } - } + @Override + public void run() { + Duration entriesMaxAge = config.database().cleanup().entriesMaxAge(); + if (!entriesMaxAge.isZero()) { + Instant threshold = Instant.now().minus(entriesMaxAge); + cleaner.cleanEntriesOlderThan(threshold); + } + } - @Override - public long getInitialDelay() { - return 5; - } + @Override + public long getInitialDelay() { + return 5; + } - @Override - public long getPeriod() { - return 60; - } - - @Override - public TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + public long getPeriod() { + return 60; + } + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java index d1776e40..364b4c71 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java @@ -1,43 +1,39 @@ package com.commafeed.backend.task; -import java.time.Instant; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; - +import jakarta.inject.Singleton; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class OldStatusesCleanupTask extends ScheduledTask { - private final CommaFeedConfiguration config; - private final DatabaseCleaningService cleaner; + private final CommaFeedConfiguration config; + private final DatabaseCleaningService cleaner; - @Override - public void run() { - Instant threshold = config.database().cleanup().statusesInstantThreshold(); - if (threshold != null) { - cleaner.cleanStatusesOlderThan(threshold); - } - } + @Override + public void run() { + Instant threshold = config.database().cleanup().statusesInstantThreshold(); + if (threshold != null) { + cleaner.cleanStatusesOlderThan(threshold); + } + } - @Override - public long getInitialDelay() { - return 15; - } + @Override + public long getInitialDelay() { + return 15; + } - @Override - public long getPeriod() { - return 60; - } - - @Override - public TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + public long getPeriod() { + return 60; + } + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java index 9b9ee5fc..9500ca01 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java @@ -1,37 +1,33 @@ package com.commafeed.backend.task; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.backend.service.db.DatabaseCleaningService; - +import jakarta.inject.Singleton; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class OrphanedContentsCleanupTask extends ScheduledTask { - private final DatabaseCleaningService cleaner; + private final DatabaseCleaningService cleaner; - @Override - public void run() { - cleaner.cleanContentsWithoutEntries(); - } + @Override + public void run() { + cleaner.cleanContentsWithoutEntries(); + } - @Override - public long getInitialDelay() { - return 25; - } + @Override + public long getInitialDelay() { + return 25; + } - @Override - public long getPeriod() { - return 60; - } - - @Override - public TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + public long getPeriod() { + return 60; + } + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java index d714fcb9..71e6132d 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java @@ -1,37 +1,33 @@ package com.commafeed.backend.task; -import java.util.concurrent.TimeUnit; - -import jakarta.inject.Singleton; - import com.commafeed.backend.service.db.DatabaseCleaningService; - +import jakarta.inject.Singleton; +import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class OrphanedFeedsCleanupTask extends ScheduledTask { - private final DatabaseCleaningService cleaner; + private final DatabaseCleaningService cleaner; - @Override - public void run() { - cleaner.cleanFeedsWithoutSubscriptions(); - } + @Override + public void run() { + cleaner.cleanFeedsWithoutSubscriptions(); + } - @Override - public long getInitialDelay() { - return 20; - } + @Override + public long getInitialDelay() { + return 20; + } - @Override - public long getPeriod() { - return 60; - } - - @Override - public TimeUnit getTimeUnit() { - return TimeUnit.MINUTES; - } + @Override + public long getPeriod() { + return 60; + } + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.MINUTES; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/ScheduledTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/ScheduledTask.java index fc29c92c..ef060453 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/ScheduledTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/ScheduledTask.java @@ -2,29 +2,34 @@ package com.commafeed.backend.task; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; - import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class ScheduledTask { - protected abstract void run(); + protected abstract void run(); - protected abstract long getInitialDelay(); + protected abstract long getInitialDelay(); - protected abstract long getPeriod(); + protected abstract long getPeriod(); - protected abstract TimeUnit getTimeUnit(); + protected abstract TimeUnit getTimeUnit(); - public void register(ScheduledExecutorService executor) { - Runnable runnable = () -> { - try { - ScheduledTask.this.run(); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - }; - log.debug("registering task {} for execution every {} {}, starting in {} {}", getClass().getSimpleName(), getPeriod(), - getTimeUnit(), getInitialDelay(), getTimeUnit()); - executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit()); - } + public void register(ScheduledExecutorService executor) { + Runnable runnable = + () -> { + try { + ScheduledTask.this.run(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + }; + log.debug( + "registering task {} for execution every {} {}, starting in {} {}", + getClass().getSimpleName(), + getPeriod(), + getTimeUnit(), + getInitialDelay(), + getTimeUnit()); + executor.scheduleWithFixedDelay(runnable, getInitialDelay(), getPeriod(), getTimeUnit()); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java index 4e511bb6..a8ece2b3 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java @@ -1,37 +1,34 @@ package com.commafeed.backend.task; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -import jakarta.inject.Singleton; - import com.commafeed.CommaFeedConfiguration; import com.google.common.util.concurrent.MoreExecutors; - import io.quarkus.arc.All; +import jakarta.inject.Singleton; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TaskScheduler { - private final List tasks; - private final CommaFeedConfiguration config; + private final List tasks; + private final CommaFeedConfiguration config; - private ScheduledExecutorService executor; + private ScheduledExecutorService executor; - public TaskScheduler(@All List tasks, CommaFeedConfiguration config) { - this.tasks = tasks; - this.config = config; - } + public TaskScheduler(@All List tasks, CommaFeedConfiguration config) { + this.tasks = tasks; + this.config = config; + } - public void start() { - this.executor = Executors.newScheduledThreadPool(tasks.size()); - this.tasks.forEach(task -> task.register(executor)); - } + public void start() { + this.executor = Executors.newScheduledThreadPool(tasks.size()); + this.tasks.forEach(task -> task.register(executor)); + } - public void stop() { - MoreExecutors.shutdownAndAwaitTermination(executor, config.shutdownTimeout()); - } + public void stop() { + MoreExecutors.shutdownAndAwaitTermination(executor, config.shutdownTimeout()); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/FeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/FeedURLProvider.java index c3983f52..61be06c8 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/FeedURLProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/FeedURLProvider.java @@ -2,11 +2,8 @@ package com.commafeed.backend.urlprovider; import java.util.List; -/** - * Tries to find a feed url given the url and page content - */ +/** Tries to find a feed url given the url and page content */ public interface FeedURLProvider { - List get(String url, String urlContent); - + List get(String url, String urlContent); } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java index 10921967..1021f176 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java @@ -1,25 +1,24 @@ package com.commafeed.backend.urlprovider; +import jakarta.inject.Singleton; import java.util.List; import java.util.stream.Stream; - -import jakarta.inject.Singleton; - import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @Singleton public class InPageReferenceFeedURLProvider implements FeedURLProvider { - @Override - public List get(String url, String urlContent) { - Document doc = Jsoup.parse(urlContent, url); - if (!"html".equals(doc.children().getFirst().tagName())) { - return List.of(); - } - return Stream.concat(doc.select("link[type=application/atom+xml]").stream(), doc.select("link[type=application/rss+xml]").stream()) - .map(node -> node.attr("abs:href")) - .toList(); - } - + @Override + public List get(String url, String urlContent) { + Document doc = Jsoup.parse(urlContent, url); + if (!"html".equals(doc.children().getFirst().tagName())) { + return List.of(); + } + return Stream.concat( + doc.select("link[type=application/atom+xml]").stream(), + doc.select("link[type=application/rss+xml]").stream()) + .map(node -> node.attr("abs:href")) + .toList(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java index 40381ec3..5674264a 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java @@ -1,30 +1,28 @@ package com.commafeed.backend.urlprovider; -import java.util.List; - import jakarta.inject.Singleton; - +import java.util.List; import org.apache.commons.lang3.Strings; /** * Workaround for Youtube channels - * - * converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL + * + *

converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL * https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID */ @Singleton public class YoutubeFeedURLProvider implements FeedURLProvider { - private static final String PREFIX = "https://www.youtube.com/channel/"; - private static final String REPLACEMENT_PREFIX = "https://www.youtube.com/feeds/videos.xml?channel_id="; + private static final String PREFIX = "https://www.youtube.com/channel/"; + private static final String REPLACEMENT_PREFIX = + "https://www.youtube.com/feeds/videos.xml?channel_id="; - @Override - public List get(String url, String urlContent) { - if (!Strings.CI.startsWith(url, PREFIX)) { - return List.of(); - } - - return List.of(REPLACEMENT_PREFIX + url.substring(PREFIX.length())); - } + @Override + public List get(String url, String urlContent) { + if (!Strings.CI.startsWith(url, PREFIX)) { + return List.of(); + } + return List.of(REPLACEMENT_PREFIX + url.substring(PREFIX.length())); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java index 7c2d2276..d93d6134 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java @@ -1,13 +1,11 @@ package com.commafeed.frontend.model; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.io.Serializable; import java.util.ArrayList; import java.util.List; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - -import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Entry details") @@ -15,27 +13,27 @@ import lombok.Data; @RegisterForReflection public class Category implements Serializable { - @Schema(description = "category id", required = true) - private String id; + @Schema(description = "category id", required = true) + private String id; - @Schema(description = "parent category id") - private String parentId; + @Schema(description = "parent category id") + private String parentId; - @Schema(description = "parent category name") - private String parentName; + @Schema(description = "parent category name") + private String parentName; - @Schema(description = "category id", required = true) - private String name; + @Schema(description = "category id", required = true) + private String name; - @Schema(description = "category children categories", required = true) - private List children = new ArrayList<>(); + @Schema(description = "category children categories", required = true) + private List children = new ArrayList<>(); - @Schema(description = "category feeds", required = true) - private List feeds = new ArrayList<>(); + @Schema(description = "category feeds", required = true) + private List feeds = new ArrayList<>(); - @Schema(description = "whether the category is expanded or collapsed", required = true) - private boolean expanded; + @Schema(description = "whether the category is expanded or collapsed", required = true) + private boolean expanded; - @Schema(description = "position of the category in the list", required = true) - private int position; -} \ No newline at end of file + @Schema(description = "position of the category in the list", required = true) + private int position; +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java index 1a4e6383..5856bb9c 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java @@ -1,13 +1,11 @@ package com.commafeed.frontend.model; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.io.Serializable; import java.util.ArrayList; import java.util.List; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - -import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "List of entries with some metadata") @@ -15,36 +13,38 @@ import lombok.Data; @RegisterForReflection public class Entries implements Serializable { - @Schema(description = "name of the feed or the category requested", required = true) - private String name; + @Schema(description = "name of the feed or the category requested", required = true) + private String name; - @Schema(description = "error or warning message") - private String message; + @Schema(description = "error or warning message") + private String message; - @Schema(description = "times the server tried to refresh the feed and failed", required = true) - private int errorCount; + @Schema(description = "times the server tried to refresh the feed and failed", required = true) + private int errorCount; - @Schema(description = "URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries") - private String feedLink; + @Schema( + description = + "URL of the website, extracted from the feed, only filled if querying for feed entries, not category entries") + private String feedLink; - @Schema(description = "list generation timestamp", required = true) - private long timestamp; + @Schema(description = "list generation timestamp", required = true) + private long timestamp; - @Schema(description = "if the query has more elements", required = true) - private boolean hasMore; + @Schema(description = "if the query has more elements", required = true) + private boolean hasMore; - @Schema(description = "the requested offset") - private int offset; + @Schema(description = "the requested offset") + private int offset; - @Schema(description = "the requested limit") - private int limit; + @Schema(description = "the requested limit") + private int limit; - @Schema(description = "list of entries", required = true) - private List entries = new ArrayList<>(); - - @Schema( - description = "if true, the unread flag was ignored in the request, all entries are returned regardless of their read status", - required = true) - private boolean ignoredReadStatus; + @Schema(description = "list of entries", required = true) + private List entries = new ArrayList<>(); + @Schema( + description = + "if true, the unread flag was ignored in the request, all entries are returned regardless of their read status", + required = true) + private boolean ignoredReadStatus; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java index 7c6cff89..455f9535 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java @@ -1,22 +1,19 @@ package com.commafeed.frontend.model; -import java.io.Serializable; -import java.time.Instant; -import java.util.List; - -import org.apache.commons.lang3.Strings; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedSubscription; - import io.quarkus.runtime.annotations.RegisterForReflection; +import java.io.Serializable; +import java.time.Instant; +import java.util.List; import lombok.Data; +import org.apache.commons.lang3.Strings; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Entry details") @@ -24,123 +21,134 @@ import lombok.Data; @RegisterForReflection public class Entry implements Serializable { - @Schema(description = "entry id", required = true) - private String id; + @Schema(description = "entry id", required = true) + private String id; - @Schema(description = "entry guid", required = true) - private String guid; + @Schema(description = "entry guid", required = true) + private String guid; - @Schema(description = "entry title", required = true) - private String title; + @Schema(description = "entry title", required = true) + private String title; - @Schema(description = "entry content", required = true) - private String content; + @Schema(description = "entry content", required = true) + private String content; - @Schema(description = "comma-separated list of categories") - private String categories; + @Schema(description = "comma-separated list of categories") + private String categories; - @Schema(description = "whether entry content and title are rtl", required = true) - private boolean rtl; + @Schema(description = "whether entry content and title are rtl", required = true) + private boolean rtl; - @Schema(description = "entry author") - private String author; + @Schema(description = "entry author") + private String author; - @Schema(description = "entry enclosure url, if any") - private String enclosureUrl; + @Schema(description = "entry enclosure url, if any") + private String enclosureUrl; - @Schema(description = "entry enclosure mime type, if any") - private String enclosureType; + @Schema(description = "entry enclosure mime type, if any") + private String enclosureType; - @Schema(description = "entry media description, if any") - private String mediaDescription; + @Schema(description = "entry media description, if any") + private String mediaDescription; - @Schema(description = "entry media thumbnail url, if any") - private String mediaThumbnailUrl; + @Schema(description = "entry media thumbnail url, if any") + private String mediaThumbnailUrl; - @Schema(description = "entry media thumbnail width, if any") - private Integer mediaThumbnailWidth; + @Schema(description = "entry media thumbnail width, if any") + private Integer mediaThumbnailWidth; - @Schema(description = "entry media thumbnail height, if any") - private Integer mediaThumbnailHeight; + @Schema(description = "entry media thumbnail height, if any") + private Integer mediaThumbnailHeight; - @Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true) - private Instant date; + @Schema(description = "entry publication date", type = SchemaType.INTEGER, required = true) + private Instant date; - @Schema(description = "entry insertion date in the database", type = SchemaType.INTEGER, required = true) - private Instant insertedDate; + @Schema( + description = "entry insertion date in the database", + type = SchemaType.INTEGER, + required = true) + private Instant insertedDate; - @Schema(description = "feed id", required = true) - private String feedId; + @Schema(description = "feed id", required = true) + private String feedId; - @Schema(description = "feed name", required = true) - private String feedName; + @Schema(description = "feed name", required = true) + private String feedName; - @Schema(description = "this entry's feed url", required = true) - private String feedUrl; + @Schema(description = "this entry's feed url", required = true) + private String feedUrl; - @Schema(description = "this entry's website url", required = true) - private String feedLink; + @Schema(description = "this entry's website url", required = true) + private String feedLink; - @Schema(description = "The favicon url to use for this feed", required = true) - private String iconUrl; + @Schema(description = "The favicon url to use for this feed", required = true) + private String iconUrl; - @Schema(description = "entry url", required = true) - private String url; + @Schema(description = "entry url", required = true) + private String url; - @Schema(description = "read status", required = true) - private boolean read; + @Schema(description = "read status", required = true) + private boolean read; - @Schema(description = "starred status", required = true) - private boolean starred; + @Schema(description = "starred status", required = true) + private boolean starred; - @Schema(description = "whether the entry is still markable (old entry statuses are discarded)", required = true) - private boolean markable; + @Schema( + description = "whether the entry is still markable (old entry statuses are discarded)", + required = true) + private boolean markable; - @Schema(description = "tags", required = true) - private List tags; + @Schema(description = "tags", required = true) + private List tags; - public static Entry build(FeedEntryStatus status, boolean proxyImages) { - Entry entry = new Entry(); + public static Entry build(FeedEntryStatus status, boolean proxyImages) { + Entry entry = new Entry(); - FeedEntry feedEntry = status.getEntry(); - FeedSubscription sub = status.getSubscription(); - FeedEntryContent content = feedEntry.getContent(); + FeedEntry feedEntry = status.getEntry(); + FeedSubscription sub = status.getSubscription(); + FeedEntryContent content = feedEntry.getContent(); - entry.setId(String.valueOf(feedEntry.getId())); - entry.setGuid(feedEntry.getGuid()); - entry.setRead(status.isRead()); - entry.setStarred(status.isStarred()); - entry.setMarkable(status.isMarkable()); - entry.setDate(feedEntry.getPublished()); - entry.setInsertedDate(feedEntry.getInserted()); - entry.setUrl(feedEntry.getUrl()); - entry.setFeedName(sub.getTitle()); - entry.setFeedId(String.valueOf(sub.getId())); - entry.setFeedUrl(sub.getFeed().getUrl()); - entry.setFeedLink(sub.getFeed().getLink()); - entry.setIconUrl(FeedUtils.getFaviconUrl(sub)); - entry.setTags(status.getTags().stream().map(FeedEntryTag::getName).toList()); + entry.setId(String.valueOf(feedEntry.getId())); + entry.setGuid(feedEntry.getGuid()); + entry.setRead(status.isRead()); + entry.setStarred(status.isStarred()); + entry.setMarkable(status.isMarkable()); + entry.setDate(feedEntry.getPublished()); + entry.setInsertedDate(feedEntry.getInserted()); + entry.setUrl(feedEntry.getUrl()); + entry.setFeedName(sub.getTitle()); + entry.setFeedId(String.valueOf(sub.getId())); + entry.setFeedUrl(sub.getFeed().getUrl()); + entry.setFeedLink(sub.getFeed().getLink()); + entry.setIconUrl(FeedUtils.getFaviconUrl(sub)); + entry.setTags(status.getTags().stream().map(FeedEntryTag::getName).toList()); - if (content != null) { - entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL); - entry.setTitle(content.getTitle()); - entry.setContent(proxyImages ? FeedUtils.proxyImages(content.getContent()) : content.getContent()); - entry.setAuthor(content.getAuthor()); + if (content != null) { + entry.setRtl(content.getDirection() == FeedEntryContent.Direction.RTL); + entry.setTitle(content.getTitle()); + entry.setContent( + proxyImages + ? FeedUtils.proxyImages(content.getContent()) + : content.getContent()); + entry.setAuthor(content.getAuthor()); - entry.setEnclosureType(content.getEnclosureType()); - entry.setEnclosureUrl(proxyImages && Strings.CS.contains(content.getEnclosureType(), "image") - ? FeedUtils.proxyImage(content.getEnclosureUrl()) - : content.getEnclosureUrl()); + entry.setEnclosureType(content.getEnclosureType()); + entry.setEnclosureUrl( + proxyImages && Strings.CS.contains(content.getEnclosureType(), "image") + ? FeedUtils.proxyImage(content.getEnclosureUrl()) + : content.getEnclosureUrl()); - entry.setMediaDescription(content.getMediaDescription()); - entry.setMediaThumbnailUrl(proxyImages ? FeedUtils.proxyImage(content.getMediaThumbnailUrl()) : content.getMediaThumbnailUrl()); - entry.setMediaThumbnailWidth(content.getMediaThumbnailWidth()); - entry.setMediaThumbnailHeight(content.getMediaThumbnailHeight()); + entry.setMediaDescription(content.getMediaDescription()); + entry.setMediaThumbnailUrl( + proxyImages + ? FeedUtils.proxyImage(content.getMediaThumbnailUrl()) + : content.getMediaThumbnailUrl()); + entry.setMediaThumbnailWidth(content.getMediaThumbnailWidth()); + entry.setMediaThumbnailHeight(content.getMediaThumbnailHeight()); - entry.setCategories(content.getCategories()); - } - - return entry; - } + entry.setCategories(content.getCategories()); + } + return entry; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java index 97f85ca6..b265a307 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java @@ -1,11 +1,9 @@ package com.commafeed.frontend.model; -import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import io.quarkus.runtime.annotations.RegisterForReflection; +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Feed details") @@ -13,10 +11,9 @@ import lombok.Data; @RegisterForReflection public class FeedInfo implements Serializable { - @Schema(description = "url", required = true) - private String url; - - @Schema(description = "title", required = true) - private String title; + @Schema(description = "url", required = true) + private String url; + @Schema(description = "title", required = true) + private String title; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java index 1d1bf062..f0ce520f 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java @@ -1,11 +1,9 @@ package com.commafeed.frontend.model; -import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import io.quarkus.runtime.annotations.RegisterForReflection; +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Server infos") @@ -13,46 +11,44 @@ import lombok.Data; @RegisterForReflection public class ServerInfo implements Serializable { - @Schema - private String announcement; + @Schema private String announcement; - @Schema(required = true) - private String version; + @Schema(required = true) + private String version; - @Schema(required = true) - private String gitCommit; + @Schema(required = true) + private String gitCommit; - @Schema(required = true) - private boolean allowRegistrations; + @Schema(required = true) + private boolean allowRegistrations; - @Schema(required = true) - private boolean emailAddressRequired; + @Schema(required = true) + private boolean emailAddressRequired; - @Schema(required = true) - private boolean smtpEnabled; + @Schema(required = true) + private boolean smtpEnabled; - @Schema(required = true) - private boolean demoAccountEnabled; + @Schema(required = true) + private boolean demoAccountEnabled; - @Schema(required = true) - private boolean websocketEnabled; + @Schema(required = true) + private boolean websocketEnabled; - @Schema(required = true) - private long websocketPingInterval; + @Schema(required = true) + private long websocketPingInterval; - @Schema(required = true) - private long treeReloadInterval; + @Schema(required = true) + private long treeReloadInterval; - @Schema(required = true) - private long forceRefreshCooldownDuration; + @Schema(required = true) + private long forceRefreshCooldownDuration; - @Schema(required = true) - private boolean initialSetupRequired; + @Schema(required = true) + private boolean initialSetupRequired; - @Schema(required = true) - private int minimumPasswordLength; - - @Schema(required = true) - private boolean pushNotificationsEnabled; + @Schema(required = true) + private int minimumPasswordLength; + @Schema(required = true) + private boolean pushNotificationsEnabled; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java index 062ba94b..8b12c420 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java @@ -1,17 +1,14 @@ package com.commafeed.frontend.model; -import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import com.commafeed.backend.model.UserSettings.IconDisplayMode; import com.commafeed.backend.model.UserSettings.PushNotificationType; import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ScrollMode; - import io.quarkus.runtime.annotations.RegisterForReflection; +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "User settings") @@ -19,113 +16,128 @@ import lombok.Data; @RegisterForReflection public class Settings implements Serializable { - @Schema(description = "user's preferred language, english if none") - private String language; + @Schema(description = "user's preferred language, english if none") + private String language; - @Schema(description = "user reads all entries or unread entries only", required = true) - private ReadingMode readingMode; + @Schema(description = "user reads all entries or unread entries only", required = true) + private ReadingMode readingMode; - @Schema(description = "user reads entries in ascending or descending order", required = true) - private ReadingOrder readingOrder; + @Schema(description = "user reads entries in ascending or descending order", required = true) + private ReadingOrder readingOrder; - @Schema(description = "user wants category and feeds with no unread entries shown", required = true) - private boolean showRead; + @Schema( + description = "user wants category and feeds with no unread entries shown", + required = true) + private boolean showRead; - @Schema(description = "In expanded view, scroll through entries mark them as read", required = true) - private boolean scrollMarks; + @Schema( + description = "In expanded view, scroll through entries mark them as read", + required = true) + private boolean scrollMarks; - @Schema(description = "user's custom css for the website") - private String customCss; + @Schema(description = "user's custom css for the website") + private String customCss; - @Schema(description = "user's custom js for the website") - private String customJs; + @Schema(description = "user's custom js for the website") + private String customJs; - @Schema(description = "user's preferred scroll speed when navigating between entries", required = true) - private int scrollSpeed; + @Schema( + description = "user's preferred scroll speed when navigating between entries", + required = true) + private int scrollSpeed; - @Schema(description = "whether to scroll to the selected entry", required = true) - private ScrollMode scrollMode; + @Schema(description = "whether to scroll to the selected entry", required = true) + private ScrollMode scrollMode; - @Schema(description = "number of entries to keep above the selected entry when scrolling", required = true) - private int entriesToKeepOnTopWhenScrolling; + @Schema( + description = "number of entries to keep above the selected entry when scrolling", + required = true) + private int entriesToKeepOnTopWhenScrolling; - @Schema(description = "whether to show the star icon in the header of entries", required = true) - private IconDisplayMode starIconDisplayMode; + @Schema(description = "whether to show the star icon in the header of entries", required = true) + private IconDisplayMode starIconDisplayMode; - @Schema(description = "whether to show the external link icon in the header of entries", required = true) - private IconDisplayMode externalLinkIconDisplayMode; + @Schema( + description = "whether to show the external link icon in the header of entries", + required = true) + private IconDisplayMode externalLinkIconDisplayMode; - @Schema(description = "ask for confirmation when marking all entries as read", required = true) - private boolean markAllAsReadConfirmation; + @Schema(description = "ask for confirmation when marking all entries as read", required = true) + private boolean markAllAsReadConfirmation; - @Schema(description = "navigate to the next unread category or feed after marking all entries as read", required = true) - private boolean markAllAsReadNavigateToNextUnread; + @Schema( + description = + "navigate to the next unread category or feed after marking all entries as read", + required = true) + private boolean markAllAsReadNavigateToNextUnread; - @Schema(description = "show commafeed's own context menu on right click", required = true) - private boolean customContextMenu; + @Schema(description = "show commafeed's own context menu on right click", required = true) + private boolean customContextMenu; - @Schema(description = "on mobile, show action buttons at the bottom of the screen", required = true) - private boolean mobileFooter; + @Schema( + description = "on mobile, show action buttons at the bottom of the screen", + required = true) + private boolean mobileFooter; - @Schema(description = "show unread count in the title", required = true) - private boolean unreadCountTitle; + @Schema(description = "show unread count in the title", required = true) + private boolean unreadCountTitle; - @Schema(description = "show unread count in the favicon", required = true) - private boolean unreadCountFavicon; + @Schema(description = "show unread count in the favicon", required = true) + private boolean unreadCountFavicon; - @Schema(description = "disable pull to refresh", required = true) - private boolean disablePullToRefresh; + @Schema(description = "disable pull to refresh", required = true) + private boolean disablePullToRefresh; - @Schema(description = "primary theme color to use in the UI") - private String primaryColor; + @Schema(description = "primary theme color to use in the UI") + private String primaryColor; - @Schema(description = "sharing settings", required = true) - private SharingSettings sharingSettings = new SharingSettings(); + @Schema(description = "sharing settings", required = true) + private SharingSettings sharingSettings = new SharingSettings(); - @Schema(description = "push notification settings", required = true) - private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings(); + @Schema(description = "push notification settings", required = true) + private PushNotificationSettings pushNotificationSettings = new PushNotificationSettings(); - @Schema(description = "User notification settings") - @Data - public static class PushNotificationSettings implements Serializable { - @Schema(description = "notification provider type") - private PushNotificationType type; + @Schema(description = "User notification settings") + @Data + public static class PushNotificationSettings implements Serializable { + @Schema(description = "notification provider type") + private PushNotificationType type; - @Schema(description = "server URL for ntfy or gotify") - private String serverUrl; + @Schema(description = "server URL for ntfy or gotify") + private String serverUrl; - @Schema(description = "user Id") - private String userId; + @Schema(description = "user Id") + private String userId; - @Schema(description = "user secret for authentication with the service") - private String userSecret; + @Schema(description = "user secret for authentication with the service") + private String userSecret; - @Schema(description = "topic") - private String topic; - } + @Schema(description = "topic") + private String topic; + } - @Schema(description = "User sharing settings") - @Data - public static class SharingSettings implements Serializable { - @Schema(required = true) - private boolean email; + @Schema(description = "User sharing settings") + @Data + public static class SharingSettings implements Serializable { + @Schema(required = true) + private boolean email; - @Schema(required = true) - private boolean gmail; + @Schema(required = true) + private boolean gmail; - @Schema(required = true) - private boolean facebook; + @Schema(required = true) + private boolean facebook; - @Schema(required = true) - private boolean twitter; + @Schema(required = true) + private boolean twitter; - @Schema(required = true) - private boolean tumblr; + @Schema(required = true) + private boolean tumblr; - @Schema(required = true) - private boolean instapaper; + @Schema(required = true) + private boolean instapaper; - @Schema(required = true) - private boolean buffer; - } + @Schema(required = true) + private boolean buffer; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java index 534bbe8f..4a42c9a7 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java @@ -1,18 +1,15 @@ package com.commafeed.frontend.model; -import java.io.Serializable; -import java.time.Instant; - -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedSubscription; - import io.quarkus.runtime.annotations.RegisterForReflection; +import java.io.Serializable; +import java.time.Instant; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "User information") @@ -20,80 +17,90 @@ import lombok.Data; @RegisterForReflection public class Subscription implements Serializable { - @Schema(description = "subscription id", required = true) - private Long id; + @Schema(description = "subscription id", required = true) + private Long id; - @Schema(description = "subscription name", required = true) - private String name; + @Schema(description = "subscription name", required = true) + private String name; - @Schema(description = "error message while fetching the feed") - private String message; + @Schema(description = "error message while fetching the feed") + private String message; - @Schema(description = "error count", required = true) - private int errorCount; + @Schema(description = "error count", required = true) + private int errorCount; - @Schema(description = "last time the feed was refreshed", type = SchemaType.INTEGER) - private Instant lastRefresh; + @Schema(description = "last time the feed was refreshed", type = SchemaType.INTEGER) + private Instant lastRefresh; - @Schema(description = "next time the feed refresh is planned, null if refresh is already queued", type = SchemaType.INTEGER) - private Instant nextRefresh; + @Schema( + description = + "next time the feed refresh is planned, null if refresh is already queued", + type = SchemaType.INTEGER) + private Instant nextRefresh; - @Schema(description = "this subscription's feed url", required = true) - private String feedUrl; + @Schema(description = "this subscription's feed url", required = true) + private String feedUrl; - @Schema(description = "this subscription's website url", required = true) - private String feedLink; + @Schema(description = "this subscription's website url", required = true) + private String feedLink; - @Schema(description = "The favicon url to use for this feed", required = true) - private String iconUrl; + @Schema(description = "The favicon url to use for this feed", required = true) + private String iconUrl; - @Schema(description = "unread count", required = true) - private long unread; + @Schema(description = "unread count", required = true) + private long unread; - @Schema(description = "category id") - private String categoryId; + @Schema(description = "category id") + private String categoryId; - @Schema(description = "position of the subscription's in the list") - private int position; + @Schema(description = "position of the subscription's in the list") + private int position; - @Schema(description = "date of the newest item", type = SchemaType.INTEGER) - private Instant newestItemTime; + @Schema(description = "date of the newest item", type = SchemaType.INTEGER) + private Instant newestItemTime; - @Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match") - private String filter; + @Schema( + description = + "CEL string evaluated on new entries to mark them as read if they do not match") + private String filter; - @Schema(description = "JEXL legacy filter") - private String filterLegacy; + @Schema(description = "JEXL legacy filter") + private String filterLegacy; - @Schema(description = "whether to send push notifications for new entries of this feed", required = true) - private boolean pushNotificationsEnabled; + @Schema( + description = "whether to send push notifications for new entries of this feed", + required = true) + private boolean pushNotificationsEnabled; - @Schema(description = "automatically mark entries as read after this many days (null to disable)") - private Integer autoMarkAsReadAfterDays; + @Schema( + description = + "automatically mark entries as read after this many days (null to disable)") + private Integer autoMarkAsReadAfterDays; - public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) { - FeedCategory category = subscription.getCategory(); - Feed feed = subscription.getFeed(); - Subscription sub = new Subscription(); - sub.setId(subscription.getId()); - sub.setName(subscription.getTitle()); - sub.setPosition(subscription.getPosition()); - sub.setMessage(feed.getMessage()); - sub.setErrorCount(feed.getErrorCount()); - sub.setFeedUrl(feed.getUrl()); - sub.setFeedLink(feed.getLink()); - sub.setIconUrl(FeedUtils.getFaviconUrl(subscription)); - sub.setLastRefresh(feed.getLastUpdated()); - sub.setNextRefresh( - (feed.getDisabledUntil() != null && feed.getDisabledUntil().isBefore(Instant.now())) ? null : feed.getDisabledUntil()); - sub.setUnread(unreadCount.getUnreadCount()); - sub.setNewestItemTime(unreadCount.getNewestItemTime()); - sub.setCategoryId(category == null ? null : String.valueOf(category.getId())); - sub.setFilter(subscription.getFilter()); - sub.setFilterLegacy(subscription.getFilterLegacy()); - sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled()); - sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays()); - return sub; - } - -} \ No newline at end of file + public static Subscription build(FeedSubscription subscription, UnreadCount unreadCount) { + FeedCategory category = subscription.getCategory(); + Feed feed = subscription.getFeed(); + Subscription sub = new Subscription(); + sub.setId(subscription.getId()); + sub.setName(subscription.getTitle()); + sub.setPosition(subscription.getPosition()); + sub.setMessage(feed.getMessage()); + sub.setErrorCount(feed.getErrorCount()); + sub.setFeedUrl(feed.getUrl()); + sub.setFeedLink(feed.getLink()); + sub.setIconUrl(FeedUtils.getFaviconUrl(subscription)); + sub.setLastRefresh(feed.getLastUpdated()); + sub.setNextRefresh( + (feed.getDisabledUntil() != null && feed.getDisabledUntil().isBefore(Instant.now())) + ? null + : feed.getDisabledUntil()); + sub.setUnread(unreadCount.getUnreadCount()); + sub.setNewestItemTime(unreadCount.getNewestItemTime()); + sub.setCategoryId(category == null ? null : String.valueOf(category.getId())); + sub.setFilter(subscription.getFilter()); + sub.setFilterLegacy(subscription.getFilterLegacy()); + sub.setPushNotificationsEnabled(subscription.isPushNotificationsEnabled()); + sub.setAutoMarkAsReadAfterDays(subscription.getAutoMarkAsReadAfterDays()); + return sub; + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java index 52925df7..493a2510 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java @@ -1,36 +1,30 @@ package com.commafeed.frontend.model; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.io.Serializable; import java.time.Instant; - +import lombok.Data; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; -import io.quarkus.runtime.annotations.RegisterForReflection; -import lombok.Data; - @SuppressWarnings("serial") @Schema(description = "Unread count") @Data @RegisterForReflection public class UnreadCount implements Serializable { - @Schema - private long feedId; + @Schema private long feedId; - @Schema - private long unreadCount; + @Schema private long unreadCount; - @Schema(type = SchemaType.INTEGER) - private Instant newestItemTime; + @Schema(type = SchemaType.INTEGER) + private Instant newestItemTime; - public UnreadCount() { - } - - public UnreadCount(long feedId, long unreadCount, Instant newestItemTime) { - this.feedId = feedId; - this.unreadCount = unreadCount; - this.newestItemTime = newestItemTime; - } + public UnreadCount() {} + public UnreadCount(long feedId, long unreadCount, Instant newestItemTime) { + this.feedId = feedId; + this.unreadCount = unreadCount; + this.newestItemTime = newestItemTime; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java index 437fb506..44d24126 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java @@ -1,48 +1,45 @@ package com.commafeed.frontend.model; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.io.Serializable; import java.time.Instant; - +import lombok.Data; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; -import io.quarkus.runtime.annotations.RegisterForReflection; -import lombok.Data; - @SuppressWarnings("serial") @Schema(description = "User information") @Data @RegisterForReflection public class UserModel implements Serializable { - @Schema(description = "user id", required = true) - private Long id; + @Schema(description = "user id", required = true) + private Long id; - @Schema(description = "user name", required = true) - private String name; + @Schema(description = "user name", required = true) + private String name; - @Schema(description = "user email, if any") - private String email; + @Schema(description = "user email, if any") + private String email; - @Schema(description = "api key") - private String apiKey; + @Schema(description = "api key") + private String apiKey; - @Schema(description = "user password, never returned by the api") - private String password; + @Schema(description = "user password, never returned by the api") + private String password; - @Schema(description = "account status", required = true) - private boolean enabled; + @Schema(description = "account status", required = true) + private boolean enabled; - @Schema(description = "account creation date", type = SchemaType.INTEGER) - private Instant created; + @Schema(description = "account creation date", type = SchemaType.INTEGER) + private Instant created; - @Schema(description = "last login date", type = SchemaType.INTEGER) - private Instant lastLogin; + @Schema(description = "last login date", type = SchemaType.INTEGER) + private Instant lastLogin; - @Schema(description = "user is admin", required = true) - private boolean admin; - - @Schema(description = "user last force refresh", type = SchemaType.INTEGER) - private Instant lastForceRefresh; + @Schema(description = "user is admin", required = true) + private boolean admin; + @Schema(description = "user last force refresh", type = SchemaType.INTEGER) + private Instant lastForceRefresh; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java index e7e98e49..2c36bf4a 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AddCategoryRequest.java @@ -1,26 +1,22 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Add Category Request") @Data public class AddCategoryRequest implements Serializable { - @Schema(description = "name", required = true) - @NotEmpty - @Size(max = 128) - private String name; - - @Schema(description = "parent category id, if any") - @Size(max = 128) - private String parentId; + @Schema(description = "name", required = true) + @NotEmpty + @Size(max = 128) + private String name; + @Schema(description = "parent category id, if any") + @Size(max = 128) + private String parentId; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AdminSaveUserRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AdminSaveUserRequest.java index 0548ff5c..5f0e41ba 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AdminSaveUserRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/AdminSaveUserRequest.java @@ -1,34 +1,31 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import com.commafeed.security.password.ValidPassword; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Save User information") @Data public class AdminSaveUserRequest implements Serializable { - @Schema(description = "user id") - private Long id; + @Schema(description = "user id") + private Long id; - @Schema(description = "user name", required = true) - private String name; + @Schema(description = "user name", required = true) + private String name; - @Schema(description = "user email, if any") - private String email; + @Schema(description = "user email, if any") + private String email; - @Schema(description = "user password") - @ValidPassword - private String password; + @Schema(description = "user password") + @ValidPassword + private String password; - @Schema(description = "account status", required = true) - private boolean enabled; + @Schema(description = "account status", required = true) + private boolean enabled; - @Schema(description = "user is admin", required = true) - private boolean admin; + @Schema(description = "user is admin", required = true) + private boolean admin; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java index c4a93605..e6152263 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CategoryModificationRequest.java @@ -1,30 +1,26 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Category modification request") @Data public class CategoryModificationRequest implements Serializable { - @Schema(description = "id", required = true) - private Long id; + @Schema(description = "id", required = true) + private Long id; - @Schema(description = "new name, null if not changed") - @Size(max = 128) - private String name; + @Schema(description = "new name, null if not changed") + @Size(max = 128) + private String name; - @Schema(description = "new parent category id") - @Size(max = 128) - private String parentId; - - @Schema(description = "new display position, null if not changed") - private Integer position; + @Schema(description = "new parent category id") + @Size(max = 128) + private String parentId; + @Schema(description = "new display position, null if not changed") + private Integer position; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java index 1fafcd3e..27f788c0 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/CollapseRequest.java @@ -1,20 +1,17 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Mark Request") @Data public class CollapseRequest implements Serializable { - @Schema(description = "category id", required = true) - private Long id; - - @Schema(description = "collapse", required = true) - private boolean collapse; + @Schema(description = "category id", required = true) + private Long id; + @Schema(description = "collapse", required = true) + private boolean collapse; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java index d7b40108..b972ac6e 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedInfoRequest.java @@ -1,22 +1,18 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Feed information request") @Data public class FeedInfoRequest implements Serializable { - @Schema(description = "feed url", required = true) - @NotEmpty - @Size(max = 4096) - private String url; - + @Schema(description = "feed url", required = true) + @NotEmpty + @Size(max = 4096) + private String url; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java index 6d26c34e..52c283b5 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/FeedModificationRequest.java @@ -1,40 +1,40 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Feed modification request") @Data public class FeedModificationRequest implements Serializable { - @Schema(description = "id", required = true) - private Long id; + @Schema(description = "id", required = true) + private Long id; - @Schema(description = "new name, null if not changed") - @Size(max = 128) - private String name; + @Schema(description = "new name, null if not changed") + @Size(max = 128) + private String name; - @Schema(description = "new parent category id") - @Size(max = 128) - private String categoryId; + @Schema(description = "new parent category id") + @Size(max = 128) + private String categoryId; - @Schema(description = "new display position, null if not changed") - private Integer position; + @Schema(description = "new display position, null if not changed") + private Integer position; - @Schema(description = "CEL string evaluated on new entries to mark them as read if they do not match") - @Size(max = 4096) - private String filter; + @Schema( + description = + "CEL string evaluated on new entries to mark them as read if they do not match") + @Size(max = 4096) + private String filter; - @Schema(description = "whether to send push notifications for new entries of this feed") - private boolean pushNotificationsEnabled; - - @Schema(description = "automatically mark entries as read after this many days (null to disable)") - private Integer autoMarkAsReadAfterDays; + @Schema(description = "whether to send push notifications for new entries of this feed") + private boolean pushNotificationsEnabled; + @Schema( + description = + "automatically mark entries as read after this many days (null to disable)") + private Integer autoMarkAsReadAfterDays; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/IDRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/IDRequest.java index cf821989..82c19cb4 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/IDRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/IDRequest.java @@ -1,17 +1,14 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema @Data public class IDRequest implements Serializable { - @Schema(required = true) - private Long id; - + @Schema(required = true) + private Long id; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/InitialSetupRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/InitialSetupRequest.java index a16182eb..389264c2 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/InitialSetupRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/InitialSetupRequest.java @@ -1,25 +1,22 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import com.commafeed.security.password.ValidPassword; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Initial admin account setup request") @Data public class InitialSetupRequest implements Serializable { - @Schema(description = "admin username", required = true) - private String name; + @Schema(description = "admin username", required = true) + private String name; - @Schema(description = "admin password", required = true) - @ValidPassword - private String password; + @Schema(description = "admin password", required = true) + @ValidPassword + private String password; - @Schema(description = "admin email") - private String email; + @Schema(description = "admin email") + private String email; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java index eed7af70..7a7d1831 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MarkRequest.java @@ -1,40 +1,39 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; -import java.util.List; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; +import java.util.List; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Mark Request") @Data public class MarkRequest implements Serializable { - @Schema(description = "entry id, category id, 'all' or 'starred'", required = true) - @NotEmpty - @Size(max = 128) - private String id; + @Schema(description = "entry id, category id, 'all' or 'starred'", required = true) + @NotEmpty + @Size(max = 128) + private String id; - @Schema(description = "mark as read or unread", required = true) - private boolean read; + @Schema(description = "mark as read or unread", required = true) + private boolean read; - @Schema(description = "mark only entries older than this") - private Long olderThan; + @Schema(description = "mark only entries older than this") + private Long olderThan; - @Schema( - description = "pass the timestamp you got from the entry list to avoid marking entries that may have been fetched in the mean time and never displayed") - private Long insertedBefore; + @Schema( + description = + "pass the timestamp you got from the entry list to avoid marking entries that may have been fetched in the mean time and never displayed") + private Long insertedBefore; - @Schema(description = "only mark read if a feed has these keywords in the title or rss content") - @Size(max = 128) - private String keywords; - - @Schema(description = "if marking a category or 'all', exclude those subscriptions from the marking") - private List excludedSubscriptions; + @Schema(description = "only mark read if a feed has these keywords in the title or rss content") + @Size(max = 128) + private String keywords; + @Schema( + description = + "if marking a category or 'all', exclude those subscriptions from the marking") + private List excludedSubscriptions; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java index afe132af..ef08ad87 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/MultipleMarkRequest.java @@ -1,20 +1,16 @@ package com.commafeed.frontend.model.request; +import jakarta.validation.Valid; import java.io.Serializable; import java.util.List; - -import jakarta.validation.Valid; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Multiple Mark Request") @Data public class MultipleMarkRequest implements Serializable { - @Schema(description = "list of mark requests", required = true) - private List<@Valid MarkRequest> requests; - + @Schema(description = "list of mark requests", required = true) + private List<@Valid MarkRequest> requests; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetConfirmationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetConfirmationRequest.java index d8ed5965..3fd6b42d 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetConfirmationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetConfirmationRequest.java @@ -1,34 +1,30 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - +import com.commafeed.security.password.ValidPassword; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - -import com.commafeed.security.password.ValidPassword; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Data @Schema public class PasswordResetConfirmationRequest implements Serializable { - @Schema(description = "email address for password recovery", required = true) - @Email - @NotEmpty - @Size(max = 255) - private String email; + @Schema(description = "email address for password recovery", required = true) + @Email + @NotEmpty + @Size(max = 255) + private String email; - @Schema(description = "password recovery token", required = true) - @NotEmpty - private String token; + @Schema(description = "password recovery token", required = true) + @NotEmpty + private String token; - @Schema(description = "new password", required = true) - @NotEmpty - @ValidPassword - private String password; + @Schema(description = "new password", required = true) + @NotEmpty + @ValidPassword + private String password; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java index 68ad861e..d4269072 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/PasswordResetRequest.java @@ -1,23 +1,20 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Data @Schema public class PasswordResetRequest implements Serializable { - @Schema(description = "email address for password recovery", required = true) - @Email - @NotEmpty - @Size(max = 255) - private String email; + @Schema(description = "email address for password recovery", required = true) + @Email + @NotEmpty + @Size(max = 255) + private String email; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java index e7a1e270..c38c0b95 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java @@ -1,34 +1,29 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - +import com.commafeed.security.password.ValidPassword; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - -import com.commafeed.security.password.ValidPassword; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Profile modification request") @Data public class ProfileModificationRequest implements Serializable { - @Schema(description = "current user password, required to change profile data", required = true) - @NotEmpty - @Size(max = 128) - private String currentPassword; + @Schema(description = "current user password, required to change profile data", required = true) + @NotEmpty + @Size(max = 128) + private String currentPassword; - @Schema(description = "changes email of the user, if specified") - @Size(max = 255) - private String email; + @Schema(description = "changes email of the user, if specified") + @Size(max = 255) + private String email; - @Schema(description = "changes password of the user, if specified") - @ValidPassword - private String newPassword; - - @Schema(description = "generate a new api key") - private boolean newApiKey; + @Schema(description = "changes password of the user, if specified") + @ValidPassword + private String newPassword; + @Schema(description = "generate a new api key") + private boolean newApiKey; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java index be75474a..e7065de4 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java @@ -1,36 +1,31 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - +import com.commafeed.security.password.ValidPassword; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - -import com.commafeed.security.password.ValidPassword; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Data @Schema public class RegistrationRequest implements Serializable { - @Schema(description = "username, between 3 and 32 characters", required = true) - @NotEmpty - @Size(min = 3, max = 32) - private String name; + @Schema(description = "username, between 3 and 32 characters", required = true) + @NotEmpty + @Size(min = 3, max = 32) + private String name; - @Schema(description = "password", required = true) - @NotEmpty - @ValidPassword - private String password; - - @Schema(description = "email address for password recovery", required = true) - @Email - @NotEmpty - @Size(max = 255) - private String email; + @Schema(description = "password", required = true) + @NotEmpty + @ValidPassword + private String password; + @Schema(description = "email address for password recovery", required = true) + @Email + @NotEmpty + @Size(max = 255) + private String email; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/StarRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/StarRequest.java index 651d30ad..3d141e6f 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/StarRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/StarRequest.java @@ -1,28 +1,24 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Star Request") @Data public class StarRequest implements Serializable { - @Schema(description = "id", required = true) - @NotEmpty - @Size(max = 128) - private String id; + @Schema(description = "id", required = true) + @NotEmpty + @Size(max = 128) + private String id; - @Schema(description = "feed id", required = true) - private Long feedId; - - @Schema(description = "starred or not", required = true) - private boolean starred; + @Schema(description = "feed id", required = true) + private Long feedId; + @Schema(description = "starred or not", required = true) + private boolean starred; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java index 2b1bdc42..ce33925f 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/SubscribeRequest.java @@ -1,30 +1,27 @@ package com.commafeed.frontend.model.request; -import java.io.Serializable; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - +import java.io.Serializable; import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Subscription request") @Data public class SubscribeRequest implements Serializable { - @Schema(description = "url of the feed", required = true) - @NotEmpty - @Size(max = 4096) - private String url; + @Schema(description = "url of the feed", required = true) + @NotEmpty + @Size(max = 4096) + private String url; - @Schema(description = "name of the feed for the user", required = true) - @NotEmpty - @Size(max = 128) - private String title; + @Schema(description = "name of the feed for the user", required = true) + @NotEmpty + @Size(max = 128) + private String title; - @Schema(description = "id of the user category to place the feed in") - @Size(max = 128) - private String categoryId; + @Schema(description = "id of the user category to place the feed in") + @Size(max = 128) + private String categoryId; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/TagRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/TagRequest.java index d5905cc1..80a14b3d 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/TagRequest.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/TagRequest.java @@ -2,20 +2,17 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; import java.util.List; - -import org.eclipse.microprofile.openapi.annotations.media.Schema; - import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; @SuppressWarnings("serial") @Schema(description = "Tag Request") @Data public class TagRequest implements Serializable { - @Schema(description = "entry id", required = true) - private Long entryId; - - @Schema(description = "tags", required = true) - private List tags; + @Schema(description = "entry id", required = true) + private Long entryId; + @Schema(description = "tags", required = true) + private List tags; } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java index 25f0c871..68dc90a3 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java @@ -1,30 +1,5 @@ package com.commafeed.frontend.resource; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - import com.codahale.metrics.MetricRegistry; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserRoleDAO; @@ -40,8 +15,29 @@ import com.commafeed.security.AuthenticationContext; import com.commafeed.security.Roles; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; - +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Path("/rest/admin") @RolesAllowed(Roles.ADMIN) @@ -52,138 +48,156 @@ import lombok.RequiredArgsConstructor; @Tag(name = "Admin") public class AdminREST { - private final AuthenticationContext authenticationContext; - private final UserDAO userDAO; - private final UserRoleDAO userRoleDAO; - private final UserService userService; - private final PasswordEncryptionService encryptionService; - private final MetricRegistry metrics; + private final AuthenticationContext authenticationContext; + private final UserDAO userDAO; + private final UserRoleDAO userRoleDAO; + private final UserService userService; + private final PasswordEncryptionService encryptionService; + private final MetricRegistry metrics; - @Path("/user/save") - @POST - @Transactional - @Operation( - summary = "Save or update a user", - description = "Save or update a user. If the id is not specified, a new user will be created") - public Response adminSaveUser(@Valid @Parameter(required = true) AdminSaveUserRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getName()); + @Path("/user/save") + @POST + @Transactional + @Operation( + summary = "Save or update a user", + description = + "Save or update a user. If the id is not specified, a new user will be created") + public Response adminSaveUser(@Valid @Parameter(required = true) AdminSaveUserRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getName()); - Long id = req.getId(); - if (id == null) { - Preconditions.checkNotNull(req.getPassword()); + Long id = req.getId(); + if (id == null) { + Preconditions.checkNotNull(req.getPassword()); - Set roles = Sets.newHashSet(Role.USER); - if (req.isAdmin()) { - roles.add(Role.ADMIN); - } - try { - id = userService.register(req.getName(), req.getPassword(), req.getEmail(), roles, true).getId(); - } catch (Exception e) { - return Response.status(Status.CONFLICT).entity(e.getMessage()).build(); - } - } else { - User user = authenticationContext.getCurrentUser(); - if (req.getId().equals(user.getId()) && !req.isEnabled()) { - return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build(); - } + Set roles = Sets.newHashSet(Role.USER); + if (req.isAdmin()) { + roles.add(Role.ADMIN); + } + try { + id = + userService + .register( + req.getName(), + req.getPassword(), + req.getEmail(), + roles, + true) + .getId(); + } catch (Exception e) { + return Response.status(Status.CONFLICT).entity(e.getMessage()).build(); + } + } else { + User user = authenticationContext.getCurrentUser(); + if (req.getId().equals(user.getId()) && !req.isEnabled()) { + return Response.status(Status.FORBIDDEN) + .entity("You cannot disable your own account.") + .build(); + } - User u = userDAO.findById(id); - u.setName(req.getName()); - if (StringUtils.isNotBlank(req.getPassword())) { - u.setPassword(encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt())); - } - u.setEmail(req.getEmail()); - u.setDisabled(!req.isEnabled()); + User u = userDAO.findById(id); + u.setName(req.getName()); + if (StringUtils.isNotBlank(req.getPassword())) { + u.setPassword( + encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt())); + } + u.setEmail(req.getEmail()); + u.setDisabled(!req.isEnabled()); - Set roles = userRoleDAO.findRoles(u); - if (req.isAdmin() && !roles.contains(Role.ADMIN)) { - userRoleDAO.persist(new UserRole(u, Role.ADMIN)); - } else if (!req.isAdmin() && roles.contains(Role.ADMIN)) { - if (userRoleDAO.countAdmins() == 1) { - return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the last admin user.").build(); - } - for (UserRole userRole : userRoleDAO.findAll(u)) { - if (userRole.getRole() == Role.ADMIN) { - userRoleDAO.delete(userRole); - } - } - } + Set roles = userRoleDAO.findRoles(u); + if (req.isAdmin() && !roles.contains(Role.ADMIN)) { + userRoleDAO.persist(new UserRole(u, Role.ADMIN)); + } else if (!req.isAdmin() && roles.contains(Role.ADMIN)) { + if (userRoleDAO.countAdmins() == 1) { + return Response.status(Status.FORBIDDEN) + .entity("You cannot remove the admin role from the last admin user.") + .build(); + } + for (UserRole userRole : userRoleDAO.findAll(u)) { + if (userRole.getRole() == Role.ADMIN) { + userRoleDAO.delete(userRole); + } + } + } + } + return Response.ok(id).build(); + } - } - return Response.ok(id).build(); + @Path("/user/get/{id}") + @GET + @Transactional + @Operation(summary = "Get user information", description = "Get user information") + public UserModel adminGetUser( + @Parameter(description = "user id", required = true) @PathParam("id") Long id) { + Preconditions.checkNotNull(id); + User u = userDAO.findById(id); + UserModel userModel = new UserModel(); + userModel.setId(u.getId()); + userModel.setName(u.getName()); + userModel.setEmail(u.getEmail()); + userModel.setEnabled(!u.isDisabled()); + userModel.setAdmin( + userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN)); + return userModel; + } - } + @Path("/user/getAll") + @GET + @Transactional + @Operation(summary = "Get all users", description = "Get all users") + public List adminGetUsers() { + Map users = new HashMap<>(); + for (UserRole role : userRoleDAO.findAll()) { + User u = role.getUser(); + UserModel userModel = + users.computeIfAbsent( + u.getId(), + k -> { + UserModel um = new UserModel(); + um.setId(u.getId()); + um.setName(u.getName()); + um.setEmail(u.getEmail()); + um.setEnabled(!u.isDisabled()); + um.setCreated(u.getCreated()); + um.setLastLogin(u.getLastLogin()); + return um; + }); - @Path("/user/get/{id}") - @GET - @Transactional - @Operation(summary = "Get user information", description = "Get user information") - public UserModel adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) { - Preconditions.checkNotNull(id); - User u = userDAO.findById(id); - UserModel userModel = new UserModel(); - userModel.setId(u.getId()); - userModel.setName(u.getName()); - userModel.setEmail(u.getEmail()); - userModel.setEnabled(!u.isDisabled()); - userModel.setAdmin(userRoleDAO.findAll(u).stream().anyMatch(r -> r.getRole() == Role.ADMIN)); - return userModel; - } + if (role.getRole() == Role.ADMIN) { + userModel.setAdmin(true); + } + } + return new ArrayList<>(users.values()); + } - @Path("/user/getAll") - @GET - @Transactional - @Operation(summary = "Get all users", description = "Get all users") - public List adminGetUsers() { - Map users = new HashMap<>(); - for (UserRole role : userRoleDAO.findAll()) { - User u = role.getUser(); - UserModel userModel = users.computeIfAbsent(u.getId(), k -> { - UserModel um = new UserModel(); - um.setId(u.getId()); - um.setName(u.getName()); - um.setEmail(u.getEmail()); - um.setEnabled(!u.isDisabled()); - um.setCreated(u.getCreated()); - um.setLastLogin(u.getLastLogin()); - return um; - }); + @Path("/user/delete") + @POST + @Transactional + @Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions") + public Response adminDeleteUser(@Parameter(required = true) IDRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - if (role.getRole() == Role.ADMIN) { - userModel.setAdmin(true); - } - } - return new ArrayList<>(users.values()); - } + User u = userDAO.findById(req.getId()); + if (u == null) { + return Response.status(Status.NOT_FOUND).build(); + } - @Path("/user/delete") - @POST - @Transactional - @Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions") - public Response adminDeleteUser(@Parameter(required = true) IDRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); - - User u = userDAO.findById(req.getId()); - if (u == null) { - return Response.status(Status.NOT_FOUND).build(); - } - - User user = authenticationContext.getCurrentUser(); - if (user.getId().equals(u.getId())) { - return Response.status(Status.FORBIDDEN).entity("You cannot delete your own user.").build(); - } - userService.unregister(u); - return Response.ok().build(); - } - - @Path("/metrics") - @GET - @Transactional - @Operation(summary = "Retrieve server metrics") - public Response getMetrics() { - return Response.ok(metrics).build(); - } + User user = authenticationContext.getCurrentUser(); + if (user.getId().equals(u.getId())) { + return Response.status(Status.FORBIDDEN) + .entity("You cannot delete your own user.") + .build(); + } + userService.unregister(u); + return Response.ok().build(); + } + @Path("/metrics") + @GET + @Transactional + @Operation(summary = "Retrieve server metrics") + public Response getMetrics() { + return Response.ok(metrics).build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java index b1e2fdb7..812087fd 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java @@ -1,41 +1,5 @@ package com.commafeed.frontend.resource; -import java.io.StringWriter; -import java.time.Instant; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriInfo; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Strings; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -67,9 +31,41 @@ import com.google.common.collect.Lists; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; - +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; +import java.io.StringWriter; +import java.time.Instant; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Path("/rest/category") @RolesAllowed(Roles.USER) @@ -81,376 +77,472 @@ import lombok.extern.slf4j.Slf4j; @Tag(name = "Feed categories") public class CategoryREST { - public static final String ALL = "all"; - public static final String STARRED = "starred"; + public static final String ALL = "all"; + public static final String STARRED = "starred"; - private static final Comparator CATEGORY_COMPARATOR = Comparator.comparing(Category::getPosition) - .thenComparing(Category::getName); - private static final Comparator SUBSCRIPTION_COMPARATOR = Comparator.comparing(Subscription::getPosition) - .thenComparing(Subscription::getName); + private static final Comparator CATEGORY_COMPARATOR = + Comparator.comparing(Category::getPosition).thenComparing(Category::getName); + private static final Comparator SUBSCRIPTION_COMPARATOR = + Comparator.comparing(Subscription::getPosition).thenComparing(Subscription::getName); - private final AuthenticationContext authenticationContext; - private final FeedCategoryDAO feedCategoryDAO; - private final FeedEntryStatusDAO feedEntryStatusDAO; - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final FeedEntryService feedEntryService; - private final FeedSubscriptionService feedSubscriptionService; - private final CommaFeedConfiguration config; - private final UriInfo uri; + private final AuthenticationContext authenticationContext; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedEntryService feedEntryService; + private final FeedSubscriptionService feedSubscriptionService; + private final CommaFeedConfiguration config; + private final UriInfo uri; - @Path("/entries") - @GET - @Transactional - @Operation(summary = "Get category entries", description = "Get a list of category entries") - @APIResponse( - responseCode = "200", - content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Entries.class)) }) - @APIResponse(responseCode = "404", description = "category not found") - public Response getCategoryEntries( - @Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, - @Parameter( - description = "all entries or only unread ones", - required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, - @Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan, - @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, - @Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, - @Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, - @Parameter( - description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords, - @Parameter( - description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, - @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) { + @Path("/entries") + @GET + @Transactional + @Operation(summary = "Get category entries", description = "Get a list of category entries") + @APIResponse( + responseCode = "200", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Entries.class)) + }) + @APIResponse(responseCode = "404", description = "category not found") + public Response getCategoryEntries( + @Parameter(description = "id of the category, 'all' or 'starred'", required = true) + @QueryParam("id") + String id, + @Parameter(description = "all entries or only unread ones", required = true) + @DefaultValue("unread") + @QueryParam("readType") + ReadingMode readType, + @Parameter(description = "only entries newer than this") @QueryParam("newerThan") + Long newerThan, + @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") + int offset, + @Parameter(description = "limit for paging, default 20, maximum 1000") + @DefaultValue("20") + @QueryParam("limit") + int limit, + @Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") + ReadingOrder order, + @Parameter( + description = + "search for keywords in either the title or the content of the entries, separated by spaces") + @QueryParam("keywords") + String keywords, + @Parameter(description = "comma-separated list of excluded subscription ids") + @QueryParam("excludedSubscriptionIds") + String excludedSubscriptionIds, + @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") + String tag) { - Preconditions.checkNotNull(readType); + Preconditions.checkNotNull(readType); - List entryKeywords = FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords)); + List entryKeywords = + FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords)); - limit = Math.min(limit, 1000); - limit = Math.max(0, limit); + limit = Math.min(limit, 1000); + limit = Math.max(0, limit); - Entries entries = new Entries(); - entries.setOffset(offset); - entries.setLimit(limit); - boolean unreadOnly = readType == ReadingMode.UNREAD; - if (StringUtils.isBlank(id)) { - id = ALL; - } + Entries entries = new Entries(); + entries.setOffset(offset); + entries.setLimit(limit); + boolean unreadOnly = readType == ReadingMode.UNREAD; + if (StringUtils.isBlank(id)) { + id = ALL; + } - Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan); + Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan); - List excludedIds = null; - if (StringUtils.isNotEmpty(excludedSubscriptionIds)) { - excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList(); - } + List excludedIds = null; + if (StringUtils.isNotEmpty(excludedSubscriptionIds)) { + excludedIds = + Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList(); + } - User user = authenticationContext.getCurrentUser(); - if (ALL.equals(id)) { - entries.setName(Optional.ofNullable(tag).orElse("All")); + User user = authenticationContext.getCurrentUser(); + if (ALL.equals(id)) { + entries.setName(Optional.ofNullable(tag).orElse("All")); - List subs = feedSubscriptionDAO.findAll(user); - removeExcludedSubscriptions(subs, excludedIds); - List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, - offset, limit + 1, order, true, tag, null, null); + List subs = feedSubscriptionDAO.findAll(user); + removeExcludedSubscriptions(subs, excludedIds); + List list = + feedEntryStatusDAO.findBySubscriptions( + user, + subs, + unreadOnly, + entryKeywords, + newerThanDate, + offset, + limit + 1, + order, + true, + tag, + null, + null); - for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); - } + for (FeedEntryStatus status : list) { + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); + } - } else if (STARRED.equals(id)) { - entries.setName("Starred"); - List starred = feedEntryStatusDAO.findStarred(user, entryKeywords, newerThanDate, offset, limit + 1, order, - true); - for (FeedEntryStatus status : starred) { - entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); - } - } else { - FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id)); - if (parent != null) { - List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); - List subs = feedSubscriptionDAO.findByCategories(user, categories); - removeExcludedSubscriptions(subs, excludedIds); - List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, - offset, limit + 1, order, true, tag, null, null); + } else if (STARRED.equals(id)) { + entries.setName("Starred"); + List starred = + feedEntryStatusDAO.findStarred( + user, entryKeywords, newerThanDate, offset, limit + 1, order, true); + for (FeedEntryStatus status : starred) { + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); + } + } else { + FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id)); + if (parent != null) { + List categories = + feedCategoryDAO.findAllChildrenCategories(user, parent); + List subs = + feedSubscriptionDAO.findByCategories(user, categories); + removeExcludedSubscriptions(subs, excludedIds); + List list = + feedEntryStatusDAO.findBySubscriptions( + user, + subs, + unreadOnly, + entryKeywords, + newerThanDate, + offset, + limit + 1, + order, + true, + tag, + null, + null); - for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); - } - entries.setName(parent.getName()); - } else { - return Response.status(Status.NOT_FOUND).entity("category not found").build(); - } - } + for (FeedEntryStatus status : list) { + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); + } + entries.setName(parent.getName()); + } else { + return Response.status(Status.NOT_FOUND) + .entity("category not found") + .build(); + } + } - boolean hasMore = entries.getEntries().size() > limit; - if (hasMore) { - entries.setHasMore(true); - entries.getEntries().removeLast(); - } + boolean hasMore = entries.getEntries().size() > limit; + if (hasMore) { + entries.setHasMore(true); + entries.getEntries().removeLast(); + } - entries.setTimestamp(System.currentTimeMillis()); - entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null); - return Response.ok(entries).build(); - } + entries.setTimestamp(System.currentTimeMillis()); + entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null); + return Response.ok(entries).build(); + } - @Path("/entriesAsFeed") - @GET - @Transactional - @Operation(summary = "Get category entries as feed", description = "Get a feed of category entries") - @Produces(MediaType.APPLICATION_XML) - public Response getCategoryEntriesAsFeed( - @Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id, - @Parameter( - description = "all entries or only unread ones", - required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, - @Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan, - @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, - @Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, - @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, - @Parameter( - description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords, - @Parameter( - description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, - @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) { + @Path("/entriesAsFeed") + @GET + @Transactional + @Operation( + summary = "Get category entries as feed", + description = "Get a feed of category entries") + @Produces(MediaType.APPLICATION_XML) + public Response getCategoryEntriesAsFeed( + @Parameter(description = "id of the category, 'all' or 'starred'", required = true) + @QueryParam("id") + String id, + @Parameter(description = "all entries or only unread ones", required = true) + @DefaultValue("all") + @QueryParam("readType") + ReadingMode readType, + @Parameter(description = "only entries newer than this") @QueryParam("newerThan") + Long newerThan, + @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") + int offset, + @Parameter(description = "limit for paging, default 20, maximum 1000") + @DefaultValue("20") + @QueryParam("limit") + int limit, + @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") + ReadingOrder order, + @Parameter( + description = + "search for keywords in either the title or the content of the entries, separated by spaces") + @QueryParam("keywords") + String keywords, + @Parameter(description = "comma-separated list of excluded subscription ids") + @QueryParam("excludedSubscriptionIds") + String excludedSubscriptionIds, + @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") + String tag) { - Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag); - if (response.getStatus() != Status.OK.getStatusCode()) { - return response; - } - Entries entries = (Entries) response.getEntity(); + Response response = + getCategoryEntries( + id, + readType, + newerThan, + offset, + limit, + order, + keywords, + excludedSubscriptionIds, + tag); + if (response.getStatus() != Status.OK.getStatusCode()) { + return response; + } + Entries entries = (Entries) response.getEntity(); - SyndFeed feed = new SyndFeedImpl(); - feed.setFeedType("rss_2.0"); - feed.setTitle("CommaFeed - " + entries.getName()); - feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(uri.getBaseUri().toString()); - feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList()); + SyndFeed feed = new SyndFeedImpl(); + feed.setFeedType("rss_2.0"); + feed.setTitle("CommaFeed - " + entries.getName()); + feed.setDescription("CommaFeed - " + entries.getName()); + feed.setLink(uri.getBaseUri().toString()); + feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList()); - SyndFeedOutput output = new SyndFeedOutput(); - StringWriter writer = new StringWriter(); - try { - output.output(feed, writer); - } catch (Exception e) { - writer.write("Could not get feed information"); - log.error(e.getMessage(), e); - } - return Response.ok(writer.toString()).build(); - } + SyndFeedOutput output = new SyndFeedOutput(); + StringWriter writer = new StringWriter(); + try { + output.output(feed, writer); + } catch (Exception e) { + writer.write("Could not get feed information"); + log.error(e.getMessage(), e); + } + return Response.ok(writer.toString()).build(); + } - @Path("/mark") - @POST - @Transactional - @Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read") - public Response markCategoryEntries(@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + @Path("/mark") + @POST + @Transactional + @Operation( + summary = "Mark category entries", + description = "Mark feed entries of this category as read") + public Response markCategoryEntries( + @Valid @Parameter(description = "category id, or 'all'", required = true) + MarkRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - Instant olderThan = req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan()); - Instant insertedBefore = req.getInsertedBefore() == null ? null : Instant.ofEpochMilli(req.getInsertedBefore()); - String keywords = req.getKeywords(); - List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); + Instant olderThan = + req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan()); + Instant insertedBefore = + req.getInsertedBefore() == null + ? null + : Instant.ofEpochMilli(req.getInsertedBefore()); + String keywords = req.getKeywords(); + List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); - User user = authenticationContext.getCurrentUser(); - if (ALL.equals(req.getId())) { - List subs = feedSubscriptionDAO.findAll(user); - removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); - feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords); - } else if (STARRED.equals(req.getId())) { - feedEntryService.markStarredEntries(user, olderThan, insertedBefore); - } else { - FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(req.getId())); - List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); - List subs = feedSubscriptionDAO.findByCategories(user, categories); - removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); - feedEntryService.markSubscriptionEntries(user, subs, olderThan, insertedBefore, entryKeywords); - } - return Response.ok().build(); - } + User user = authenticationContext.getCurrentUser(); + if (ALL.equals(req.getId())) { + List subs = feedSubscriptionDAO.findAll(user); + removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); + feedEntryService.markSubscriptionEntries( + user, subs, olderThan, insertedBefore, entryKeywords); + } else if (STARRED.equals(req.getId())) { + feedEntryService.markStarredEntries(user, olderThan, insertedBefore); + } else { + FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(req.getId())); + List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); + List subs = feedSubscriptionDAO.findByCategories(user, categories); + removeExcludedSubscriptions(subs, req.getExcludedSubscriptions()); + feedEntryService.markSubscriptionEntries( + user, subs, olderThan, insertedBefore, entryKeywords); + } + return Response.ok().build(); + } - private void removeExcludedSubscriptions(List subs, List excludedIds) { - if (CollectionUtils.isNotEmpty(excludedIds)) { - subs.removeIf(sub -> excludedIds.contains(sub.getId())); - } - } + private void removeExcludedSubscriptions(List subs, List excludedIds) { + if (CollectionUtils.isNotEmpty(excludedIds)) { + subs.removeIf(sub -> excludedIds.contains(sub.getId())); + } + } - @Path("/add") - @POST - @Transactional - @Operation(summary = "Add a category", description = "Add a new feed category") - public Long addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getName()); + @Path("/add") + @POST + @Transactional + @Operation(summary = "Add a category", description = "Add a new feed category") + public Long addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getName()); - User user = authenticationContext.getCurrentUser(); + User user = authenticationContext.getCurrentUser(); - FeedCategory cat = new FeedCategory(); - cat.setName(req.getName()); - cat.setUser(user); - cat.setPosition(0); - String parentId = req.getParentId(); - if (parentId != null && !ALL.equals(parentId)) { - FeedCategory parent = new FeedCategory(); - parent.setId(Long.valueOf(parentId)); - cat.setParent(parent); - } - feedCategoryDAO.persist(cat); - return cat.getId(); - } + FeedCategory cat = new FeedCategory(); + cat.setName(req.getName()); + cat.setUser(user); + cat.setPosition(0); + String parentId = req.getParentId(); + if (parentId != null && !ALL.equals(parentId)) { + FeedCategory parent = new FeedCategory(); + parent.setId(Long.valueOf(parentId)); + cat.setParent(parent); + } + feedCategoryDAO.persist(cat); + return cat.getId(); + } - @POST - @Path("/delete") - @Transactional - @Operation(summary = "Delete a category", description = "Delete an existing feed category") - public Response deleteCategory(@Parameter(required = true) IDRequest req) { + @POST + @Path("/delete") + @Transactional + @Operation(summary = "Delete a category", description = "Delete an existing feed category") + public Response deleteCategory(@Parameter(required = true) IDRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - User user = authenticationContext.getCurrentUser(); - FeedCategory cat = feedCategoryDAO.findById(user, req.getId()); - if (cat != null) { - List subs = feedSubscriptionDAO.findByCategory(user, cat); - for (FeedSubscription sub : subs) { - sub.setCategory(null); - } + User user = authenticationContext.getCurrentUser(); + FeedCategory cat = feedCategoryDAO.findById(user, req.getId()); + if (cat != null) { + List subs = feedSubscriptionDAO.findByCategory(user, cat); + for (FeedSubscription sub : subs) { + sub.setCategory(null); + } - List categories = feedCategoryDAO.findAllChildrenCategories(user, cat); - for (FeedCategory child : categories) { - if (!child.getId().equals(cat.getId()) && child.getParent().getId().equals(cat.getId())) { - child.setParent(null); - } - } + List categories = feedCategoryDAO.findAllChildrenCategories(user, cat); + for (FeedCategory child : categories) { + if (!child.getId().equals(cat.getId()) + && child.getParent().getId().equals(cat.getId())) { + child.setParent(null); + } + } - feedCategoryDAO.delete(cat); - return Response.ok().build(); - } else { - return Response.status(Status.NOT_FOUND).build(); - } - } + feedCategoryDAO.delete(cat); + return Response.ok().build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } - @POST - @Path("/modify") - @Transactional - @Operation(summary = "Modify a category", description = "Modify an existing feed category") - public Response modifyCategory(@Valid @Parameter(required = true) CategoryModificationRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + @POST + @Path("/modify") + @Transactional + @Operation(summary = "Modify a category", description = "Modify an existing feed category") + public Response modifyCategory( + @Valid @Parameter(required = true) CategoryModificationRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - User user = authenticationContext.getCurrentUser(); - FeedCategory category = feedCategoryDAO.findById(user, req.getId()); + User user = authenticationContext.getCurrentUser(); + FeedCategory category = feedCategoryDAO.findById(user, req.getId()); - if (StringUtils.isNotBlank(req.getName())) { - category.setName(req.getName()); - } + if (StringUtils.isNotBlank(req.getName())) { + category.setName(req.getName()); + } - FeedCategory parent = null; - if (req.getParentId() != null && !ALL.equals(req.getParentId()) - && !Strings.CS.equals(req.getParentId(), String.valueOf(req.getId()))) { - parent = feedCategoryDAO.findById(user, Long.valueOf(req.getParentId())); - } - category.setParent(parent); + FeedCategory parent = null; + if (req.getParentId() != null + && !ALL.equals(req.getParentId()) + && !Strings.CS.equals(req.getParentId(), String.valueOf(req.getId()))) { + parent = feedCategoryDAO.findById(user, Long.valueOf(req.getParentId())); + } + category.setParent(parent); - if (req.getPosition() != null) { - List categories = feedCategoryDAO.findByParent(user, parent); - categories.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition())); + if (req.getPosition() != null) { + List categories = feedCategoryDAO.findByParent(user, parent); + categories.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition())); - int existingIndex = -1; - for (int i = 0; i < categories.size(); i++) { - if (Objects.equals(categories.get(i).getId(), category.getId())) { - existingIndex = i; - } - } - if (existingIndex != -1) { - categories.remove(existingIndex); - } + int existingIndex = -1; + for (int i = 0; i < categories.size(); i++) { + if (Objects.equals(categories.get(i).getId(), category.getId())) { + existingIndex = i; + } + } + if (existingIndex != -1) { + categories.remove(existingIndex); + } - categories.add(Math.min(req.getPosition(), categories.size()), category); - for (int i = 0; i < categories.size(); i++) { - categories.get(i).setPosition(i); - } - } + categories.add(Math.min(req.getPosition(), categories.size()), category); + for (int i = 0; i < categories.size(); i++) { + categories.get(i).setPosition(i); + } + } - return Response.ok().build(); - } + return Response.ok().build(); + } - @POST - @Path("/collapse") - @Transactional - @Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category") - public Response collapseCategory(@Parameter(required = true) CollapseRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + @POST + @Path("/collapse") + @Transactional + @Operation( + summary = "Collapse a category", + description = "Save collapsed or expanded status for a category") + public Response collapseCategory(@Parameter(required = true) CollapseRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - User user = authenticationContext.getCurrentUser(); - FeedCategory category = feedCategoryDAO.findById(user, req.getId()); - if (category == null) { - return Response.status(Status.NOT_FOUND).build(); - } - category.setCollapsed(req.isCollapse()); + User user = authenticationContext.getCurrentUser(); + FeedCategory category = feedCategoryDAO.findById(user, req.getId()); + if (category == null) { + return Response.status(Status.NOT_FOUND).build(); + } + category.setCollapsed(req.isCollapse()); - return Response.ok().build(); - } + return Response.ok().build(); + } - @GET - @Path("/unreadCount") - @Transactional - @Operation(summary = "Get unread count for feed subscriptions") - public List getUnreadCount() { - User user = authenticationContext.getCurrentUser(); - Map unreadCount = feedSubscriptionService.getUnreadCount(user); - return Lists.newArrayList(unreadCount.values()); - } + @GET + @Path("/unreadCount") + @Transactional + @Operation(summary = "Get unread count for feed subscriptions") + public List getUnreadCount() { + User user = authenticationContext.getCurrentUser(); + Map unreadCount = feedSubscriptionService.getUnreadCount(user); + return Lists.newArrayList(unreadCount.values()); + } - @GET - @Path("/get") - @Transactional - @Operation(summary = "Get root category", description = "Get all categories and subscriptions of the user") - public Category getRootCategory() { - User user = authenticationContext.getCurrentUser(); + @GET + @Path("/get") + @Transactional + @Operation( + summary = "Get root category", + description = "Get all categories and subscriptions of the user") + public Category getRootCategory() { + User user = authenticationContext.getCurrentUser(); - List categories = feedCategoryDAO.findAll(user); - List subscriptions = feedSubscriptionDAO.findAll(user); - Map unreadCount = feedSubscriptionService.getUnreadCount(user); + List categories = feedCategoryDAO.findAll(user); + List subscriptions = feedSubscriptionDAO.findAll(user); + Map unreadCount = feedSubscriptionService.getUnreadCount(user); - Category root = buildCategory(null, categories, subscriptions, unreadCount); - root.setId("all"); - root.setName("All"); + Category root = buildCategory(null, categories, subscriptions, unreadCount); + root.setId("all"); + root.setName("All"); - return root; - } + return root; + } - private Category buildCategory(Long id, List categories, List subscriptions, - Map unreadCount) { - Category category = new Category(); - category.setId(String.valueOf(id)); - category.setExpanded(true); + private Category buildCategory( + Long id, + List categories, + List subscriptions, + Map unreadCount) { + Category category = new Category(); + category.setId(String.valueOf(id)); + category.setExpanded(true); - for (FeedCategory c : categories) { - if (id == null && c.getParent() == null || c.getParent() != null && Objects.equals(c.getParent().getId(), id)) { - Category child = buildCategory(c.getId(), categories, subscriptions, unreadCount); - child.setId(String.valueOf(c.getId())); - child.setName(c.getName()); - child.setPosition(c.getPosition()); - if (c.getParent() != null && c.getParent().getId() != null) { - child.setParentId(String.valueOf(c.getParent().getId())); - child.setParentName(c.getParent().getName()); - } - child.setExpanded(!c.isCollapsed()); - category.getChildren().add(child); - } - } - category.getChildren().sort(CATEGORY_COMPARATOR); + for (FeedCategory c : categories) { + if (id == null && c.getParent() == null + || c.getParent() != null && Objects.equals(c.getParent().getId(), id)) { + Category child = buildCategory(c.getId(), categories, subscriptions, unreadCount); + child.setId(String.valueOf(c.getId())); + child.setName(c.getName()); + child.setPosition(c.getPosition()); + if (c.getParent() != null && c.getParent().getId() != null) { + child.setParentId(String.valueOf(c.getParent().getId())); + child.setParentName(c.getParent().getName()); + } + child.setExpanded(!c.isCollapsed()); + category.getChildren().add(child); + } + } + category.getChildren().sort(CATEGORY_COMPARATOR); - for (FeedSubscription subscription : subscriptions) { - if (id == null && subscription.getCategory() == null - || subscription.getCategory() != null && Objects.equals(subscription.getCategory().getId(), id)) { - UnreadCount uc = unreadCount.get(subscription.getId()); - Subscription sub = Subscription.build(subscription, uc); - category.getFeeds().add(sub); - } - } - category.getFeeds().sort(SUBSCRIPTION_COMPARATOR); - - return category; - } + for (FeedSubscription subscription : subscriptions) { + if (id == null && subscription.getCategory() == null + || subscription.getCategory() != null + && Objects.equals(subscription.getCategory().getId(), id)) { + UnreadCount uc = unreadCount.get(subscription.getId()); + Subscription sub = Subscription.build(subscription, uc); + category.getFeeds().add(sub); + } + } + category.getFeeds().sort(SUBSCRIPTION_COMPARATOR); + return category; + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java index 8fad1a25..5faa1b08 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java @@ -1,23 +1,5 @@ package com.commafeed.frontend.resource; -import java.util.List; - -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryService; @@ -29,8 +11,22 @@ import com.commafeed.frontend.model.request.TagRequest; import com.commafeed.security.AuthenticationContext; import com.commafeed.security.Roles; import com.google.common.base.Preconditions; - +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Path("/rest/entry") @RolesAllowed(Roles.USER) @@ -41,78 +37,87 @@ import lombok.RequiredArgsConstructor; @Tag(name = "Feed entries") public class EntryREST { - private final AuthenticationContext authenticationContext; - private final FeedEntryTagDAO feedEntryTagDAO; - private final FeedEntryService feedEntryService; - private final FeedEntryTagService feedEntryTagService; + private final AuthenticationContext authenticationContext; + private final FeedEntryTagDAO feedEntryTagDAO; + private final FeedEntryService feedEntryService; + private final FeedEntryTagService feedEntryTagService; - @Path("/mark") - @POST - @Transactional - @Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread") - public Response markEntry(@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + @Path("/mark") + @POST + @Transactional + @Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread") + public Response markEntry( + @Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - User user = authenticationContext.getCurrentUser(); - feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead()); - return Response.ok().build(); - } + User user = authenticationContext.getCurrentUser(); + feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead()); + return Response.ok().build(); + } - @Path("/markMultiple") - @POST - @Transactional - @Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread") - public Response markEntries(@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getRequests()); + @Path("/markMultiple") + @POST + @Transactional + @Operation( + summary = "Mark multiple feed entries", + description = "Mark feed entries as read/unread") + public Response markEntries( + @Valid @Parameter(description = "Multiple Mark Request", required = true) + MultipleMarkRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getRequests()); - User user = authenticationContext.getCurrentUser(); - for (MarkRequest r : req.getRequests()) { - Preconditions.checkNotNull(r.getId()); - feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead()); - } + User user = authenticationContext.getCurrentUser(); + for (MarkRequest r : req.getRequests()) { + Preconditions.checkNotNull(r.getId()); + feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead()); + } - return Response.ok().build(); - } + return Response.ok().build(); + } - @Path("/star") - @POST - @Transactional - @Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread") - public Response starEntry(@Valid @Parameter(description = "Star Request", required = true) StarRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); - Preconditions.checkNotNull(req.getFeedId()); + @Path("/star") + @POST + @Transactional + @Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread") + public Response starEntry( + @Valid @Parameter(description = "Star Request", required = true) StarRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); + Preconditions.checkNotNull(req.getFeedId()); - User user = authenticationContext.getCurrentUser(); - feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred()); + User user = authenticationContext.getCurrentUser(); + feedEntryService.starEntry( + user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred()); - return Response.ok().build(); - } + return Response.ok().build(); + } - @Path("/tags") - @GET - @Transactional - @Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user") - public Response getTags() { - User user = authenticationContext.getCurrentUser(); - List tags = feedEntryTagDAO.findByUser(user); - return Response.ok(tags).build(); - } + @Path("/tags") + @GET + @Transactional + @Operation( + summary = "Get list of tags for the user", + description = "Get list of tags for the user") + public Response getTags() { + User user = authenticationContext.getCurrentUser(); + List tags = feedEntryTagDAO.findByUser(user); + return Response.ok(tags).build(); + } - @Path("/tag") - @POST - @Transactional - @Operation(summary = "Set feed entry tags") - public Response tagEntry(@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getEntryId()); + @Path("/tag") + @POST + @Transactional + @Operation(summary = "Set feed entry tags") + public Response tagEntry( + @Valid @Parameter(description = "Tag Request", required = true) TagRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getEntryId()); - User user = authenticationContext.getCurrentUser(); - feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags()); - - return Response.ok().build(); - } + User user = authenticationContext.getCurrentUser(); + feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags()); + return Response.ok().build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java index 76c3787c..8b938eae 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java @@ -1,43 +1,5 @@ package com.commafeed.frontend.resource; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriInfo; - -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.SystemUtils; -import org.apache.hc.core5.http.HttpStatus; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; -import org.jboss.resteasy.reactive.Cache; -import org.jboss.resteasy.reactive.RestForm; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConstants; import com.commafeed.backend.dao.FeedCategoryDAO; @@ -85,9 +47,43 @@ import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedOutput; import com.rometools.rome.io.WireFeedOutput; - +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; +import org.apache.hc.core5.http.HttpStatus; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.resteasy.reactive.Cache; +import org.jboss.resteasy.reactive.RestForm; @Path("/rest/feed") @RolesAllowed(Roles.USER) @@ -99,422 +95,518 @@ import lombok.extern.slf4j.Slf4j; @Tag(name = "Feeds") public class FeedREST { - private static final FeedEntry TEST_ENTRY = initTestEntry(); + private static final FeedEntry TEST_ENTRY = initTestEntry(); - private final AuthenticationContext authenticationContext; - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final FeedCategoryDAO feedCategoryDAO; - private final FeedEntryStatusDAO feedEntryStatusDAO; - private final FeedFetcher feedFetcher; - private final FeedFaviconService feedFaviconService; - private final FeedEntryService feedEntryService; - private final FeedSubscriptionService feedSubscriptionService; - private final FeedEntryFilteringService feedEntryFilteringService; - private final OPMLImporter opmlImporter; - private final OPMLExporter opmlExporter; - private final CommaFeedConfiguration config; - private final UriInfo uri; + private final AuthenticationContext authenticationContext; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + private final FeedFetcher feedFetcher; + private final FeedFaviconService feedFaviconService; + private final FeedEntryService feedEntryService; + private final FeedSubscriptionService feedSubscriptionService; + private final FeedEntryFilteringService feedEntryFilteringService; + private final OPMLImporter opmlImporter; + private final OPMLExporter opmlExporter; + private final CommaFeedConfiguration config; + private final UriInfo uri; - private static FeedEntry initTestEntry() { - FeedEntry entry = new FeedEntry(); - entry.setUrl("https://github.com/Athou/commafeed"); + private static FeedEntry initTestEntry() { + FeedEntry entry = new FeedEntry(); + entry.setUrl("https://github.com/Athou/commafeed"); - FeedEntryContent content = new FeedEntryContent(); - content.setAuthor("Athou"); - content.setTitle("Merge pull request #662 from Athou/dw8"); - content.setContent("Merge pull request #662 from Athou/dw8"); - entry.setContent(content); - return entry; - } + FeedEntryContent content = new FeedEntryContent(); + content.setAuthor("Athou"); + content.setTitle("Merge pull request #662 from Athou/dw8"); + content.setContent("Merge pull request #662 from Athou/dw8"); + entry.setContent(content); + return entry; + } - @Path("/entries") - @GET - @Transactional - @Operation(summary = "Get feed entries", description = "Get a list of feed entries") - @APIResponse( - responseCode = "200", - content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Entries.class)) }) - @APIResponse(responseCode = "404", description = "feed not found") - public Response getFeedEntries(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, - @Parameter( - description = "all entries or only unread ones", - required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType, - @Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan, - @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, - @Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, - @Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter( - description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords) { + @Path("/entries") + @GET + @Transactional + @Operation(summary = "Get feed entries", description = "Get a list of feed entries") + @APIResponse( + responseCode = "200", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Entries.class)) + }) + @APIResponse(responseCode = "404", description = "feed not found") + public Response getFeedEntries( + @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, + @Parameter(description = "all entries or only unread ones", required = true) + @DefaultValue("unread") + @QueryParam("readType") + ReadingMode readType, + @Parameter(description = "only entries newer than this") @QueryParam("newerThan") + Long newerThan, + @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") + int offset, + @Parameter(description = "limit for paging, default 20, maximum 1000") + @DefaultValue("20") + @QueryParam("limit") + int limit, + @Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") + ReadingOrder order, + @Parameter( + description = + "search for keywords in either the title or the content of the entries, separated by spaces") + @QueryParam("keywords") + String keywords) { - Preconditions.checkNotNull(id); - Preconditions.checkNotNull(readType); + Preconditions.checkNotNull(id); + Preconditions.checkNotNull(readType); - List entryKeywords = FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords)); + List entryKeywords = + FeedEntryKeyword.fromQueryString(StringUtils.trimToNull(keywords)); - limit = Math.min(limit, 1000); - limit = Math.max(0, limit); + limit = Math.min(limit, 1000); + limit = Math.max(0, limit); - Entries entries = new Entries(); - entries.setOffset(offset); - entries.setLimit(limit); + Entries entries = new Entries(); + entries.setOffset(offset); + entries.setLimit(limit); - boolean unreadOnly = readType == ReadingMode.UNREAD; + boolean unreadOnly = readType == ReadingMode.UNREAD; - Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan); + Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan); - User user = authenticationContext.getCurrentUser(); - FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id)); - if (subscription != null) { - entries.setName(subscription.getTitle()); - entries.setMessage(subscription.getFeed().getMessage()); - entries.setErrorCount(subscription.getFeed().getErrorCount()); - entries.setFeedLink(subscription.getFeed().getLink()); + User user = authenticationContext.getCurrentUser(); + FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id)); + if (subscription != null) { + entries.setName(subscription.getTitle()); + entries.setMessage(subscription.getFeed().getMessage()); + entries.setErrorCount(subscription.getFeed().getErrorCount()); + entries.setFeedLink(subscription.getFeed().getLink()); - List list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly, - entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null); + List list = + feedEntryStatusDAO.findBySubscriptions( + user, + Collections.singletonList(subscription), + unreadOnly, + entryKeywords, + newerThanDate, + offset, + limit + 1, + order, + true, + null, + null, + null); - for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); - } + for (FeedEntryStatus status : list) { + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); + } - boolean hasMore = entries.getEntries().size() > limit; - if (hasMore) { - entries.setHasMore(true); - entries.getEntries().removeLast(); - } - } else { - return Response.status(Status.NOT_FOUND).entity("feed not found").build(); - } + boolean hasMore = entries.getEntries().size() > limit; + if (hasMore) { + entries.setHasMore(true); + entries.getEntries().removeLast(); + } + } else { + return Response.status(Status.NOT_FOUND) + .entity("feed not found") + .build(); + } - entries.setTimestamp(System.currentTimeMillis()); - entries.setIgnoredReadStatus(keywords != null); - return Response.ok(entries).build(); - } + entries.setTimestamp(System.currentTimeMillis()); + entries.setIgnoredReadStatus(keywords != null); + return Response.ok(entries).build(); + } - @Path("/entriesAsFeed") - @GET - @Transactional - @Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries") - @Produces(MediaType.APPLICATION_XML) - public Response getFeedEntriesAsFeed(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, - @Parameter( - description = "all entries or only unread ones", - required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType, - @Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan, - @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, - @Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, - @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter( - description = "search for keywords in either the title or the content of the entries, separated by spaces") @QueryParam("keywords") String keywords) { + @Path("/entriesAsFeed") + @GET + @Transactional + @Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries") + @Produces(MediaType.APPLICATION_XML) + public Response getFeedEntriesAsFeed( + @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, + @Parameter(description = "all entries or only unread ones", required = true) + @DefaultValue("all") + @QueryParam("readType") + ReadingMode readType, + @Parameter(description = "only entries newer than this") @QueryParam("newerThan") + Long newerThan, + @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") + int offset, + @Parameter(description = "limit for paging, default 20, maximum 1000") + @DefaultValue("20") + @QueryParam("limit") + int limit, + @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") + ReadingOrder order, + @Parameter( + description = + "search for keywords in either the title or the content of the entries, separated by spaces") + @QueryParam("keywords") + String keywords) { - Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords); - if (response.getStatus() != Status.OK.getStatusCode()) { - return response; - } - Entries entries = (Entries) response.getEntity(); + Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords); + if (response.getStatus() != Status.OK.getStatusCode()) { + return response; + } + Entries entries = (Entries) response.getEntity(); - SyndFeed feed = new SyndFeedImpl(); - feed.setFeedType("rss_2.0"); - feed.setTitle("CommaFeed - " + entries.getName()); - feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(uri.getBaseUri().toString()); - feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList()); + SyndFeed feed = new SyndFeedImpl(); + feed.setFeedType("rss_2.0"); + feed.setTitle("CommaFeed - " + entries.getName()); + feed.setDescription("CommaFeed - " + entries.getName()); + feed.setLink(uri.getBaseUri().toString()); + feed.setEntries(entries.getEntries().stream().map(FeedUtils::asRss).toList()); - SyndFeedOutput output = new SyndFeedOutput(); - StringWriter writer = new StringWriter(); - try { - output.output(feed, writer); - } catch (Exception e) { - writer.write("Could not get feed information"); - log.error(e.getMessage(), e); - } - return Response.ok(writer.toString()).build(); - } + SyndFeedOutput output = new SyndFeedOutput(); + StringWriter writer = new StringWriter(); + try { + output.output(feed, writer); + } catch (Exception e) { + writer.write("Could not get feed information"); + log.error(e.getMessage(), e); + } + return Response.ok(writer.toString()).build(); + } - private FeedInfo fetchFeedInternal(String url) { - FeedInfo info; - url = StringUtils.trimToEmpty(url); - url = prependHttp(url); - try { - FeedFetcherResult feedFetcherResult = feedFetcher.fetch(url, true, null, null, null, null); - info = new FeedInfo(); - info.setUrl(feedFetcherResult.urlAfterRedirect()); - info.setTitle(feedFetcherResult.feed().title()); + private FeedInfo fetchFeedInternal(String url) { + FeedInfo info; + url = StringUtils.trimToEmpty(url); + url = prependHttp(url); + try { + FeedFetcherResult feedFetcherResult = + feedFetcher.fetch(url, true, null, null, null, null); + info = new FeedInfo(); + info.setUrl(feedFetcherResult.urlAfterRedirect()); + info.setTitle(feedFetcherResult.feed().title()); - } catch (Exception e) { - log.debug(e.getMessage(), e); - throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR); - } - return info; - } + } catch (Exception e) { + log.debug(e.getMessage(), e); + throw new WebApplicationException(e.getMessage(), Status.INTERNAL_SERVER_ERROR); + } + return info; + } - @POST - @Path("/fetch") - @Transactional - @Operation(summary = "Fetch a feed", description = "Fetch a feed by its url") - @APIResponse( - responseCode = "200", - content = { @Content(mediaType = "application/json", schema = @Schema(implementation = FeedInfo.class)) }) - @APIResponse(responseCode = "404", description = "feed not found") - public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getUrl()); + @POST + @Path("/fetch") + @Transactional + @Operation(summary = "Fetch a feed", description = "Fetch a feed by its url") + @APIResponse( + responseCode = "200", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = FeedInfo.class)) + }) + @APIResponse(responseCode = "404", description = "feed not found") + public Response fetchFeed( + @Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getUrl()); - FeedInfo info; - try { - info = fetchFeedInternal(req.getUrl()); - } catch (Exception e) { - Throwable cause = Throwables.getRootCause(e); - return Response.status(Status.INTERNAL_SERVER_ERROR).entity(cause.getMessage()).type(MediaType.TEXT_PLAIN).build(); - } - return Response.ok(info).build(); - } + FeedInfo info; + try { + info = fetchFeedInternal(req.getUrl()); + } catch (Exception e) { + Throwable cause = Throwables.getRootCause(e); + return Response.status(Status.INTERNAL_SERVER_ERROR) + .entity(cause.getMessage()) + .type(MediaType.TEXT_PLAIN) + .build(); + } + return Response.ok(info).build(); + } - @Path("/refreshAll") - @GET - @Transactional - @Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue") - public Response queueAllForRefresh() { - User user = authenticationContext.getCurrentUser(); - try { - feedSubscriptionService.refreshAll(user); - return Response.ok().build(); - } catch (ForceFeedRefreshTooSoonException e) { - return Response.status(HttpStatus.SC_TOO_MANY_REQUESTS).build(); - } + @Path("/refreshAll") + @GET + @Transactional + @Operation( + summary = "Queue all feeds of the user for refresh", + description = "Manually add all feeds of the user to the refresh queue") + public Response queueAllForRefresh() { + User user = authenticationContext.getCurrentUser(); + try { + feedSubscriptionService.refreshAll(user); + return Response.ok().build(); + } catch (ForceFeedRefreshTooSoonException e) { + return Response.status(HttpStatus.SC_TOO_MANY_REQUESTS).build(); + } + } - } + @Path("/mark") + @POST + @Transactional + @Operation( + summary = "Mark feed entries", + description = "Mark feed entries as read (unread is not supported)") + public Response markFeedEntries( + @Valid @Parameter(description = "Mark request", required = true) MarkRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - @Path("/mark") - @POST - @Transactional - @Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)") - public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + Instant olderThan = + req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan()); + Instant insertedBefore = + req.getInsertedBefore() == null + ? null + : Instant.ofEpochMilli(req.getInsertedBefore()); + String keywords = req.getKeywords(); + List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); - Instant olderThan = req.getOlderThan() == null ? null : Instant.ofEpochMilli(req.getOlderThan()); - Instant insertedBefore = req.getInsertedBefore() == null ? null : Instant.ofEpochMilli(req.getInsertedBefore()); - String keywords = req.getKeywords(); - List entryKeywords = FeedEntryKeyword.fromQueryString(keywords); + User user = authenticationContext.getCurrentUser(); + FeedSubscription subscription = + feedSubscriptionDAO.findById(user, Long.valueOf(req.getId())); + if (subscription != null) { + feedEntryService.markSubscriptionEntries( + user, + Collections.singletonList(subscription), + olderThan, + insertedBefore, + entryKeywords); + } + return Response.ok().build(); + } - User user = authenticationContext.getCurrentUser(); - FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId())); - if (subscription != null) { - feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, insertedBefore, - entryKeywords); - } - return Response.ok().build(); - } + @GET + @Path("/get/{id}") + @Transactional + @Operation(summary = "get feed") + @APIResponse( + responseCode = "200", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Subscription.class)) + }) + @APIResponse(responseCode = "404", description = "feed not found") + public Response getFeed( + @Parameter(description = "user id", required = true) @PathParam("id") Long id) { + Preconditions.checkNotNull(id); - @GET - @Path("/get/{id}") - @Transactional - @Operation(summary = "get feed") - @APIResponse( - responseCode = "200", - content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class)) }) - @APIResponse(responseCode = "404", description = "feed not found") - public Response getFeed(@Parameter(description = "user id", required = true) @PathParam("id") Long id) { - Preconditions.checkNotNull(id); + User user = authenticationContext.getCurrentUser(); + FeedSubscription sub = feedSubscriptionDAO.findById(user, id); + if (sub == null) { + return Response.status(Status.NOT_FOUND).build(); + } + UnreadCount unreadCount = feedSubscriptionService.getUnreadCount(user).get(id); + return Response.ok(Subscription.build(sub, unreadCount)).build(); + } - User user = authenticationContext.getCurrentUser(); - FeedSubscription sub = feedSubscriptionDAO.findById(user, id); - if (sub == null) { - return Response.status(Status.NOT_FOUND).build(); - } - UnreadCount unreadCount = feedSubscriptionService.getUnreadCount(user).get(id); - return Response.ok(Subscription.build(sub, unreadCount)).build(); - } + @GET + @Path("/favicon/{id}") + @Cache(maxAge = 2592000) + @Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon") + public Response getFeedFavicon( + @Parameter(description = "subscription id", required = true) @PathParam("id") Long id) { + Preconditions.checkNotNull(id); - @GET - @Path("/favicon/{id}") - @Cache(maxAge = 2592000) - @Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon") - public Response getFeedFavicon(@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) { - Preconditions.checkNotNull(id); + User user = authenticationContext.getCurrentUser(); + FeedSubscription subscription = feedSubscriptionDAO.findById(user, id); + if (subscription == null) { + return Response.status(Status.NOT_FOUND).build(); + } - User user = authenticationContext.getCurrentUser(); - FeedSubscription subscription = feedSubscriptionDAO.findById(user, id); - if (subscription == null) { - return Response.status(Status.NOT_FOUND).build(); - } + Feed feed = subscription.getFeed(); + if (feed.getLastUpdated() == null) { + // the feed has never been fetched yet so the iconUrl field hasn't been updated yet, and + // the + // icon can't be fetched + // this can happen for newly subscribed feeds: + // the feed has been added in the tree in the web client and the favicon is being + // fetched, + // but the feed has not been fetched yet + // the client is configured to retry in that case + return Response.status(Status.SERVICE_UNAVAILABLE) + .entity("Feed has not been fetched yet, please retry in a bit") + .type(MediaType.TEXT_PLAIN) + .build(); + } - Feed feed = subscription.getFeed(); - if (feed.getLastUpdated() == null) { - // the feed has never been fetched yet so the iconUrl field hasn't been updated yet, and the icon can't be fetched - // this can happen for newly subscribed feeds: - // the feed has been added in the tree in the web client and the favicon is being fetched, - // but the feed has not been fetched yet - // the client is configured to retry in that case - return Response.status(Status.SERVICE_UNAVAILABLE) - .entity("Feed has not been fetched yet, please retry in a bit") - .type(MediaType.TEXT_PLAIN) - .build(); - } + Favicon icon = feedFaviconService.fetchFavicon(feed); + return Response.ok(icon.icon(), icon.mediaType()).build(); + } - Favicon icon = feedFaviconService.fetchFavicon(feed); - return Response.ok(icon.icon(), icon.mediaType()).build(); - } + @POST + @Path("/subscribe") + @Transactional + @Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed") + @APIResponse( + responseCode = "200", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class)) + }) + public Response subscribe( + @Valid @Parameter(description = "subscription request", required = true) + SubscribeRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getTitle()); + Preconditions.checkNotNull(req.getUrl()); - @POST - @Path("/subscribe") - @Transactional - @Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed") - @APIResponse( - responseCode = "200", - content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Long.class)) }) - public Response subscribe(@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getTitle()); - Preconditions.checkNotNull(req.getUrl()); + try { + FeedCategory category = null; + if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) { + category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId())); + } - try { - FeedCategory category = null; - if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) { - category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId())); - } + FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl())); + User user = authenticationContext.getCurrentUser(); + long subscriptionId = + feedSubscriptionService.subscribe( + user, info.getUrl(), req.getTitle(), category, 0); + return Response.ok(subscriptionId).build(); + } catch (Exception e) { + log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e); + return Response.status(Status.SERVICE_UNAVAILABLE) + .entity("Failed to subscribe to URL " + req.getUrl() + ": " + e.getMessage()) + .build(); + } + } - FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl())); - User user = authenticationContext.getCurrentUser(); - long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category, 0); - return Response.ok(subscriptionId).build(); - } catch (Exception e) { - log.error("Failed to subscribe to URL {}: {}", req.getUrl(), e.getMessage(), e); - return Response.status(Status.SERVICE_UNAVAILABLE) - .entity("Failed to subscribe to URL " + req.getUrl() + ": " + e.getMessage()) - .build(); - } - } + @GET + @Path("/subscribe") + @Transactional + @Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed") + public Response subscribeFromUrl( + @Parameter(description = "feed url", required = true) @QueryParam("url") String url) { + try { + Preconditions.checkNotNull(url); + FeedInfo info = fetchFeedInternal(prependHttp(url)); + User user = authenticationContext.getCurrentUser(); + feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0); + } catch (Exception e) { + log.info("Could not subscribe to url {} : {}", url, e.getMessage()); + } + return Response.temporaryRedirect(uri.getBaseUri()).build(); + } - @GET - @Path("/subscribe") - @Transactional - @Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed") - public Response subscribeFromUrl(@Parameter(description = "feed url", required = true) @QueryParam("url") String url) { - try { - Preconditions.checkNotNull(url); - FeedInfo info = fetchFeedInternal(prependHttp(url)); - User user = authenticationContext.getCurrentUser(); - feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle(), null, 0); - } catch (Exception e) { - log.info("Could not subscribe to url {} : {}", url, e.getMessage()); - } - return Response.temporaryRedirect(uri.getBaseUri()).build(); - } + private String prependHttp(String url) { + if (!url.startsWith("http")) { + url = "http://" + url; + } + return url; + } - private String prependHttp(String url) { - if (!url.startsWith("http")) { - url = "http://" + url; - } - return url; - } + @POST + @Path("/unsubscribe") + @Transactional + @Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed") + public Response unsubscribe(@Parameter(required = true) IDRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - @POST - @Path("/unsubscribe") - @Transactional - @Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed") - public Response unsubscribe(@Parameter(required = true) IDRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); + boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId()); + if (deleted) { + return Response.ok().build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } - User user = authenticationContext.getCurrentUser(); - boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId()); - if (deleted) { - return Response.ok().build(); - } else { - return Response.status(Status.NOT_FOUND).build(); - } - } + @POST + @Path("/modify") + @Transactional + @Operation(summary = "Modify a subscription", description = "Modify a feed subscription") + public Response modifyFeed( + @Valid @Parameter(description = "subscription id", required = true) + FeedModificationRequest req) { + Preconditions.checkNotNull(req); + Preconditions.checkNotNull(req.getId()); - @POST - @Path("/modify") - @Transactional - @Operation(summary = "Modify a subscription", description = "Modify a feed subscription") - public Response modifyFeed(@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) { - Preconditions.checkNotNull(req); - Preconditions.checkNotNull(req.getId()); + try { + feedEntryFilteringService.filterMatchesEntry(req.getFilter(), TEST_ENTRY); + } catch (FeedEntryFilterException e) { + return Response.status(Status.BAD_REQUEST) + .entity(e.getMessage()) + .type(MediaType.TEXT_PLAIN) + .build(); + } - try { - feedEntryFilteringService.filterMatchesEntry(req.getFilter(), TEST_ENTRY); - } catch (FeedEntryFilterException e) { - return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).type(MediaType.TEXT_PLAIN).build(); - } + User user = authenticationContext.getCurrentUser(); + FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId()); - User user = authenticationContext.getCurrentUser(); - FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId()); + subscription.setFilter(req.getFilter()); + if (StringUtils.isNotBlank(subscription.getFilter())) { + // if the new filter is filled, remove the legacy filter + subscription.setFilterLegacy(null); + } - subscription.setFilter(req.getFilter()); - if (StringUtils.isNotBlank(subscription.getFilter())) { - // if the new filter is filled, remove the legacy filter - subscription.setFilterLegacy(null); - } + subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled()); + subscription.setAutoMarkAsReadAfterDays(req.getAutoMarkAsReadAfterDays()); - subscription.setPushNotificationsEnabled(req.isPushNotificationsEnabled()); - subscription.setAutoMarkAsReadAfterDays(req.getAutoMarkAsReadAfterDays()); + if (StringUtils.isNotBlank(req.getName())) { + subscription.setTitle(req.getName()); + } - if (StringUtils.isNotBlank(req.getName())) { - subscription.setTitle(req.getName()); - } + FeedCategory parent = null; + if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) { + parent = feedCategoryDAO.findById(user, Long.valueOf(req.getCategoryId())); + } + subscription.setCategory(parent); - FeedCategory parent = null; - if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) { - parent = feedCategoryDAO.findById(user, Long.valueOf(req.getCategoryId())); - } - subscription.setCategory(parent); + if (req.getPosition() != null) { + List subs = feedSubscriptionDAO.findByCategory(user, parent); + subs.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition())); - if (req.getPosition() != null) { - List subs = feedSubscriptionDAO.findByCategory(user, parent); - subs.sort((o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition())); + int existingIndex = -1; + for (int i = 0; i < subs.size(); i++) { + if (Objects.equals(subs.get(i).getId(), subscription.getId())) { + existingIndex = i; + } + } + if (existingIndex != -1) { + subs.remove(existingIndex); + } - int existingIndex = -1; - for (int i = 0; i < subs.size(); i++) { - if (Objects.equals(subs.get(i).getId(), subscription.getId())) { - existingIndex = i; - } - } - if (existingIndex != -1) { - subs.remove(existingIndex); - } + subs.add(Math.min(req.getPosition(), subs.size()), subscription); + for (int i = 0; i < subs.size(); i++) { + subs.get(i).setPosition(i); + } + } - subs.add(Math.min(req.getPosition(), subs.size()), subscription); - for (int i = 0; i < subs.size(); i++) { - subs.get(i).setPosition(i); - } - } + return Response.ok().build(); + } - return Response.ok().build(); - } + @POST + @Path("/import") + @Transactional + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Operation( + summary = "OPML import", + description = "Import an OPML file, posted as a FORM with the 'file' name") + public Response importOpml( + @Parameter(description = "ompl file", required = true) @RestForm("file") String opml) { + User user = authenticationContext.getCurrentUser(); + if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) { + return Response.status(Status.FORBIDDEN) + .entity("Import is disabled for the demo account") + .build(); + } + try { + // opml will be encoded in the default JVM encoding, bu we want UTF-8 + opmlImporter.importOpml( + user, + new String(opml.getBytes(SystemUtils.FILE_ENCODING), StandardCharsets.UTF_8)); + } catch (Exception e) { + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); + } + return Response.ok().build(); + } - @POST - @Path("/import") - @Transactional - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name") - public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) { - User user = authenticationContext.getCurrentUser(); - if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) { - return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build(); - } - try { - // opml will be encoded in the default JVM encoding, bu we want UTF-8 - opmlImporter.importOpml(user, new String(opml.getBytes(SystemUtils.FILE_ENCODING), StandardCharsets.UTF_8)); - } catch (Exception e) { - return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); - } - return Response.ok().build(); - } - - @GET - @Path("/export") - @Transactional - @Produces(MediaType.APPLICATION_XML) - @Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions") - public Response exportOpml() throws FeedException { - User user = authenticationContext.getCurrentUser(); - Opml opml = opmlExporter.export(user); - - WireFeedOutput output = new WireFeedOutput(); - String opmlString = output.outputString(opml); - return Response.ok(opmlString).build(); - } + @GET + @Path("/export") + @Transactional + @Produces(MediaType.APPLICATION_XML) + @Operation( + summary = "OPML export", + description = "Export an OPML file of the user's subscriptions") + public Response exportOpml() throws FeedException { + User user = authenticationContext.getCurrentUser(); + Opml opml = opmlExporter.export(user); + WireFeedOutput output = new WireFeedOutput(); + String opmlString = output.outputString(opml); + return Response.ok(opmlString).build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/OpenAPI.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/OpenAPI.java index 41e18678..ee1a0d14 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/OpenAPI.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/OpenAPI.java @@ -1,7 +1,6 @@ package com.commafeed.frontend.resource; import jakarta.ws.rs.core.Application; - import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType; import org.eclipse.microprofile.openapi.annotations.info.Info; @@ -10,9 +9,8 @@ import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme; import org.eclipse.microprofile.openapi.annotations.servers.Server; @OpenAPIDefinition( - info = @Info(title = "CommaFeed API", version = "1.0.0"), - servers = { @Server(description = "CommaFeed API", url = "/") }, - security = { @SecurityRequirement(name = "basicAuth") }) + info = @Info(title = "CommaFeed API", version = "1.0.0"), + servers = {@Server(description = "CommaFeed API", url = "/")}, + security = {@SecurityRequirement(name = "basicAuth")}) @SecurityScheme(securitySchemeName = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic") -public class OpenAPI extends Application { -} +public class OpenAPI extends Application {} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java index c3ec5a38..816f5a26 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java @@ -1,5 +1,13 @@ package com.commafeed.frontend.resource; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedVersion; +import com.commafeed.backend.HttpGetter; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.feed.ImageProxyUrl; +import com.commafeed.backend.service.db.DatabaseStartupService; +import com.commafeed.frontend.model.ServerInfo; +import com.commafeed.security.Roles; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; @@ -12,24 +20,12 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; - +import lombok.RequiredArgsConstructor; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.tags.Tag; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedVersion; -import com.commafeed.backend.HttpGetter; -import com.commafeed.backend.HttpGetter.HttpResult; -import com.commafeed.backend.feed.ImageProxyUrl; -import com.commafeed.backend.service.db.DatabaseStartupService; -import com.commafeed.frontend.model.ServerInfo; -import com.commafeed.security.Roles; - -import lombok.RequiredArgsConstructor; - @Path("/rest/server") - @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @RequiredArgsConstructor @@ -37,52 +33,54 @@ import lombok.RequiredArgsConstructor; @Tag(name = "Server") public class ServerREST { - private final HttpGetter httpGetter; - private final CommaFeedConfiguration config; - private final CommaFeedVersion version; - private final DatabaseStartupService databaseStartupService; + private final HttpGetter httpGetter; + private final CommaFeedConfiguration config; + private final CommaFeedVersion version; + private final DatabaseStartupService databaseStartupService; - @Path("/get") - @GET - @PermitAll - @Transactional - @Operation(summary = "Get server infos", description = "Get server infos") - public ServerInfo getServerInfos() { - ServerInfo infos = new ServerInfo(); - infos.setAnnouncement(config.announcement().orElse(null)); - infos.setVersion(version.getVersion()); - infos.setGitCommit(version.getGitCommit()); - infos.setAllowRegistrations(config.users().allowRegistrations()); - infos.setEmailAddressRequired(config.users().emailAddressRequired()); - infos.setSmtpEnabled(config.passwordRecoveryEnabled()); - infos.setDemoAccountEnabled(config.users().createDemoAccount()); - infos.setWebsocketEnabled(config.websocket().enabled()); - infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis()); - infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis()); - infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis()); - infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired()); - infos.setMinimumPasswordLength(config.users().minimumPasswordLength()); - infos.setPushNotificationsEnabled(config.pushNotifications().enabled()); - return infos; - } + @Path("/get") + @GET + @PermitAll + @Transactional + @Operation(summary = "Get server infos", description = "Get server infos") + public ServerInfo getServerInfos() { + ServerInfo infos = new ServerInfo(); + infos.setAnnouncement(config.announcement().orElse(null)); + infos.setVersion(version.getVersion()); + infos.setGitCommit(version.getGitCommit()); + infos.setAllowRegistrations(config.users().allowRegistrations()); + infos.setEmailAddressRequired(config.users().emailAddressRequired()); + infos.setSmtpEnabled(config.passwordRecoveryEnabled()); + infos.setDemoAccountEnabled(config.users().createDemoAccount()); + infos.setWebsocketEnabled(config.websocket().enabled()); + infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis()); + infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis()); + infos.setForceRefreshCooldownDuration( + config.feedRefresh().forceRefreshCooldownDuration().toMillis()); + infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired()); + infos.setMinimumPasswordLength(config.users().minimumPasswordLength()); + infos.setPushNotificationsEnabled(config.pushNotifications().enabled()); + return infos; + } - @Path("/proxy") - @GET - @RolesAllowed(Roles.USER) - @Transactional - @Operation(summary = "proxy image") - @Produces("image/png") - public Response getProxiedImage(@Parameter(description = "image url", required = true) @QueryParam("u") String url) { - if (!config.imageProxyEnabled()) { - return Response.status(Status.FORBIDDEN).build(); - } + @Path("/proxy") + @GET + @RolesAllowed(Roles.USER) + @Transactional + @Operation(summary = "proxy image") + @Produces("image/png") + public Response getProxiedImage( + @Parameter(description = "image url", required = true) @QueryParam("u") String url) { + if (!config.imageProxyEnabled()) { + return Response.status(Status.FORBIDDEN).build(); + } - url = ImageProxyUrl.decode(url); - try { - HttpResult result = httpGetter.get(url); - return Response.ok(result.content()).build(); - } catch (Exception e) { - return Response.status(Status.SERVICE_UNAVAILABLE).entity(e.getMessage()).build(); - } - } + url = ImageProxyUrl.decode(url); + try { + HttpResult result = httpGetter.get(url); + return Response.ok(result.content()).build(); + } catch (Exception e) { + return Response.status(Status.SERVICE_UNAVAILABLE).entity(e.getMessage()).build(); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java index 916e56c6..f05e26b8 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java @@ -1,36 +1,5 @@ package com.commafeed.frontend.resource; -import java.net.URISyntaxException; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import jakarta.annotation.security.PermitAll; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriInfo; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hc.core5.net.URIBuilder; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConstants; import com.commafeed.backend.Digests; @@ -67,9 +36,36 @@ import com.commafeed.frontend.model.request.RegistrationRequest; import com.commafeed.security.AuthenticationContext; import com.commafeed.security.Roles; import com.google.common.base.Preconditions; - +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; +import java.net.URISyntaxException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.core5.net.URIBuilder; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Path("/rest/user") @RolesAllowed(Roles.USER) @@ -81,371 +77,410 @@ import lombok.extern.slf4j.Slf4j; @Tag(name = "Users") public class UserREST { - private final AuthenticationContext authenticationContext; - private final UserDAO userDAO; - private final UserRoleDAO userRoleDAO; - private final UserSettingsDAO userSettingsDAO; - private final UserService userService; - private final PasswordEncryptionService encryptionService; - private final DatabaseStartupService databaseStartupService; - private final MailService mailService; - private final CommaFeedConfiguration config; - private final UriInfo uri; - private final PushNotificationService pushNotificationService; + private final AuthenticationContext authenticationContext; + private final UserDAO userDAO; + private final UserRoleDAO userRoleDAO; + private final UserSettingsDAO userSettingsDAO; + private final UserService userService; + private final PasswordEncryptionService encryptionService; + private final DatabaseStartupService databaseStartupService; + private final MailService mailService; + private final CommaFeedConfiguration config; + private final UriInfo uri; + private final PushNotificationService pushNotificationService; - @Path("/settings") - @GET - @Transactional - @Operation(summary = "Retrieve user settings", description = "Retrieve user settings") - public Settings getUserSettings() { - Settings s = new Settings(); + @Path("/settings") + @GET + @Transactional + @Operation(summary = "Retrieve user settings", description = "Retrieve user settings") + public Settings getUserSettings() { + Settings s = new Settings(); - User user = authenticationContext.getCurrentUser(); - UserSettings settings = userSettingsDAO.findByUser(user); - if (settings != null) { - s.setReadingMode(settings.getReadingMode()); - s.setReadingOrder(settings.getReadingOrder()); - s.setShowRead(settings.isShowRead()); + User user = authenticationContext.getCurrentUser(); + UserSettings settings = userSettingsDAO.findByUser(user); + if (settings != null) { + s.setReadingMode(settings.getReadingMode()); + s.setReadingOrder(settings.getReadingOrder()); + s.setShowRead(settings.isShowRead()); - s.getSharingSettings().setEmail(settings.isEmail()); - s.getSharingSettings().setGmail(settings.isGmail()); - s.getSharingSettings().setFacebook(settings.isFacebook()); - s.getSharingSettings().setTwitter(settings.isTwitter()); - s.getSharingSettings().setTumblr(settings.isTumblr()); - s.getSharingSettings().setInstapaper(settings.isInstapaper()); - s.getSharingSettings().setBuffer(settings.isBuffer()); + s.getSharingSettings().setEmail(settings.isEmail()); + s.getSharingSettings().setGmail(settings.isGmail()); + s.getSharingSettings().setFacebook(settings.isFacebook()); + s.getSharingSettings().setTwitter(settings.isTwitter()); + s.getSharingSettings().setTumblr(settings.isTumblr()); + s.getSharingSettings().setInstapaper(settings.isInstapaper()); + s.getSharingSettings().setBuffer(settings.isBuffer()); - s.setScrollMarks(settings.isScrollMarks()); - s.setCustomCss(settings.getCustomCss()); - s.setCustomJs(settings.getCustomJs()); - s.setLanguage(settings.getLanguage()); - s.setScrollSpeed(settings.getScrollSpeed()); - s.setScrollMode(settings.getScrollMode()); - s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling()); - s.setStarIconDisplayMode(settings.getStarIconDisplayMode()); - s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode()); - s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation()); - s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread()); - s.setCustomContextMenu(settings.isCustomContextMenu()); - s.setMobileFooter(settings.isMobileFooter()); - s.setUnreadCountTitle(settings.isUnreadCountTitle()); - s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); - s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); - s.setPrimaryColor(settings.getPrimaryColor()); + s.setScrollMarks(settings.isScrollMarks()); + s.setCustomCss(settings.getCustomCss()); + s.setCustomJs(settings.getCustomJs()); + s.setLanguage(settings.getLanguage()); + s.setScrollSpeed(settings.getScrollSpeed()); + s.setScrollMode(settings.getScrollMode()); + s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling()); + s.setStarIconDisplayMode(settings.getStarIconDisplayMode()); + s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode()); + s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation()); + s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread()); + s.setCustomContextMenu(settings.isCustomContextMenu()); + s.setMobileFooter(settings.isMobileFooter()); + s.setUnreadCountTitle(settings.isUnreadCountTitle()); + s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); + s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); + s.setPrimaryColor(settings.getPrimaryColor()); - if (settings.getPushNotifications() != null) { - s.getPushNotificationSettings().setType(settings.getPushNotifications().getType()); - s.getPushNotificationSettings().setServerUrl(settings.getPushNotifications().getServerUrl()); - s.getPushNotificationSettings().setUserId(settings.getPushNotifications().getUserId()); - s.getPushNotificationSettings().setUserSecret(settings.getPushNotifications().getUserSecret()); - s.getPushNotificationSettings().setTopic(settings.getPushNotifications().getTopic()); - } - } else { - s.setReadingMode(ReadingMode.UNREAD); - s.setReadingOrder(ReadingOrder.DESC); - s.setShowRead(true); + if (settings.getPushNotifications() != null) { + s.getPushNotificationSettings().setType(settings.getPushNotifications().getType()); + s.getPushNotificationSettings() + .setServerUrl(settings.getPushNotifications().getServerUrl()); + s.getPushNotificationSettings() + .setUserId(settings.getPushNotifications().getUserId()); + s.getPushNotificationSettings() + .setUserSecret(settings.getPushNotifications().getUserSecret()); + s.getPushNotificationSettings() + .setTopic(settings.getPushNotifications().getTopic()); + } + } else { + s.setReadingMode(ReadingMode.UNREAD); + s.setReadingOrder(ReadingOrder.DESC); + s.setShowRead(true); - s.getSharingSettings().setEmail(true); - s.getSharingSettings().setGmail(true); - s.getSharingSettings().setFacebook(true); - s.getSharingSettings().setTwitter(true); - s.getSharingSettings().setTumblr(true); - s.getSharingSettings().setInstapaper(true); - s.getSharingSettings().setBuffer(true); + s.getSharingSettings().setEmail(true); + s.getSharingSettings().setGmail(true); + s.getSharingSettings().setFacebook(true); + s.getSharingSettings().setTwitter(true); + s.getSharingSettings().setTumblr(true); + s.getSharingSettings().setInstapaper(true); + s.getSharingSettings().setBuffer(true); - s.setScrollMarks(true); - s.setScrollSpeed(400); - s.setScrollMode(ScrollMode.IF_NEEDED); - s.setEntriesToKeepOnTopWhenScrolling(1); - s.setStarIconDisplayMode(IconDisplayMode.ON_DESKTOP); - s.setExternalLinkIconDisplayMode(IconDisplayMode.ON_DESKTOP); - s.setMarkAllAsReadConfirmation(true); - s.setMarkAllAsReadNavigateToNextUnread(false); - s.setCustomContextMenu(true); - s.setMobileFooter(false); - s.setUnreadCountTitle(false); - s.setUnreadCountFavicon(true); - s.setDisablePullToRefresh(false); - } - return s; - } + s.setScrollMarks(true); + s.setScrollSpeed(400); + s.setScrollMode(ScrollMode.IF_NEEDED); + s.setEntriesToKeepOnTopWhenScrolling(1); + s.setStarIconDisplayMode(IconDisplayMode.ON_DESKTOP); + s.setExternalLinkIconDisplayMode(IconDisplayMode.ON_DESKTOP); + s.setMarkAllAsReadConfirmation(true); + s.setMarkAllAsReadNavigateToNextUnread(false); + s.setCustomContextMenu(true); + s.setMobileFooter(false); + s.setUnreadCountTitle(false); + s.setUnreadCountFavicon(true); + s.setDisablePullToRefresh(false); + } + return s; + } - @Path("/settings") - @POST - @Transactional - @Operation(summary = "Save user settings", description = "Save user settings") - public Response saveUserSettings(@Parameter(required = true) Settings settings) { - Preconditions.checkNotNull(settings); + @Path("/settings") + @POST + @Transactional + @Operation(summary = "Save user settings", description = "Save user settings") + public Response saveUserSettings(@Parameter(required = true) Settings settings) { + Preconditions.checkNotNull(settings); - User user = authenticationContext.getCurrentUser(); - UserSettings s = userSettingsDAO.findByUser(user); - if (s == null) { - s = new UserSettings(); - s.setUser(user); - } - s.setReadingMode(settings.getReadingMode()); - s.setReadingOrder(settings.getReadingOrder()); - s.setShowRead(settings.isShowRead()); - s.setScrollMarks(settings.isScrollMarks()); - s.setCustomCss(settings.getCustomCss()); - s.setCustomJs(CommaFeedConstants.USERNAME_DEMO.equals(user.getName()) ? "" : settings.getCustomJs()); - s.setLanguage(settings.getLanguage()); - s.setScrollSpeed(settings.getScrollSpeed()); - s.setScrollMode(settings.getScrollMode()); - s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling()); - s.setStarIconDisplayMode(settings.getStarIconDisplayMode()); - s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode()); - s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation()); - s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread()); - s.setCustomContextMenu(settings.isCustomContextMenu()); - s.setMobileFooter(settings.isMobileFooter()); - s.setUnreadCountTitle(settings.isUnreadCountTitle()); - s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); - s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); - s.setPrimaryColor(settings.getPrimaryColor()); + User user = authenticationContext.getCurrentUser(); + UserSettings s = userSettingsDAO.findByUser(user); + if (s == null) { + s = new UserSettings(); + s.setUser(user); + } + s.setReadingMode(settings.getReadingMode()); + s.setReadingOrder(settings.getReadingOrder()); + s.setShowRead(settings.isShowRead()); + s.setScrollMarks(settings.isScrollMarks()); + s.setCustomCss(settings.getCustomCss()); + s.setCustomJs( + CommaFeedConstants.USERNAME_DEMO.equals(user.getName()) + ? "" + : settings.getCustomJs()); + s.setLanguage(settings.getLanguage()); + s.setScrollSpeed(settings.getScrollSpeed()); + s.setScrollMode(settings.getScrollMode()); + s.setEntriesToKeepOnTopWhenScrolling(settings.getEntriesToKeepOnTopWhenScrolling()); + s.setStarIconDisplayMode(settings.getStarIconDisplayMode()); + s.setExternalLinkIconDisplayMode(settings.getExternalLinkIconDisplayMode()); + s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation()); + s.setMarkAllAsReadNavigateToNextUnread(settings.isMarkAllAsReadNavigateToNextUnread()); + s.setCustomContextMenu(settings.isCustomContextMenu()); + s.setMobileFooter(settings.isMobileFooter()); + s.setUnreadCountTitle(settings.isUnreadCountTitle()); + s.setUnreadCountFavicon(settings.isUnreadCountFavicon()); + s.setDisablePullToRefresh(settings.isDisablePullToRefresh()); + s.setPrimaryColor(settings.getPrimaryColor()); - PushNotificationUserSettings ps = new PushNotificationUserSettings(); - ps.setType(settings.getPushNotificationSettings().getType()); - ps.setServerUrl(settings.getPushNotificationSettings().getServerUrl()); - ps.setUserId(settings.getPushNotificationSettings().getUserId()); - ps.setUserSecret(settings.getPushNotificationSettings().getUserSecret()); - ps.setTopic(settings.getPushNotificationSettings().getTopic()); - s.setPushNotifications(ps); + PushNotificationUserSettings ps = new PushNotificationUserSettings(); + ps.setType(settings.getPushNotificationSettings().getType()); + ps.setServerUrl(settings.getPushNotificationSettings().getServerUrl()); + ps.setUserId(settings.getPushNotificationSettings().getUserId()); + ps.setUserSecret(settings.getPushNotificationSettings().getUserSecret()); + ps.setTopic(settings.getPushNotificationSettings().getTopic()); + s.setPushNotifications(ps); - s.setEmail(settings.getSharingSettings().isEmail()); - s.setGmail(settings.getSharingSettings().isGmail()); - s.setFacebook(settings.getSharingSettings().isFacebook()); - s.setTwitter(settings.getSharingSettings().isTwitter()); - s.setTumblr(settings.getSharingSettings().isTumblr()); - s.setInstapaper(settings.getSharingSettings().isInstapaper()); - s.setBuffer(settings.getSharingSettings().isBuffer()); + s.setEmail(settings.getSharingSettings().isEmail()); + s.setGmail(settings.getSharingSettings().isGmail()); + s.setFacebook(settings.getSharingSettings().isFacebook()); + s.setTwitter(settings.getSharingSettings().isTwitter()); + s.setTumblr(settings.getSharingSettings().isTumblr()); + s.setInstapaper(settings.getSharingSettings().isInstapaper()); + s.setBuffer(settings.getSharingSettings().isBuffer()); - userSettingsDAO.merge(s); - return Response.ok().build(); + userSettingsDAO.merge(s); + return Response.ok().build(); + } - } + @Path("/pushNotificationTest") + @POST + @Transactional + @Operation(summary = "Send a test push notification") + public Response sendTestPushNotification( + @Parameter(required = true) PushNotificationSettings settings) { + FeedSubscription sub = new FeedSubscription(); + sub.setTitle("CommaFeed Test Feed"); + sub.setFeed(new Feed()); - @Path("/pushNotificationTest") - @POST - @Transactional - @Operation(summary = "Send a test push notification") - public Response sendTestPushNotification(@Parameter(required = true) PushNotificationSettings settings) { - FeedSubscription sub = new FeedSubscription(); - sub.setTitle("CommaFeed Test Feed"); - sub.setFeed(new Feed()); + FeedEntryContent content = new FeedEntryContent(); + content.setTitle("Test Entry"); - FeedEntryContent content = new FeedEntryContent(); - content.setTitle("Test Entry"); + FeedEntry entry = new FeedEntry(); + entry.setContent(content); - FeedEntry entry = new FeedEntry(); - entry.setContent(content); + PushNotificationUserSettings pushSettings = new PushNotificationUserSettings(); + pushSettings.setType(settings.getType()); + pushSettings.setServerUrl(settings.getServerUrl()); + pushSettings.setUserId(settings.getUserId()); + pushSettings.setUserSecret(settings.getUserSecret()); + pushSettings.setTopic(settings.getTopic()); - PushNotificationUserSettings pushSettings = new PushNotificationUserSettings(); - pushSettings.setType(settings.getType()); - pushSettings.setServerUrl(settings.getServerUrl()); - pushSettings.setUserId(settings.getUserId()); - pushSettings.setUserSecret(settings.getUserSecret()); - pushSettings.setTopic(settings.getTopic()); + try { + pushNotificationService.notify(pushSettings, sub, entry); + } catch (Exception e) { + return Response.status(Status.INTERNAL_SERVER_ERROR) + .entity(e.getCause().getMessage()) + .type(MediaType.TEXT_PLAIN) + .build(); + } - try { - pushNotificationService.notify(pushSettings, sub, entry); - } catch (Exception e) { - return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build(); - } + return Response.ok().build(); + } - return Response.ok().build(); - } + @Path("/profile") + @GET + @Transactional + @Operation(summary = "Retrieve user's profile") + public UserModel getUserProfile() { + User user = authenticationContext.getCurrentUser(); - @Path("/profile") - @GET - @Transactional - @Operation(summary = "Retrieve user's profile") - public UserModel getUserProfile() { - User user = authenticationContext.getCurrentUser(); + UserModel userModel = new UserModel(); + userModel.setId(user.getId()); + userModel.setName(user.getName()); + userModel.setEmail(user.getEmail()); + userModel.setEnabled(!user.isDisabled()); + userModel.setApiKey(user.getApiKey()); + userModel.setLastForceRefresh(user.getLastForceRefresh()); + for (UserRole role : userRoleDAO.findAll(user)) { + if (role.getRole() == Role.ADMIN) { + userModel.setAdmin(true); + } + } + return userModel; + } - UserModel userModel = new UserModel(); - userModel.setId(user.getId()); - userModel.setName(user.getName()); - userModel.setEmail(user.getEmail()); - userModel.setEnabled(!user.isDisabled()); - userModel.setApiKey(user.getApiKey()); - userModel.setLastForceRefresh(user.getLastForceRefresh()); - for (UserRole role : userRoleDAO.findAll(user)) { - if (role.getRole() == Role.ADMIN) { - userModel.setAdmin(true); - } - } - return userModel; - } + @Path("/profile") + @POST + @Transactional + @Operation(summary = "Save user's profile") + public Response saveUserProfile( + @Valid @Parameter(required = true) ProfileModificationRequest request) { + User user = authenticationContext.getCurrentUser(); + if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) { + return Response.status(Status.FORBIDDEN) + .entity("the profile of the demo account cannot be modified") + .build(); + } - @Path("/profile") - @POST - @Transactional - @Operation(summary = "Save user's profile") - public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) { - User user = authenticationContext.getCurrentUser(); - if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) { - return Response.status(Status.FORBIDDEN).entity("the profile of the demo account cannot be modified").build(); - } + Optional login = userService.login(user.getName(), request.getCurrentPassword()); + if (login.isEmpty()) { + throw new BadRequestException("invalid password"); + } - Optional login = userService.login(user.getName(), request.getCurrentPassword()); - if (login.isEmpty()) { - throw new BadRequestException("invalid password"); - } + String email = StringUtils.trimToNull(request.getEmail()); + if (StringUtils.isNotBlank(email)) { + User u = userDAO.findByEmail(email); + if (u != null && !user.getId().equals(u.getId())) { + throw new BadRequestException("email already taken"); + } + user.setEmail(email); + } - String email = StringUtils.trimToNull(request.getEmail()); - if (StringUtils.isNotBlank(email)) { - User u = userDAO.findByEmail(email); - if (u != null && !user.getId().equals(u.getId())) { - throw new BadRequestException("email already taken"); - } - user.setEmail(email); - } + if (StringUtils.isNotBlank(request.getNewPassword())) { + byte[] password = + encryptionService.getEncryptedPassword( + request.getNewPassword(), user.getSalt()); + user.setPassword(password); + user.setApiKey(userService.generateApiKey(user)); + } - if (StringUtils.isNotBlank(request.getNewPassword())) { - byte[] password = encryptionService.getEncryptedPassword(request.getNewPassword(), user.getSalt()); - user.setPassword(password); - user.setApiKey(userService.generateApiKey(user)); - } + if (request.isNewApiKey()) { + user.setApiKey(userService.generateApiKey(user)); + } - if (request.isNewApiKey()) { - user.setApiKey(userService.generateApiKey(user)); - } + userDAO.merge(user); + return Response.ok().build(); + } - userDAO.merge(user); - return Response.ok().build(); - } + @Path("/register") + @PermitAll + @POST + @Transactional + @Operation(summary = "Register a new account") + public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) { + try { + userService.register( + req.getName(), + req.getPassword(), + req.getEmail(), + Collections.singletonList(Role.USER)); + return Response.ok().build(); + } catch (final IllegalArgumentException e) { + throw new BadRequestException(e.getMessage()); + } + } - @Path("/register") - @PermitAll - @POST - @Transactional - @Operation(summary = "Register a new account") - public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) { - try { - userService.register(req.getName(), req.getPassword(), req.getEmail(), Collections.singletonList(Role.USER)); - return Response.ok().build(); - } catch (final IllegalArgumentException e) { - throw new BadRequestException(e.getMessage()); - } - } + @Path("/initialSetup") + @PermitAll + @POST + @Transactional + @Operation( + summary = "Create the initial admin account", + description = "This endpoint is only available when no users exist in the database") + public Response initialSetup(@Valid @Parameter(required = true) InitialSetupRequest req) { + boolean initialSetupRequired = databaseStartupService.isInitialSetupRequired(); + if (!initialSetupRequired) { + return Response.status(Status.BAD_REQUEST) + .entity("Initial setup has already been completed") + .build(); + } - @Path("/initialSetup") - @PermitAll - @POST - @Transactional - @Operation( - summary = "Create the initial admin account", - description = "This endpoint is only available when no users exist in the database") - public Response initialSetup(@Valid @Parameter(required = true) InitialSetupRequest req) { - boolean initialSetupRequired = databaseStartupService.isInitialSetupRequired(); - if (!initialSetupRequired) { - return Response.status(Status.BAD_REQUEST).entity("Initial setup has already been completed").build(); - } + userService.register( + req.getName(), + req.getPassword(), + req.getEmail(), + List.of(Role.ADMIN, Role.USER), + true); - userService.register(req.getName(), req.getPassword(), req.getEmail(), List.of(Role.ADMIN, Role.USER), true); + if (config.users().createDemoAccount()) { + User demo = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO); + if (demo == null) { + userService.createDemoUser(); + } + } - if (config.users().createDemoAccount()) { - User demo = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO); - if (demo == null) { - userService.createDemoUser(); - } - } + return Response.ok().build(); + } - return Response.ok().build(); - } + @Path("/passwordReset") + @PermitAll + @POST + @Transactional + @Operation(summary = "send a password reset email") + public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) { + if (!config.passwordRecoveryEnabled()) { + throw new IllegalArgumentException( + "Password recovery is not enabled on this CommaFeed instance"); + } - @Path("/passwordReset") - @PermitAll - @POST - @Transactional - @Operation(summary = "send a password reset email") - public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) { - if (!config.passwordRecoveryEnabled()) { - throw new IllegalArgumentException("Password recovery is not enabled on this CommaFeed instance"); - } + User user = userDAO.findByEmail(req.getEmail()); + if (user == null) { + return Response.ok().build(); + } - User user = userDAO.findByEmail(req.getEmail()); - if (user == null) { - return Response.ok().build(); - } + try { + user.setRecoverPasswordToken(Digests.sha1Hex(UUID.randomUUID().toString())); + user.setRecoverPasswordTokenDate(Instant.now()); - try { - user.setRecoverPasswordToken(Digests.sha1Hex(UUID.randomUUID().toString())); - user.setRecoverPasswordTokenDate(Instant.now()); + mailService.sendMail(user, "Password recovery", buildEmailContent(user)); + return Response.ok().build(); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Status.INTERNAL_SERVER_ERROR) + .entity("could not send email") + .type(MediaType.TEXT_PLAIN) + .build(); + } + } - mailService.sendMail(user, "Password recovery", buildEmailContent(user)); - return Response.ok().build(); - } catch (Exception e) { - log.error(e.getMessage(), e); - return Response.status(Status.INTERNAL_SERVER_ERROR).entity("could not send email").type(MediaType.TEXT_PLAIN).build(); - } - } + private String buildEmailContent(User user) throws URISyntaxException { + String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString()); + return String.format( + "You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.", + user.getName(), callbackUrl(user, publicUrl)); + } - private String buildEmailContent(User user) throws URISyntaxException { - String publicUrl = Urls.removeTrailingSlash(uri.getBaseUri().toString()); - return String.format( - "You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.", - user.getName(), callbackUrl(user, publicUrl)); - } + private String callbackUrl(User user, String publicUrl) throws URISyntaxException { + URIBuilder queryBuilder = new URIBuilder(); + queryBuilder.addParameter("email", user.getEmail()); + queryBuilder.addParameter("token", user.getRecoverPasswordToken()); + String queryString = queryBuilder.build().getRawQuery(); + return publicUrl + "/#/passwordReset?" + queryString; + } - private String callbackUrl(User user, String publicUrl) throws URISyntaxException { - URIBuilder queryBuilder = new URIBuilder(); - queryBuilder.addParameter("email", user.getEmail()); - queryBuilder.addParameter("token", user.getRecoverPasswordToken()); - String queryString = queryBuilder.build().getRawQuery(); - return publicUrl + "/#/passwordReset?" + queryString; - } + @Path("/passwordResetCallback") + @PermitAll + @POST + @Transactional + @Operation(summary = "confirm password reset with new password") + public Response passwordRecoveryCallback( + @Valid @Parameter(required = true) PasswordResetConfirmationRequest req) { + String email = req.getEmail(); + String token = req.getToken(); + String password = req.getPassword(); - @Path("/passwordResetCallback") - @PermitAll - @POST - @Transactional - @Operation(summary = "confirm password reset with new password") - public Response passwordRecoveryCallback(@Valid @Parameter(required = true) PasswordResetConfirmationRequest req) { - String email = req.getEmail(); - String token = req.getToken(); - String password = req.getPassword(); + Preconditions.checkNotNull(email); + Preconditions.checkNotNull(token); + Preconditions.checkNotNull(password); - Preconditions.checkNotNull(email); - Preconditions.checkNotNull(token); - Preconditions.checkNotNull(password); + User user = userDAO.findByEmail(email); + if (user == null + || user.getRecoverPasswordToken() == null + || !user.getRecoverPasswordToken().equals(token)) { + return Response.status(Status.UNAUTHORIZED) + .entity("Email not found or invalid token.") + .build(); + } + if (ChronoUnit.MINUTES.between(user.getRecoverPasswordTokenDate(), Instant.now()) >= 30) { + return Response.status(Status.UNAUTHORIZED).entity("Token expired.").build(); + } - User user = userDAO.findByEmail(email); - if (user == null || user.getRecoverPasswordToken() == null || !user.getRecoverPasswordToken().equals(token)) { - return Response.status(Status.UNAUTHORIZED).entity("Email not found or invalid token.").build(); - } - if (ChronoUnit.MINUTES.between(user.getRecoverPasswordTokenDate(), Instant.now()) >= 30) { - return Response.status(Status.UNAUTHORIZED).entity("Token expired.").build(); - } + byte[] encryptedPassword = encryptionService.getEncryptedPassword(password, user.getSalt()); + user.setPassword(encryptedPassword); + if (StringUtils.isNotBlank(user.getApiKey())) { + user.setApiKey(userService.generateApiKey(user)); + } + user.setRecoverPasswordToken(null); + user.setRecoverPasswordTokenDate(null); - byte[] encryptedPassword = encryptionService.getEncryptedPassword(password, user.getSalt()); - user.setPassword(encryptedPassword); - if (StringUtils.isNotBlank(user.getApiKey())) { - user.setApiKey(userService.generateApiKey(user)); - } - user.setRecoverPasswordToken(null); - user.setRecoverPasswordTokenDate(null); + return Response.ok().build(); + } - return Response.ok().build(); - } + @Path("/profile/deleteAccount") + @POST + @Transactional + @Operation(summary = "Delete the user account") + public Response deleteUser() { + User user = authenticationContext.getCurrentUser(); + if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) { + return Response.status(Status.FORBIDDEN) + .entity("the demo account cannot be deleted") + .build(); + } - @Path("/profile/deleteAccount") - @POST - @Transactional - @Operation(summary = "Delete the user account") - public Response deleteUser() { - User user = authenticationContext.getCurrentUser(); - if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) { - return Response.status(Status.FORBIDDEN).entity("the demo account cannot be deleted").build(); - } + Set roles = userRoleDAO.findRoles(user); + if (roles.contains(Role.ADMIN) && userRoleDAO.countAdmins() == 1) { + return Response.status(Status.FORBIDDEN) + .entity("The last admin account cannot be deleted") + .build(); + } - Set roles = userRoleDAO.findRoles(user); - if (roles.contains(Role.ADMIN) && userRoleDAO.countAdmins() == 1) { - return Response.status(Status.FORBIDDEN).entity("The last admin account cannot be deleted").build(); - } - - userService.unregister(userDAO.findById(user.getId())); - return Response.ok().build(); - } + userService.unregister(userDAO.findById(user.getId())); + return Response.ok().build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java index 93e6a582..de4af4fb 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java @@ -1,37 +1,5 @@ package com.commafeed.frontend.resource.fever; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import jakarta.annotation.security.PermitAll; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.UriInfo; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.jboss.resteasy.reactive.server.multipart.FormValue; -import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; - import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -52,18 +20,46 @@ import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeed; import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup; import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup; import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem; - +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.UriInfo; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.jboss.resteasy.reactive.server.multipart.FormValue; +import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; /** * Fever-compatible API - * + * *

    - *
  • url: /rest/fever/user/${userId}
  • - *
  • login: username
  • - *
  • password: api key
  • + *
  • url: /rest/fever/user/${userId} + *
  • login: username + *
  • password: api key *
- * + * * See https://feedafever.com/api */ @Path("/rest/fever") @@ -73,264 +69,336 @@ import lombok.RequiredArgsConstructor; @Singleton public class FeverREST { - private static final String PATH = "/user/{userId}{optionalTrailingFever : (/fever)?}{optionalTrailingSlash : (/)?}"; - private static final int UNREAD_ITEM_IDS_BATCH_SIZE = 1000; - private static final int SAVED_ITEM_IDS_BATCH_SIZE = 1000; - private static final int ITEMS_BATCH_SIZE = 200; + private static final String PATH = + "/user/{userId}{optionalTrailingFever : (/fever)?}{optionalTrailingSlash : (/)?}"; + private static final int UNREAD_ITEM_IDS_BATCH_SIZE = 1000; + private static final int SAVED_ITEM_IDS_BATCH_SIZE = 1000; + private static final int ITEMS_BATCH_SIZE = 200; - private final UserService userService; - private final FeedEntryService feedEntryService; - private final FeedFaviconService feedFaviconService; - private final FeedEntryDAO feedEntryDAO; - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final FeedCategoryDAO feedCategoryDAO; - private final FeedEntryStatusDAO feedEntryStatusDAO; + private final UserService userService; + private final FeedEntryService feedEntryService; + private final FeedFaviconService feedFaviconService; + private final FeedEntryDAO feedEntryDAO; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; - // expected Fever API - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Path(PATH) - @POST - @Transactional - @Operation(hidden = true) - public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap form) { - Map params = new HashMap<>(); - uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); - form.forEach((k, v) -> params.put(k, v.getFirst())); - return handle(userId, params); - } + // expected Fever API + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Path(PATH) + @POST + @Transactional + @Operation(hidden = true) + public FeverResponse formUrlencoded( + @Context UriInfo uri, + @PathParam("userId") Long userId, + MultivaluedMap form) { + Map params = new HashMap<>(); + uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); + form.forEach((k, v) -> params.put(k, v.getFirst())); + return handle(userId, params); + } - // workaround for some readers that post data without any media type, and all params in the url - // e.g. FeedMe - @Path(PATH) - @POST - @Transactional - @Operation(hidden = true) - public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) { - Map params = new HashMap<>(); - uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); - return handle(userId, params); - } + // workaround for some readers that post data without any media type, and all params in the url + // e.g. FeedMe + @Path(PATH) + @POST + @Transactional + @Operation(hidden = true) + public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) { + Map params = new HashMap<>(); + uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); + return handle(userId, params); + } - // workaround for some readers that use GET instead of POST - // e.g. Unread - @Path(PATH) - @GET - @Transactional - @Operation(hidden = true) - public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) { - Map params = new HashMap<>(); - uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); - return handle(userId, params); - } + // workaround for some readers that use GET instead of POST + // e.g. Unread + @Path(PATH) + @GET + @Transactional + @Operation(hidden = true) + public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) { + Map params = new HashMap<>(); + uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); + return handle(userId, params); + } - // workaround for some readers that post data using MultiPart FormData instead of the classic POST - // e.g. Raven Reader - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Path(PATH) - @POST - @Transactional - @Operation(hidden = true) - public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) { - Map params = new HashMap<>(); - uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); - form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null))); - return handle(userId, params); - } + // workaround for some readers that post data using MultiPart FormData instead of the classic + // POST + // e.g. Raven Reader + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path(PATH) + @POST + @Transactional + @Operation(hidden = true) + public FeverResponse formData( + @Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) { + Map params = new HashMap<>(); + uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst())); + form.getValues() + .forEach( + (k, v) -> + params.put( + k, + v.stream() + .map(FormValue::getValue) + .findFirst() + .orElse(null))); + return handle(userId, params); + } - public FeverResponse handle(long userId, Map params) { - User user = auth(userId, params.get("api_key")).orElse(null); - if (user == null) { - FeverResponse resp = new FeverResponse(); - resp.setAuth(false); - return resp; - } + public FeverResponse handle(long userId, Map params) { + User user = auth(userId, params.get("api_key")).orElse(null); + if (user == null) { + FeverResponse resp = new FeverResponse(); + resp.setAuth(false); + return resp; + } - FeverResponse resp = new FeverResponse(); - resp.setAuth(true); + FeverResponse resp = new FeverResponse(); + resp.setAuth(true); - List subscriptions = feedSubscriptionDAO.findAll(user); - resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions)); + List subscriptions = feedSubscriptionDAO.findAll(user); + resp.setLastRefreshedOnTime(buildLastRefreshedOnTime(subscriptions)); - if (params.containsKey("groups") || params.containsKey("feeds")) { - resp.setFeedsGroups(buildFeedsGroups(subscriptions)); + if (params.containsKey("groups") || params.containsKey("feeds")) { + resp.setFeedsGroups(buildFeedsGroups(subscriptions)); - if (params.containsKey("groups")) { - List categories = feedCategoryDAO.findAll(user); - resp.setGroups(buildGroups(categories)); - } + if (params.containsKey("groups")) { + List categories = feedCategoryDAO.findAll(user); + resp.setGroups(buildGroups(categories)); + } - if (params.containsKey("feeds")) { - resp.setFeeds(buildFeeds(subscriptions)); - } - } + if (params.containsKey("feeds")) { + resp.setFeeds(buildFeeds(subscriptions)); + } + } - if (params.containsKey("unread_item_ids")) { - resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions)); - } + if (params.containsKey("unread_item_ids")) { + resp.setUnreadItemIds(buildUnreadItemIds(user, subscriptions)); + } - if (params.containsKey("saved_item_ids")) { - resp.setSavedItemIds(buildSavedItemIds(user)); - } + if (params.containsKey("saved_item_ids")) { + resp.setSavedItemIds(buildSavedItemIds(user)); + } - if (params.containsKey("items")) { - if (params.containsKey("with_ids")) { - String withIds = params.get("with_ids"); - List entryIds = Stream.of(withIds.split(",")).map(String::trim).toList(); - resp.setItems(buildItems(user, subscriptions, entryIds)); - } else { - Long sinceId = Optional.ofNullable(params.get("since_id")).filter(StringUtils::isNotBlank).map(Long::valueOf).orElse(null); - Long maxId = Optional.ofNullable(params.get("max_id")).filter(StringUtils::isNotBlank).map(Long::valueOf).orElse(null); - resp.setItems(buildItems(user, subscriptions, sinceId, maxId)); - } - } + if (params.containsKey("items")) { + if (params.containsKey("with_ids")) { + String withIds = params.get("with_ids"); + List entryIds = Stream.of(withIds.split(",")).map(String::trim).toList(); + resp.setItems(buildItems(user, subscriptions, entryIds)); + } else { + Long sinceId = + Optional.ofNullable(params.get("since_id")) + .filter(StringUtils::isNotBlank) + .map(Long::valueOf) + .orElse(null); + Long maxId = + Optional.ofNullable(params.get("max_id")) + .filter(StringUtils::isNotBlank) + .map(Long::valueOf) + .orElse(null); + resp.setItems(buildItems(user, subscriptions, sinceId, maxId)); + } + } - if (params.containsKey("favicons")) { - resp.setFavicons(buildFavicons(subscriptions)); - } + if (params.containsKey("favicons")) { + resp.setFavicons(buildFavicons(subscriptions)); + } - if (params.containsKey("links")) { - resp.setLinks(Collections.emptyList()); - } + if (params.containsKey("links")) { + resp.setLinks(Collections.emptyList()); + } - if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) { - long id = Long.parseLong(params.get("id")); - String before = params.get("before"); - Instant insertedBefore = before == null ? null : Instant.ofEpochSecond(Long.parseLong(before)); - mark(user, params.get("mark"), id, params.get("as"), insertedBefore); - } + if (params.containsKey("mark") && params.containsKey("id") && params.containsKey("as")) { + long id = Long.parseLong(params.get("id")); + String before = params.get("before"); + Instant insertedBefore = + before == null ? null : Instant.ofEpochSecond(Long.parseLong(before)); + mark(user, params.get("mark"), id, params.get("as"), insertedBefore); + } - return resp; - } + return resp; + } - private Optional auth(Long userId, String feverApiKey) { - return userService.login(userId, feverApiKey); - } + private Optional auth(Long userId, String feverApiKey) { + return userService.login(userId, feverApiKey); + } - private long buildLastRefreshedOnTime(List subscriptions) { - return subscriptions.stream() - .map(FeedSubscription::getFeed) - .map(Feed::getLastUpdated) - .filter(Objects::nonNull) - .max(Comparator.naturalOrder()) - .map(Instant::getEpochSecond) - .orElse(0L); - } + private long buildLastRefreshedOnTime(List subscriptions) { + return subscriptions.stream() + .map(FeedSubscription::getFeed) + .map(Feed::getLastUpdated) + .filter(Objects::nonNull) + .max(Comparator.naturalOrder()) + .map(Instant::getEpochSecond) + .orElse(0L); + } - private List buildFeedsGroups(List subscriptions) { - return subscriptions.stream() - .collect(Collectors.groupingBy(s -> s.getCategory() == null ? 0 : s.getCategory().getId())) - .entrySet() - .stream() - .map(e -> { - FeverFeedGroup fg = new FeverFeedGroup(); - fg.setGroupId(e.getKey()); - fg.setFeedIds(e.getValue().stream().map(FeedSubscription::getId).toList()); - return fg; - }) - .toList(); - } + private List buildFeedsGroups(List subscriptions) { + return subscriptions.stream() + .collect( + Collectors.groupingBy( + s -> s.getCategory() == null ? 0 : s.getCategory().getId())) + .entrySet() + .stream() + .map( + e -> { + FeverFeedGroup fg = new FeverFeedGroup(); + fg.setGroupId(e.getKey()); + fg.setFeedIds( + e.getValue().stream().map(FeedSubscription::getId).toList()); + return fg; + }) + .toList(); + } - private List buildGroups(List categories) { - return categories.stream().map(c -> { - FeverGroup g = new FeverGroup(); - g.setId(c.getId()); - g.setTitle(c.getName()); - return g; - }).toList(); - } + private List buildGroups(List categories) { + return categories.stream() + .map( + c -> { + FeverGroup g = new FeverGroup(); + g.setId(c.getId()); + g.setTitle(c.getName()); + return g; + }) + .toList(); + } - private List buildFeeds(List subscriptions) { - return subscriptions.stream().map(s -> { - FeverFeed f = new FeverFeed(); - f.setId(s.getId()); - f.setFaviconId(s.getId()); - f.setTitle(s.getTitle()); - f.setUrl(s.getFeed().getUrl()); - f.setSiteUrl(s.getFeed().getLink()); - f.setSpark(false); - f.setLastUpdatedOnTime(s.getFeed().getLastUpdated() == null ? 0 : s.getFeed().getLastUpdated().getEpochSecond()); - return f; - }).toList(); - } + private List buildFeeds(List subscriptions) { + return subscriptions.stream() + .map( + s -> { + FeverFeed f = new FeverFeed(); + f.setId(s.getId()); + f.setFaviconId(s.getId()); + f.setTitle(s.getTitle()); + f.setUrl(s.getFeed().getUrl()); + f.setSiteUrl(s.getFeed().getLink()); + f.setSpark(false); + f.setLastUpdatedOnTime( + s.getFeed().getLastUpdated() == null + ? 0 + : s.getFeed().getLastUpdated().getEpochSecond()); + return f; + }) + .toList(); + } - private List buildUnreadItemIds(User user, List subscriptions) { - List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, - UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.DESC, false, null, null, null); - return statuses.stream().map(s -> s.getEntry().getId()).toList(); - } + private List buildUnreadItemIds(User user, List subscriptions) { + List statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subscriptions, + true, + null, + null, + 0, + UNREAD_ITEM_IDS_BATCH_SIZE, + ReadingOrder.DESC, + false, + null, + null, + null); + return statuses.stream().map(s -> s.getEntry().getId()).toList(); + } - private List buildSavedItemIds(User user) { - List statuses = feedEntryStatusDAO.findStarred(user, null, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.DESC, - false); - return statuses.stream().map(s -> s.getEntry().getId()).toList(); - } + private List buildSavedItemIds(User user) { + List statuses = + feedEntryStatusDAO.findStarred( + user, null, null, 0, SAVED_ITEM_IDS_BATCH_SIZE, ReadingOrder.DESC, false); + return statuses.stream().map(s -> s.getEntry().getId()).toList(); + } - private List buildItems(User user, List subscriptions, List entryIds) { - List items = new ArrayList<>(); + private List buildItems( + User user, List subscriptions, List entryIds) { + List items = new ArrayList<>(); - Map subscriptionsByFeedId = subscriptions.stream() - .collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s)); - for (String entryId : entryIds) { - FeedEntry entry = feedEntryDAO.findById(Long.parseLong(entryId)); - FeedSubscription sub = subscriptionsByFeedId.get(entry.getFeed().getId()); - if (sub != null) { - FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); - items.add(mapStatus(status)); - } - } + Map subscriptionsByFeedId = + subscriptions.stream().collect(Collectors.toMap(s -> s.getFeed().getId(), s -> s)); + for (String entryId : entryIds) { + FeedEntry entry = feedEntryDAO.findById(Long.parseLong(entryId)); + FeedSubscription sub = subscriptionsByFeedId.get(entry.getFeed().getId()); + if (sub != null) { + FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry); + items.add(mapStatus(status)); + } + } - return items; - } + return items; + } - private List buildItems(User user, List subscriptions, Long sinceId, Long maxId) { - List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE, - ReadingOrder.DESC, false, null, sinceId, maxId); - return statuses.stream().map(this::mapStatus).toList(); - } + private List buildItems( + User user, List subscriptions, Long sinceId, Long maxId) { + List statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subscriptions, + false, + null, + null, + 0, + ITEMS_BATCH_SIZE, + ReadingOrder.DESC, + false, + null, + sinceId, + maxId); + return statuses.stream().map(this::mapStatus).toList(); + } - private FeverItem mapStatus(FeedEntryStatus s) { - FeverItem i = new FeverItem(); - i.setId(s.getEntry().getId()); - i.setFeedId(s.getSubscription().getId()); - i.setTitle(s.getEntry().getContent().getTitle()); - i.setAuthor(s.getEntry().getContent().getAuthor()); - i.setHtml(Optional.ofNullable(s.getEntry().getContent().getContent()).orElse("")); - i.setUrl(s.getEntry().getUrl()); - i.setSaved(s.isStarred()); - i.setRead(s.isRead()); - i.setCreatedOnTime(s.getEntryPublished().getEpochSecond()); - return i; - } + private FeverItem mapStatus(FeedEntryStatus s) { + FeverItem i = new FeverItem(); + i.setId(s.getEntry().getId()); + i.setFeedId(s.getSubscription().getId()); + i.setTitle(s.getEntry().getContent().getTitle()); + i.setAuthor(s.getEntry().getContent().getAuthor()); + i.setHtml(Optional.ofNullable(s.getEntry().getContent().getContent()).orElse("")); + i.setUrl(s.getEntry().getUrl()); + i.setSaved(s.isStarred()); + i.setRead(s.isRead()); + i.setCreatedOnTime(s.getEntryPublished().getEpochSecond()); + return i; + } - private List buildFavicons(List subscriptions) { - return subscriptions.stream().map(s -> { - Favicon favicon = feedFaviconService.fetchFavicon(s.getFeed()); + private List buildFavicons(List subscriptions) { + return subscriptions.stream() + .map( + s -> { + Favicon favicon = feedFaviconService.fetchFavicon(s.getFeed()); - FeverFavicon f = new FeverFavicon(); - f.setId(s.getFeed().getId()); - f.setData(String.format("data:%s;base64,%s", favicon.mediaType(), Base64.getEncoder().encodeToString(favicon.icon()))); - return f; - }).toList(); - } - - private void mark(User user, String source, long id, String action, Instant insertedBefore) { - if ("item".equals(source)) { - if ("read".equals(action) || "unread".equals(action)) { - feedEntryService.markEntry(user, id, "read".equals(action)); - } else if ("saved".equals(action) || "unsaved".equals(action)) { - FeedEntry entry = feedEntryDAO.findById(id); - FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed()); - feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action)); - } - } else if ("feed".equals(source)) { - FeedSubscription subscription = feedSubscriptionDAO.findById(user, id); - feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), null, insertedBefore, null); - } else if ("group".equals(source)) { - FeedCategory parent = feedCategoryDAO.findById(user, id); - List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); - List subscriptions = feedSubscriptionDAO.findByCategories(user, categories); - feedEntryService.markSubscriptionEntries(user, subscriptions, null, insertedBefore, null); - } - } + FeverFavicon f = new FeverFavicon(); + f.setId(s.getFeed().getId()); + f.setData( + String.format( + "data:%s;base64,%s", + favicon.mediaType(), + Base64.getEncoder().encodeToString(favicon.icon()))); + return f; + }) + .toList(); + } + private void mark(User user, String source, long id, String action, Instant insertedBefore) { + if ("item".equals(source)) { + if ("read".equals(action) || "unread".equals(action)) { + feedEntryService.markEntry(user, id, "read".equals(action)); + } else if ("saved".equals(action) || "unsaved".equals(action)) { + FeedEntry entry = feedEntryDAO.findById(id); + FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed()); + feedEntryService.starEntry(user, id, sub.getId(), "saved".equals(action)); + } + } else if ("feed".equals(source)) { + FeedSubscription subscription = feedSubscriptionDAO.findById(user, id); + feedEntryService.markSubscriptionEntries( + user, Collections.singletonList(subscription), null, insertedBefore, null); + } else if ("group".equals(source)) { + FeedCategory parent = feedCategoryDAO.findById(user, id); + List categories = feedCategoryDAO.findAllChildrenCategories(user, parent); + List subscriptions = + feedSubscriptionDAO.findByCategories(user, categories); + feedEntryService.markSubscriptionEntries( + user, subscriptions, null, insertedBefore, null); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java index 46290715..78c571ab 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverResponse.java @@ -1,10 +1,5 @@ package com.commafeed.frontend.resource.fever; -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat.Shape; import com.fasterxml.jackson.annotation.JsonInclude; @@ -18,161 +13,168 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; - +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.Data; @JsonInclude(Include.NON_NULL) @Data public class FeverResponse { - @JsonProperty("api_version") - private int apiVersion = 3; + @JsonProperty("api_version") + private int apiVersion = 3; - @JsonProperty("auth") - @JsonFormat(shape = Shape.NUMBER) - private boolean auth; + @JsonProperty("auth") + @JsonFormat(shape = Shape.NUMBER) + private boolean auth; - @JsonProperty("last_refreshed_on_time") - private Long lastRefreshedOnTime; + @JsonProperty("last_refreshed_on_time") + private Long lastRefreshedOnTime; - @JsonProperty("groups") - private List groups; + @JsonProperty("groups") + private List groups; - @JsonProperty("feeds") - private List feeds; + @JsonProperty("feeds") + private List feeds; - @JsonProperty("feeds_groups") - private List feedsGroups; + @JsonProperty("feeds_groups") + private List feedsGroups; - @JsonProperty("unread_item_ids") - @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) - @JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class) - private List unreadItemIds; + @JsonProperty("unread_item_ids") + @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) + @JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class) + private List unreadItemIds; - @JsonProperty("saved_item_ids") - @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) - @JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class) - private List savedItemIds; + @JsonProperty("saved_item_ids") + @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) + @JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class) + private List savedItemIds; - @JsonProperty("items") - private List items; + @JsonProperty("items") + private List items; - @JsonProperty("favicons") - private List favicons; + @JsonProperty("favicons") + private List favicons; - @JsonProperty("links") - private List links; + @JsonProperty("links") + private List links; - @Data - public static class FeverGroup { + @Data + public static class FeverGroup { - @JsonProperty("id") - private long id; + @JsonProperty("id") + private long id; - @JsonProperty("title") - private String title; - } + @JsonProperty("title") + private String title; + } - @Data - public static class FeverFeed { + @Data + public static class FeverFeed { - @JsonProperty("id") - private long id; + @JsonProperty("id") + private long id; - @JsonProperty("favicon_id") - private long faviconId; + @JsonProperty("favicon_id") + private long faviconId; - @JsonProperty("title") - private String title; + @JsonProperty("title") + private String title; - @JsonProperty("url") - private String url; + @JsonProperty("url") + private String url; - @JsonProperty("site_url") - private String siteUrl; + @JsonProperty("site_url") + private String siteUrl; - @JsonProperty("is_spark") - @JsonFormat(shape = Shape.NUMBER) - private boolean spark; + @JsonProperty("is_spark") + @JsonFormat(shape = Shape.NUMBER) + private boolean spark; - @JsonProperty("last_updated_on_time") - private long lastUpdatedOnTime; - } + @JsonProperty("last_updated_on_time") + private long lastUpdatedOnTime; + } - @Data - public static class FeverFeedGroup { + @Data + public static class FeverFeedGroup { - @JsonProperty("group_id") - private long groupId; + @JsonProperty("group_id") + private long groupId; - @JsonProperty("feed_ids") - @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) - @JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class) - private List feedIds; - } + @JsonProperty("feed_ids") + @JsonSerialize(using = LongListToCommaSeparatedStringSerializer.class) + @JsonDeserialize(using = CommaSeparatedStringToLongListDeserializer.class) + private List feedIds; + } - @Data - public static class FeverItem { + @Data + public static class FeverItem { - @JsonProperty("id") - private long id; + @JsonProperty("id") + private long id; - @JsonProperty("feed_id") - private long feedId; + @JsonProperty("feed_id") + private long feedId; - @JsonProperty("title") - private String title; + @JsonProperty("title") + private String title; - @JsonProperty("author") - private String author; + @JsonProperty("author") + private String author; - @JsonProperty("html") - private String html; + @JsonProperty("html") + private String html; - @JsonProperty("url") - private String url; + @JsonProperty("url") + private String url; - @JsonProperty("is_saved") - @JsonFormat(shape = Shape.NUMBER) - private boolean saved; + @JsonProperty("is_saved") + @JsonFormat(shape = Shape.NUMBER) + private boolean saved; - @JsonProperty("is_read") - @JsonFormat(shape = Shape.NUMBER) - private boolean read; + @JsonProperty("is_read") + @JsonFormat(shape = Shape.NUMBER) + private boolean read; - @JsonProperty("created_on_time") - private long createdOnTime; + @JsonProperty("created_on_time") + private long createdOnTime; + } - } + @Data + public static class FeverFavicon { - @Data - public static class FeverFavicon { + @JsonProperty("id") + private long id; - @JsonProperty("id") - private long id; + @JsonProperty("data") + private String data; + } - @JsonProperty("data") - private String data; - } + @Data + public static class FeverLink {} - @Data - public static class FeverLink { + public static class LongListToCommaSeparatedStringSerializer + extends JsonSerializer> { + @Override + public void serialize( + List input, + JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) + throws IOException { + String output = input.stream().map(String::valueOf).collect(Collectors.joining(",")); + jsonGenerator.writeObject(output); + } + } - } - - public static class LongListToCommaSeparatedStringSerializer extends JsonSerializer> { - @Override - public void serialize(List input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { - String output = input.stream().map(String::valueOf).collect(Collectors.joining(",")); - jsonGenerator.writeObject(output); - } - } - - public static class CommaSeparatedStringToLongListDeserializer extends JsonDeserializer> { - @Override - public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - String value = ctxt.readValue(p, String.class); - return Stream.of(value.split(",")).map(Long::valueOf).toList(); - } - } + public static class CommaSeparatedStringToLongListDeserializer + extends JsonDeserializer> { + @Override + public List deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + String value = ctxt.readValue(p, String.class); + return Stream.of(value.split(",")).map(Long::valueOf).toList(); + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java index 8d62408b..01431de4 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java @@ -1,19 +1,16 @@ package com.commafeed.frontend.servlet; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; - -import org.eclipse.microprofile.openapi.annotations.Operation; - import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; import com.commafeed.security.AuthenticationContext; - +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; import lombok.RequiredArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.Operation; @Path("/custom_css.css") @Produces("text/css") @@ -21,24 +18,23 @@ import lombok.RequiredArgsConstructor; @Singleton public class CustomCssServlet { - private final AuthenticationContext authenticationContext; - private final UserSettingsDAO userSettingsDAO; + private final AuthenticationContext authenticationContext; + private final UserSettingsDAO userSettingsDAO; - @GET - @Transactional - @Operation(hidden = true) - public String get() { - User user = authenticationContext.getCurrentUser(); - if (user == null) { - return ""; - } + @GET + @Transactional + @Operation(hidden = true) + public String get() { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return ""; + } - UserSettings settings = userSettingsDAO.findByUser(user); - if (settings == null) { - return ""; - } - - return settings.getCustomCss(); - } + UserSettings settings = userSettingsDAO.findByUser(user); + if (settings == null) { + return ""; + } + return settings.getCustomCss(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java index 6c76cf3a..6c21823f 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java @@ -1,19 +1,16 @@ package com.commafeed.frontend.servlet; -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; - -import org.eclipse.microprofile.openapi.annotations.Operation; - import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; import com.commafeed.security.AuthenticationContext; - +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; import lombok.RequiredArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.Operation; @Path("/custom_js.js") @Produces("application/javascript") @@ -21,24 +18,23 @@ import lombok.RequiredArgsConstructor; @Singleton public class CustomJsServlet { - private final AuthenticationContext authenticationContext; - private final UserSettingsDAO userSettingsDAO; + private final AuthenticationContext authenticationContext; + private final UserSettingsDAO userSettingsDAO; - @GET - @Transactional - @Operation(hidden = true) - public String get() { - User user = authenticationContext.getCurrentUser(); - if (user == null) { - return ""; - } + @GET + @Transactional + @Operation(hidden = true) + public String get() { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return ""; + } - UserSettings settings = userSettingsDAO.findByUser(user); - if (settings == null) { - return ""; - } - - return settings.getCustomJs(); - } + UserSettings settings = userSettingsDAO.findByUser(user); + if (settings == null) { + return ""; + } + return settings.getCustomJs(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java index a6167cd7..b15a3857 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java @@ -1,5 +1,6 @@ package com.commafeed.frontend.servlet; +import com.commafeed.security.CookieService; import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; import jakarta.ws.rs.GET; @@ -7,12 +8,8 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; - -import org.eclipse.microprofile.openapi.annotations.Operation; - -import com.commafeed.security.CookieService; - import lombok.RequiredArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.Operation; @RequiredArgsConstructor @Path("/logout") @@ -20,13 +17,13 @@ import lombok.RequiredArgsConstructor; @Singleton public class LogoutServlet { - private final UriInfo uri; - private final CookieService cookieService; + private final UriInfo uri; + private final CookieService cookieService; - @GET - @Operation(hidden = true) - public Response get() { - NewCookie removeCookie = cookieService.buildLogoutCookie(); - return Response.temporaryRedirect(uri.getBaseUri()).cookie(removeCookie).build(); - } + @GET + @Operation(hidden = true) + public Response get() { + NewCookie removeCookie = cookieService.buildLogoutCookie(); + return Response.temporaryRedirect(uri.getBaseUri()).cookie(removeCookie).build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java index 816aad69..c6ac5707 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java @@ -1,20 +1,5 @@ package com.commafeed.frontend.servlet; -import java.net.URI; -import java.util.List; - -import jakarta.inject.Singleton; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.microprofile.openapi.annotations.Operation; - import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -26,51 +11,79 @@ import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.security.AuthenticationContext; - +import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import java.net.URI; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.Operation; @Path("/next") @RequiredArgsConstructor @Singleton public class NextUnreadServlet { - private final FeedSubscriptionDAO feedSubscriptionDAO; - private final FeedEntryStatusDAO feedEntryStatusDAO; - private final FeedCategoryDAO feedCategoryDAO; - private final FeedEntryService feedEntryService; - private final AuthenticationContext authenticationContext; - private final UriInfo uri; + private final FeedSubscriptionDAO feedSubscriptionDAO; + private final FeedEntryStatusDAO feedEntryStatusDAO; + private final FeedCategoryDAO feedCategoryDAO; + private final FeedEntryService feedEntryService; + private final AuthenticationContext authenticationContext; + private final UriInfo uri; - @GET - @Transactional - @Operation(hidden = true) - public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { - User user = authenticationContext.getCurrentUser(); - if (user == null) { - return Response.temporaryRedirect(uri.getBaseUri()).build(); - } + @GET + @Transactional + @Operation(hidden = true) + public Response get( + @QueryParam("category") String categoryId, + @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return Response.temporaryRedirect(uri.getBaseUri()).build(); + } - FeedEntryStatus s = null; - if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { - List subs = feedSubscriptionDAO.findAll(user); - List statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, null, - null, null); - s = statuses.stream().findFirst().orElse(null); - } else { - FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId)); - if (category != null) { - List children = feedCategoryDAO.findAllChildrenCategories(user, category); - List subscriptions = feedSubscriptionDAO.findByCategories(user, children); - List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order, - true, null, null, null); - s = statuses.stream().findFirst().orElse(null); - } - } - if (s != null) { - feedEntryService.markEntry(user, s.getEntry().getId(), true); - } + FeedEntryStatus s = null; + if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { + List subs = feedSubscriptionDAO.findAll(user); + List statuses = + feedEntryStatusDAO.findBySubscriptions( + user, subs, true, null, null, 0, 1, order, true, null, null, null); + s = statuses.stream().findFirst().orElse(null); + } else { + FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId)); + if (category != null) { + List children = + feedCategoryDAO.findAllChildrenCategories(user, category); + List subscriptions = + feedSubscriptionDAO.findByCategories(user, children); + List statuses = + feedEntryStatusDAO.findBySubscriptions( + user, + subscriptions, + true, + null, + null, + 0, + 1, + order, + true, + null, + null, + null); + s = statuses.stream().findFirst().orElse(null); + } + } + if (s != null) { + feedEntryService.markEntry(user, s.getEntry().getId(), true); + } - String url = s == null ? uri.getBaseUri().toString() : s.getEntry().getUrl(); - return Response.temporaryRedirect(URI.create(url)).build(); - } + String url = s == null ? uri.getBaseUri().toString() : s.getEntry().getUrl(); + return Response.temporaryRedirect(URI.create(url)).build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java index 43cf286a..a506c049 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java @@ -1,5 +1,6 @@ package com.commafeed.frontend.servlet; +import com.commafeed.CommaFeedConfiguration; import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; import jakarta.ws.rs.GET; @@ -7,14 +8,10 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; - +import lombok.RequiredArgsConstructor; import org.apache.hc.core5.http.HttpStatus; import org.eclipse.microprofile.openapi.annotations.Operation; -import com.commafeed.CommaFeedConfiguration; - -import lombok.RequiredArgsConstructor; - @Path("/robots.txt") @PermitAll @Produces(MediaType.TEXT_PLAIN) @@ -22,15 +19,15 @@ import lombok.RequiredArgsConstructor; @Singleton public class RobotsTxtDisallowAllServlet { - private final CommaFeedConfiguration config; + private final CommaFeedConfiguration config; - @GET - @Operation(hidden = true) - public Response get() { - if (config.hideFromWebCrawlers()) { - return Response.ok("User-agent: *\nDisallow: /").build(); - } else { - return Response.status(HttpStatus.SC_NOT_FOUND).build(); - } - } -} \ No newline at end of file + @GET + @Operation(hidden = true) + public Response get() { + if (config.hideFromWebCrawlers()) { + return Response.ok("User-agent: *\nDisallow: /").build(); + } else { + return Response.status(HttpStatus.SC_NOT_FOUND).build(); + } + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java index bb1703c0..73f9450a 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java @@ -1,7 +1,8 @@ package com.commafeed.frontend.ws; -import java.io.IOException; - +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.model.User; +import com.commafeed.security.AuthenticationContext; import jakarta.inject.Singleton; import jakarta.websocket.CloseReason; import jakarta.websocket.CloseReason.CloseCodes; @@ -10,11 +11,7 @@ import jakarta.websocket.OnMessage; import jakarta.websocket.OnOpen; import jakarta.websocket.Session; import jakarta.websocket.server.ServerEndpoint; - -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.model.User; -import com.commafeed.security.AuthenticationContext; - +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,37 +21,36 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class WebSocketEndpoint { - private final AuthenticationContext authenticationContext; - private final CommaFeedConfiguration config; - private final WebSocketSessions sessions; + private final AuthenticationContext authenticationContext; + private final CommaFeedConfiguration config; + private final WebSocketSessions sessions; - @OnOpen - public void onOpen(Session session) throws IOException { - User user = authenticationContext.getCurrentUser(); - if (user == null) { - reject(session); - return; - } + @OnOpen + public void onOpen(Session session) throws IOException { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + reject(session); + return; + } - log.debug("created websocket session for user '{}'", user.getName()); - sessions.add(user.getId(), session); - session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000); - } + log.debug("created websocket session for user '{}'", user.getName()); + sessions.add(user.getId(), session); + session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000); + } - @OnMessage - public void onMessage(String message, Session session) { - if ("ping".equals(message)) { - session.getAsyncRemote().sendText("pong"); - } - } + @OnMessage + public void onMessage(String message, Session session) { + if ("ping".equals(message)) { + session.getAsyncRemote().sendText("pong"); + } + } - @OnClose - public void onClose(Session session) { - sessions.remove(session); - } - - private void reject(Session session) throws IOException { - session.close(new CloseReason(CloseCodes.VIOLATED_POLICY, "unauthorized")); - } + @OnClose + public void onClose(Session session) { + sessions.remove(session); + } + private void reject(Session session) throws IOException { + session.close(new CloseReason(CloseCodes.VIOLATED_POLICY, "unauthorized")); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketMessageBuilder.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketMessageBuilder.java index 8a7cbcdb..84b72025 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketMessageBuilder.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketMessageBuilder.java @@ -1,14 +1,12 @@ package com.commafeed.frontend.ws; import com.commafeed.backend.model.FeedSubscription; - import lombok.experimental.UtilityClass; @UtilityClass public class WebSocketMessageBuilder { - public static String newFeedEntries(FeedSubscription subscription, long count) { - return String.format("%s:%s:%s", "new-feed-entries", subscription.getId(), count); - } - + public static String newFeedEntries(FeedSubscription subscription, long count) { + return String.format("%s:%s:%s", "new-feed-entries", subscription.getId(), count); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java index 0a7bdf14..60aa389c 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java +++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java @@ -1,49 +1,52 @@ package com.commafeed.frontend.ws; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.inject.Singleton; -import jakarta.websocket.Session; - import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; import com.commafeed.backend.model.User; - +import jakarta.inject.Singleton; +import jakarta.websocket.Session; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j public class WebSocketSessions { - // a user may have multiple sessions (two tabs, two devices, ...) - private final Map> sessions = new ConcurrentHashMap<>(); + // a user may have multiple sessions (two tabs, two devices, ...) + private final Map> sessions = new ConcurrentHashMap<>(); - public WebSocketSessions(MetricRegistry metrics) { - metrics.register(MetricRegistry.name(getClass(), "users"), - (Gauge) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count()); - metrics.register(MetricRegistry.name(getClass(), "sessions"), - (Gauge) () -> sessions.values().stream().mapToLong(Set::size).sum()); - } + public WebSocketSessions(MetricRegistry metrics) { + metrics.register( + MetricRegistry.name(getClass(), "users"), + (Gauge) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count()); + metrics.register( + MetricRegistry.name(getClass(), "sessions"), + (Gauge) () -> sessions.values().stream().mapToLong(Set::size).sum()); + } - public void add(Long userId, Session session) { - sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session); - } + public void add(Long userId, Session session) { + sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session); + } - public void remove(Session session) { - sessions.values().forEach(v -> v.remove(session)); - } + public void remove(Session session) { + sessions.values().forEach(v -> v.remove(session)); + } - public void sendMessage(User user, String text) { - Set userSessions = sessions.get(user.getId()); - if (userSessions != null && !userSessions.isEmpty()) { - log.debug("sending '{}' to user {} via websocket ({} sessions)", text, user.getId(), userSessions.size()); - for (Session userSession : userSessions) { - if (userSession.isOpen()) { - userSession.getAsyncRemote().sendText(text); - } - } - } - } + public void sendMessage(User user, String text) { + Set userSessions = sessions.get(user.getId()); + if (userSessions != null && !userSessions.isEmpty()) { + log.debug( + "sending '{}' to user {} via websocket ({} sessions)", + text, + user.getId(), + userSessions.size()); + for (Session userSession : userSessions) { + if (userSession.isOpen()) { + userSession.getAsyncRemote().sendText(text); + } + } + } + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java index ca8e11e2..500d8ef4 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java +++ b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java @@ -1,30 +1,28 @@ package com.commafeed.security; -import jakarta.inject.Singleton; - import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; - import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class AuthenticationContext { - private final SecurityIdentity securityIdentity; - private final UserDAO userDAO; + private final SecurityIdentity securityIdentity; + private final UserDAO userDAO; - public User getCurrentUser() { - if (securityIdentity.isAnonymous()) { - return null; - } + public User getCurrentUser() { + if (securityIdentity.isAnonymous()) { + return null; + } - String userId = securityIdentity.getPrincipal().getName(); - if (userId == null) { - return null; - } + String userId = securityIdentity.getPrincipal().getName(); + if (userId == null) { + return null; + } - return userDAO.findById(Long.valueOf(userId)); - } + return userDAO.findById(Long.valueOf(userId)); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/CookieService.java b/commafeed-server/src/main/java/com/commafeed/security/CookieService.java index 84235e74..3b60aa65 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/CookieService.java +++ b/commafeed-server/src/main/java/com/commafeed/security/CookieService.java @@ -1,24 +1,25 @@ package com.commafeed.security; -import java.time.Instant; -import java.util.Date; - +import io.quarkus.vertx.http.runtime.VertxHttpConfig; import jakarta.inject.Singleton; import jakarta.ws.rs.core.NewCookie; - -import io.quarkus.vertx.http.runtime.VertxHttpConfig; +import java.time.Instant; +import java.util.Date; @Singleton public class CookieService { - private final String cookieName; + private final String cookieName; - public CookieService(VertxHttpConfig config) { - this.cookieName = config.auth().form().cookieName(); - } - - public NewCookie buildLogoutCookie() { - return new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build(); - } + public CookieService(VertxHttpConfig config) { + this.cookieName = config.auth().form().cookieName(); + } + public NewCookie buildLogoutCookie() { + return new NewCookie.Builder(cookieName) + .maxAge(0) + .expiry(Date.from(Instant.EPOCH)) + .path("/") + .build(); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/Roles.java b/commafeed-server/src/main/java/com/commafeed/security/Roles.java index 757fa829..aa0f8e72 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/Roles.java +++ b/commafeed-server/src/main/java/com/commafeed/security/Roles.java @@ -4,6 +4,6 @@ import lombok.experimental.UtilityClass; @UtilityClass public class Roles { - public static final String USER = "USER"; - public static final String ADMIN = "ADMIN"; + public static final String USER = "USER"; + public static final String ADMIN = "ADMIN"; } diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java index 4efd63dd..34e0fa36 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java @@ -1,16 +1,9 @@ package com.commafeed.security.identity; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import jakarta.inject.Singleton; - import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.UserService; - import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; @@ -19,33 +12,42 @@ import io.quarkus.security.identity.request.TokenAuthenticationRequest; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton -public class DatabaseApiKeyIdentityProvider implements IdentityProvider { +public class DatabaseApiKeyIdentityProvider + implements IdentityProvider { - private final UnitOfWork unitOfWork; - private final UserService userService; + private final UnitOfWork unitOfWork; + private final UserService userService; - @Override - public Class getRequestType() { - return TokenAuthenticationRequest.class; - } + @Override + public Class getRequestType() { + return TokenAuthenticationRequest.class; + } - @Override - public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { - return context.runBlocking(() -> { - Optional user = unitOfWork.call(() -> userService.login(request.getToken().getToken())); - if (user.isEmpty()) { - throw new AuthenticationFailedException("could not find a user with this api key"); - } + @Override + public Uni authenticate( + TokenAuthenticationRequest request, AuthenticationRequestContext context) { + return context.runBlocking( + () -> { + Optional user = + unitOfWork.call(() -> userService.login(request.getToken().getToken())); + if (user.isEmpty()) { + throw new AuthenticationFailedException( + "could not find a user with this api key"); + } - Set roles = unitOfWork.call(() -> userService.getRoles(user.get())); - return QuarkusSecurityIdentity.builder() - .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId()))) - .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) - .build(); - }); - } + Set roles = unitOfWork.call(() -> userService.getRoles(user.get())); + return QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId()))) + .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) + .build(); + }); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java index 2fd42183..601d2c80 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java @@ -1,16 +1,9 @@ package com.commafeed.security.identity; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import jakarta.inject.Singleton; - import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.UserService; - import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; @@ -19,34 +12,46 @@ import io.quarkus.security.identity.request.UsernamePasswordAuthenticationReques import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton -public class DatabaseUsernamePasswordIdentityProvider implements IdentityProvider { +public class DatabaseUsernamePasswordIdentityProvider + implements IdentityProvider { - private final UnitOfWork unitOfWork; - private final UserService userService; + private final UnitOfWork unitOfWork; + private final UserService userService; - @Override - public Class getRequestType() { - return UsernamePasswordAuthenticationRequest.class; - } + @Override + public Class getRequestType() { + return UsernamePasswordAuthenticationRequest.class; + } - @Override - public Uni authenticate(UsernamePasswordAuthenticationRequest request, AuthenticationRequestContext context) { - return context.runBlocking(() -> { - Optional user = unitOfWork - .call(() -> userService.login(request.getUsername(), new String(request.getPassword().getPassword()))); - if (user.isEmpty()) { - throw new AuthenticationFailedException("wrong username or password"); - } + @Override + public Uni authenticate( + UsernamePasswordAuthenticationRequest request, AuthenticationRequestContext context) { + return context.runBlocking( + () -> { + Optional user = + unitOfWork.call( + () -> + userService.login( + request.getUsername(), + new String( + request.getPassword().getPassword()))); + if (user.isEmpty()) { + throw new AuthenticationFailedException("wrong username or password"); + } - Set roles = unitOfWork.call(() -> userService.getRoles(user.get())); - return QuarkusSecurityIdentity.builder() - .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId()))) - .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) - .build(); - }); - } + Set roles = unitOfWork.call(() -> userService.getRoles(user.get())); + return QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId()))) + .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) + .build(); + }); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java index 6de26dc5..9f5c5def 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java @@ -1,17 +1,11 @@ package com.commafeed.security.identity; -import java.util.Set; -import java.util.stream.Collectors; - -import jakarta.inject.Singleton; - import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.internal.PostLoginActivities; - import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; @@ -20,39 +14,46 @@ import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Singleton public class TrustedIdentityProvider implements IdentityProvider { - private final UnitOfWork unitOfWork; - private final UserService userService; - private final UserDAO userDAO; - private final PostLoginActivities postLoginActivities; + private final UnitOfWork unitOfWork; + private final UserService userService; + private final UserDAO userDAO; + private final PostLoginActivities postLoginActivities; - @Override - public Class getRequestType() { - return TrustedAuthenticationRequest.class; - } + @Override + public Class getRequestType() { + return TrustedAuthenticationRequest.class; + } - @Override - public Uni authenticate(TrustedAuthenticationRequest request, AuthenticationRequestContext context) { - return context.runBlocking(() -> { - Long userId = Long.valueOf(request.getPrincipal()); - User user = unitOfWork.call(() -> userDAO.findById(userId)); - if (user == null) { - throw new AuthenticationFailedException("user not found"); - } + @Override + public Uni authenticate( + TrustedAuthenticationRequest request, AuthenticationRequestContext context) { + return context.runBlocking( + () -> { + Long userId = Long.valueOf(request.getPrincipal()); + User user = unitOfWork.call(() -> userDAO.findById(userId)); + if (user == null) { + throw new AuthenticationFailedException("user not found"); + } - // execute post login activities manually because we didn't call login() since we received a trusted authentication request - unitOfWork.run(() -> postLoginActivities.executeFor(user)); + // execute post login activities manually because we didn't call login() since + // we received + // a trusted authentication request + unitOfWork.run(() -> postLoginActivities.executeFor(user)); - Set roles = unitOfWork.call(() -> userService.getRoles(user)); - return QuarkusSecurityIdentity.builder() - .setPrincipal(new QuarkusPrincipal(String.valueOf(userId))) - .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) - .build(); - }); - } + Set roles = unitOfWork.call(() -> userService.getRoles(user)); + return QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(String.valueOf(userId))) + .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet())) + .build(); + }); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java index be6e3bd7..f8355dda 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java +++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java @@ -1,10 +1,5 @@ package com.commafeed.security.mechanism; -import java.util.Optional; -import java.util.Set; - -import jakarta.inject.Singleton; - import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; @@ -14,34 +9,38 @@ import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; +import jakarta.inject.Singleton; +import java.util.Optional; +import java.util.Set; @Singleton public class ApiKeyAuthenticationMecanism implements HttpAuthenticationMechanism { - @Override - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - // only authorize api key for GET requests - if (!context.request().method().name().equals("GET")) { - return Uni.createFrom().optional(Optional.empty()); - } + @Override + public Uni authenticate( + RoutingContext context, IdentityProviderManager identityProviderManager) { + // only authorize api key for GET requests + if (!context.request().method().name().equals("GET")) { + return Uni.createFrom().optional(Optional.empty()); + } - String apiKey = context.request().getParam("apiKey"); - if (apiKey == null) { - return Uni.createFrom().optional(Optional.empty()); - } + String apiKey = context.request().getParam("apiKey"); + if (apiKey == null) { + return Uni.createFrom().optional(Optional.empty()); + } - TokenCredential token = new TokenCredential(apiKey, "apiKey"); - TokenAuthenticationRequest request = new TokenAuthenticationRequest(token); - return identityProviderManager.authenticate(request); - } + TokenCredential token = new TokenCredential(apiKey, "apiKey"); + TokenAuthenticationRequest request = new TokenAuthenticationRequest(token); + return identityProviderManager.authenticate(request); + } - @Override - public Uni getChallenge(RoutingContext context) { - return Uni.createFrom().optional(Optional.empty()); - } + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().optional(Optional.empty()); + } - @Override - public Set> getCredentialTypes() { - return Set.of(TokenAuthenticationRequest.class); - } + @Override + public Set> getCredentialTypes() { + return Set.of(TokenAuthenticationRequest.class); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java index 44adfccb..d631779e 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java +++ b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java @@ -1,10 +1,9 @@ package com.commafeed.security.password; -import java.util.List; - import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; - +import java.util.List; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.passay.DefaultPasswordValidator; import org.passay.PasswordData; @@ -13,42 +12,41 @@ import org.passay.ValidationResult; import org.passay.rule.LengthRule; import org.passay.rule.WhitespaceRule; -import lombok.Setter; - public class PasswordConstraintValidator implements ConstraintValidator { - @Setter - private static int minimumPasswordLength; + @Setter private static int minimumPasswordLength; - @Override - public void initialize(ValidPassword constraintAnnotation) { - // nothing to do - } + @Override + public void initialize(ValidPassword constraintAnnotation) { + // nothing to do + } - @Override - public boolean isValid(String value, ConstraintValidatorContext context) { - if (StringUtils.isBlank(value)) { - return true; - } + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (StringUtils.isBlank(value)) { + return true; + } - PasswordValidator validator = buildPasswordValidator(); - ValidationResult result = validator.validate(new PasswordData(value)); + PasswordValidator validator = buildPasswordValidator(); + ValidationResult result = validator.validate(new PasswordData(value)); - if (result.isValid()) { - return true; - } + if (result.isValid()) { + return true; + } - List messages = result.getMessages(); - String message = String.join(System.lineSeparator(), messages); - context.buildConstraintViolationWithTemplate(message).addConstraintViolation().disableDefaultConstraintViolation(); - return false; - } + List messages = result.getMessages(); + String message = String.join(System.lineSeparator(), messages); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation() + .disableDefaultConstraintViolation(); + return false; + } - private PasswordValidator buildPasswordValidator() { - return new DefaultPasswordValidator( - // length - new LengthRule(minimumPasswordLength, 256), - // no whitespace - new WhitespaceRule()); - } + private PasswordValidator buildPasswordValidator() { + return new DefaultPasswordValidator( + // length + new LengthRule(minimumPasswordLength, 256), + // no whitespace + new WhitespaceRule()); + } } diff --git a/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java index e563e0d4..0ef051e6 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java +++ b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java @@ -1,23 +1,22 @@ package com.commafeed.security.password; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - @Documented @Constraint(validatedBy = PasswordConstraintValidator.class) -@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ValidPassword { - String message() default "Invalid Password"; + String message() default "Invalid Password"; - Class[] groups() default {}; + Class[] groups() default {}; - Class[] payload() default {}; + Class[] payload() default {}; } diff --git a/commafeed-server/src/main/java/com/commafeed/tools/CommaFeedPropertiesGenerator.java b/commafeed-server/src/main/java/com/commafeed/tools/CommaFeedPropertiesGenerator.java index 348c6d58..b2765f45 100644 --- a/commafeed-server/src/main/java/com/commafeed/tools/CommaFeedPropertiesGenerator.java +++ b/commafeed-server/src/main/java/com/commafeed/tools/CommaFeedPropertiesGenerator.java @@ -1,5 +1,14 @@ package com.commafeed.tools; +import com.commafeed.CommaFeedConfiguration; +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.model.AbstractConfigItem; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -10,82 +19,78 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; - import org.apache.commons.io.IOUtils; -import com.commafeed.CommaFeedConfiguration; - -import io.quarkus.annotation.processor.Outputs; -import io.quarkus.annotation.processor.documentation.config.model.AbstractConfigItem; -import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; -import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; -import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; -import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; -import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; -import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; - /** - * This class generates an application.properties file with all the properties from {@link CommaFeedConfiguration}. + * This class generates an application.properties file with all the properties from {@link + * CommaFeedConfiguration}. * - * This is useful for people who want to be able to configure CommaFeed without having to look at the code or the documentation, or for - * distribution packages that want to provide a default configuration file. - * - **/ + *

This is useful for people who want to be able to configure CommaFeed without having to look at + * the code or the documentation, or for distribution packages that want to provide a default + * configuration file. + */ public class CommaFeedPropertiesGenerator { - private final List lines = new ArrayList<>(); + private final List lines = new ArrayList<>(); - public static void main(String[] args) throws Exception { - Path targetPath = Paths.get(args[0]); + public static void main(String[] args) throws Exception { + Path targetPath = Paths.get(args[0]); - Path modelPath = targetPath.resolve(Outputs.QUARKUS_CONFIG_DOC_MODEL); - Path javadocPath = targetPath.resolve(Outputs.QUARKUS_CONFIG_DOC_JAVADOC); - Path outputPath = targetPath.resolve("quarkus-generated-doc").resolve("application.properties"); + Path modelPath = targetPath.resolve(Outputs.QUARKUS_CONFIG_DOC_MODEL); + Path javadocPath = targetPath.resolve(Outputs.QUARKUS_CONFIG_DOC_JAVADOC); + Path outputPath = + targetPath.resolve("quarkus-generated-doc").resolve("application.properties"); - try (InputStream model = Files.newInputStream(modelPath); - InputStream javadoc = Files.newInputStream(javadocPath); - OutputStream output = Files.newOutputStream(outputPath)) { - new CommaFeedPropertiesGenerator().generate(model, javadoc, output); - } - } + try (InputStream model = Files.newInputStream(modelPath); + InputStream javadoc = Files.newInputStream(javadocPath); + OutputStream output = Files.newOutputStream(outputPath)) { + new CommaFeedPropertiesGenerator().generate(model, javadoc, output); + } + } - void generate(InputStream model, InputStream javadoc, OutputStream output) throws IOException { - ResolvedModel resolvedModel = JacksonMappers.yamlObjectReader().readValue(model, ResolvedModel.class); - JavadocElements javadocElements = JacksonMappers.yamlObjectReader().readValue(javadoc, JavadocElements.class); + void generate(InputStream model, InputStream javadoc, OutputStream output) throws IOException { + ResolvedModel resolvedModel = + JacksonMappers.yamlObjectReader().readValue(model, ResolvedModel.class); + JavadocElements javadocElements = + JacksonMappers.yamlObjectReader().readValue(javadoc, JavadocElements.class); - for (ConfigRoot configRoot : resolvedModel.getConfigRoots()) { - for (AbstractConfigItem item : configRoot.getItems()) { - handleAbstractConfigItem(item, javadocElements); - } - } + for (ConfigRoot configRoot : resolvedModel.getConfigRoots()) { + for (AbstractConfigItem item : configRoot.getItems()) { + handleAbstractConfigItem(item, javadocElements); + } + } - IOUtils.write(String.join("\n", lines), output, StandardCharsets.UTF_8); - } + IOUtils.write(String.join("\n", lines), output, StandardCharsets.UTF_8); + } - private void handleAbstractConfigItem(AbstractConfigItem item, JavadocElements javadocElements) { - if (item.isSection()) { - handleSection((ConfigSection) item, javadocElements); - } else { - handleProperty((ConfigProperty) item, javadocElements); - } - } + private void handleAbstractConfigItem( + AbstractConfigItem item, JavadocElements javadocElements) { + if (item.isSection()) { + handleSection((ConfigSection) item, javadocElements); + } else { + handleProperty((ConfigProperty) item, javadocElements); + } + } - private void handleSection(ConfigSection section, JavadocElements javadocElements) { - for (AbstractConfigItem item : section.getItems()) { - handleAbstractConfigItem(item, javadocElements); - } - } + private void handleSection(ConfigSection section, JavadocElements javadocElements) { + for (AbstractConfigItem item : section.getItems()) { + handleAbstractConfigItem(item, javadocElements); + } + } - private void handleProperty(ConfigProperty property, JavadocElements javadocElements) { - String key = property.getPath().property(); - String description = javadocElements.elements() - .get(property.getSourceType() + "." + property.getSourceElementName()) - .description() - .replace("\n", "\n# "); - String defaultValue = Optional.ofNullable(property.getDefaultValue()).orElse("").toLowerCase(); + private void handleProperty(ConfigProperty property, JavadocElements javadocElements) { + String key = property.getPath().property(); + String description = + javadocElements + .elements() + .get(property.getSourceType() + "." + property.getSourceElementName()) + .description() + .replace("\n", "\n# "); + String defaultValue = + Optional.ofNullable(property.getDefaultValue()).orElse("").toLowerCase(); - lines.add("# " + description); - lines.add(key + "=" + defaultValue); - lines.add(""); - } + lines.add("# " + description); + lines.add(key + "=" + defaultValue); + lines.add(""); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java index 25784b30..86fdc0f2 100644 --- a/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java +++ b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java @@ -1,35 +1,37 @@ package com.commafeed; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.persistence.EntityManager; - -import org.hibernate.Session; -import org.kohsuke.MetaInfServices; - import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.persistence.EntityManager; +import org.hibernate.Session; +import org.kohsuke.MetaInfServices; -/** - * Resets database between tests - */ +/** Resets database between tests */ @MetaInfServices public class DatabaseReset implements QuarkusTestBeforeEachCallback { - @Override - public void beforeEach(QuarkusTestMethodContext context) { - // stop the application to make sure that there are no active transactions when we truncate the tables - getBean(CommaFeedApplication.class).stop(new ShutdownEvent()); + @Override + public void beforeEach(QuarkusTestMethodContext context) { + // stop the application to make sure that there are no active transactions when we truncate + // the + // tables + getBean(CommaFeedApplication.class).stop(new ShutdownEvent()); - // truncate all tables so that we have a clean slate for the next test - getBean(EntityManager.class).unwrap(Session.class).getSessionFactory().getSchemaManager().truncateMappedObjects(); + // truncate all tables so that we have a clean slate for the next test + getBean(EntityManager.class) + .unwrap(Session.class) + .getSessionFactory() + .getSchemaManager() + .truncateMappedObjects(); - // restart the application - getBean(CommaFeedApplication.class).start(new StartupEvent()); - } + // restart the application + getBean(CommaFeedApplication.class).start(new StartupEvent()); + } - private static T getBean(Class clazz) { - return CDI.current().select(clazz).get(); - } + private static T getBean(Class clazz) { + return CDI.current().select(clazz).get(); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java b/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java index 1c2cf213..7df0a5bc 100644 --- a/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java +++ b/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java @@ -1,43 +1,54 @@ package com.commafeed; +import com.rometools.rome.feed.CopyFrom; +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.io.WireFeedGenerator; +import com.rometools.rome.io.WireFeedParser; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.reflections.Reflections; import org.reflections.scanners.Scanners; -import com.rometools.rome.feed.CopyFrom; -import com.rometools.rome.feed.module.Module; -import com.rometools.rome.io.WireFeedGenerator; -import com.rometools.rome.io.WireFeedParser; - -import io.quarkus.runtime.annotations.RegisterForReflection; - class NativeImageClassesTest { - @Test - void annotationContainsAllRequiredRomeClasses() { - Reflections reflections = new Reflections("com.rometools"); - Set> classesInAnnotation = Set - .copyOf(List.of(NativeImageClasses.class.getAnnotation(RegisterForReflection.class).targets())); + @Test + void annotationContainsAllRequiredRomeClasses() { + Reflections reflections = new Reflections("com.rometools"); + Set> classesInAnnotation = + Set.copyOf( + List.of( + NativeImageClasses.class + .getAnnotation(RegisterForReflection.class) + .targets())); - List> missingClasses = new ArrayList<>(); - for (Class clazz : List.of(Module.class, Cloneable.class, CopyFrom.class, WireFeedParser.class, WireFeedGenerator.class)) { - Set> moduleClasses = new HashSet<>(reflections.get(Scanners.SubTypes.of(clazz).asClass())); - moduleClasses.removeIf(c -> c.isInterface() || Modifier.isAbstract(c.getModifiers()) || !Modifier.isPublic(c.getModifiers())); - moduleClasses.removeAll(classesInAnnotation); - missingClasses.addAll(moduleClasses); - } + List> missingClasses = new ArrayList<>(); + for (Class clazz : + List.of( + Module.class, + Cloneable.class, + CopyFrom.class, + WireFeedParser.class, + WireFeedGenerator.class)) { + Set> moduleClasses = + new HashSet<>(reflections.get(Scanners.SubTypes.of(clazz).asClass())); + moduleClasses.removeIf( + c -> + c.isInterface() + || Modifier.isAbstract(c.getModifiers()) + || !Modifier.isPublic(c.getModifiers())); + moduleClasses.removeAll(classesInAnnotation); + missingClasses.addAll(moduleClasses); + } - missingClasses.sort(Comparator.comparing(Class::getName)); - missingClasses.forEach(c -> System.out.println(c.getName() + ".class,")); - Assertions.assertEquals(List.of(), missingClasses); - } - -} \ No newline at end of file + missingClasses.sort(Comparator.comparing(Class::getName)); + missingClasses.forEach(c -> System.out.println(c.getName() + ".class,")); + Assertions.assertEquals(List.of(), missingClasses); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/TestConstants.java b/commafeed-server/src/test/java/com/commafeed/TestConstants.java index 91a28eeb..bccff27a 100644 --- a/commafeed-server/src/test/java/com/commafeed/TestConstants.java +++ b/commafeed-server/src/test/java/com/commafeed/TestConstants.java @@ -1,6 +1,6 @@ package com.commafeed; public class TestConstants { - public static final String ADMIN_USERNAME = "admin"; - public static final String ADMIN_PASSWORD = "admin"; + public static final String ADMIN_USERNAME = "admin"; + public static final String ADMIN_PASSWORD = "admin"; } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/DigestsTest.java b/commafeed-server/src/test/java/com/commafeed/backend/DigestsTest.java index e4ad91f8..69745fd2 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/DigestsTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/DigestsTest.java @@ -5,14 +5,14 @@ import org.junit.jupiter.api.Test; class DigestsTest { - @Test - void sha1Hex() { - Assertions.assertEquals("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", Digests.sha1Hex("hello")); - } + @Test + void sha1Hex() { + Assertions.assertEquals( + "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", Digests.sha1Hex("hello")); + } - @Test - void md5Hex() { - Assertions.assertEquals("5d41402abc4b2a76b9719d911017c592", Digests.md5Hex("hello")); - } - -} \ No newline at end of file + @Test + void md5Hex() { + Assertions.assertEquals("5d41402abc4b2a76b9719d911017c592", Digests.md5Hex("hello")); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java index 16dc72e9..8da32a58 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -1,5 +1,14 @@ package com.commafeed.backend; +import com.codahale.metrics.MetricRegistry; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedVersion; +import com.commafeed.backend.HttpGetter.HttpResponseException; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.HttpGetter.NotModifiedException; +import com.commafeed.backend.HttpGetter.TooManyRequestsException; +import com.google.common.net.HttpHeaders; +import io.quarkus.runtime.configuration.MemorySize; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -15,7 +24,6 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPOutputStream; - import org.apache.commons.io.IOUtils; import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.core5.http.HttpStatus; @@ -35,387 +43,508 @@ import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import org.mockserver.model.MediaType; -import com.codahale.metrics.MetricRegistry; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedVersion; -import com.commafeed.backend.HttpGetter.HttpResponseException; -import com.commafeed.backend.HttpGetter.HttpResult; -import com.commafeed.backend.HttpGetter.NotModifiedException; -import com.commafeed.backend.HttpGetter.TooManyRequestsException; -import com.google.common.net.HttpHeaders; - -import io.quarkus.runtime.configuration.MemorySize; - class HttpGetterTest { - private static final Instant NOW = Instant.now(); - - private MockServerClient mockServerClient; - private String feedUrl; - private byte[] feedContent; - - private CommaFeedConfiguration config; - - private HttpClientFactory provider; - private HttpGetter getter; - - @BeforeEach - void init() throws IOException { - this.mockServerClient = ClientAndServer.startClientAndServer(0); - this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/"; - this.feedContent = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/feed/rss.xml"))); - - this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); - Mockito.when(config.httpClient().userAgent()).thenReturn(Optional.of("http-getter-test")); - Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofSeconds(30)); - Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(30)); - Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofSeconds(30)); - Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30)); - Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30)); - Mockito.when(config.httpClient().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); - Mockito.when(config.httpClient().cache().enabled()).thenReturn(true); - Mockito.when(config.httpClient().cache().maximumMemorySize()).thenReturn(new MemorySize(new BigInteger("100000"))); - Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1)); - Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); - - this.provider = new HttpClientFactory(config, Mockito.mock(CommaFeedVersion.class)); - this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); - } - - @AfterEach - void tearDown() { - if (this.mockServerClient != null) { - this.mockServerClient.stop(); - } - } - - @ParameterizedTest - @ValueSource( - ints = { HttpStatus.SC_UNAUTHORIZED, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_NOT_FOUND, HttpStatus.SC_INTERNAL_SERVER_ERROR }) - void errorCodes(int code) { - this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withStatusCode(code)); - - HttpResponseException e = Assertions.assertThrows(HttpResponseException.class, () -> getter.get(this.feedUrl)); - Assertions.assertEquals(code, e.getCode()); - } - - @Test - void validFeed() throws Exception { - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response() - .withBody(feedContent) - .withContentType(MediaType.APPLICATION_ATOM_XML) - .withHeader(HttpHeaders.LAST_MODIFIED, "123456") - .withHeader(HttpHeaders.ETAG, "78910") - .withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate") - .withHeader(HttpHeaders.RETRY_AFTER, "120")); - - HttpResult result = getter.get(this.feedUrl); - Assertions.assertArrayEquals(feedContent, result.content()); - Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.contentType()); - Assertions.assertEquals("123456", result.lastModifiedSince()); - Assertions.assertEquals("78910", result.eTag()); - Assertions.assertEquals(Duration.ofSeconds(60), result.validFor()); - Assertions.assertEquals(this.feedUrl, result.urlAfterRedirect()); - } - - @Test - void ignoreInvalidCacheControlValue() throws Exception { - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response() - .withBody(feedContent) - .withContentType(MediaType.APPLICATION_ATOM_XML) - .withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60; must-revalidate")); - - HttpResult result = getter.get(this.feedUrl); - Assertions.assertEquals(Duration.ZERO, result.validFor()); - } - - @Test - void tooManyRequestsExceptionSeconds() { - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond( - HttpResponse.response().withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS).withHeader(HttpHeaders.RETRY_AFTER, "120")); - - TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl)); - Assertions.assertEquals(NOW.plusSeconds(120), e.getRetryAfter()); - } - - @Test - void tooManyRequestsExceptionDate() { - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response() - .withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS) - .withHeader(HttpHeaders.RETRY_AFTER, "Wed, 21 Oct 2015 07:28:00 GMT")); - - TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl)); - Assertions.assertEquals(Instant.parse("2015-10-21T07:28:00Z"), e.getRetryAfter()); - } - - @ParameterizedTest - @ValueSource( - ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT, - HttpStatus.SC_PERMANENT_REDIRECT }) - void followRedirects(int code) throws Exception { - // first redirect - this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/")) - .respond(HttpResponse.response() - .withStatusCode(code) - .withHeader(HttpHeaders.LOCATION, "http://localhost:" + this.mockServerClient.getPort() + "/redirected")); - - // second redirect - this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/redirected")) - .respond(HttpResponse.response() - .withStatusCode(code) - .withHeader(HttpHeaders.LOCATION, "http://localhost:" + this.mockServerClient.getPort() + "/redirected-2")); - - // final destination - this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/redirected-2")) - .respond(HttpResponse.response().withBody(feedContent).withContentType(MediaType.APPLICATION_ATOM_XML)); - - HttpResult result = getter.get(this.feedUrl); - Assertions.assertEquals("http://localhost:" + this.mockServerClient.getPort() + "/redirected-2", result.urlAfterRedirect()); - } - - @Test - void dataTimeout() { - Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500)); - this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); - - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response().withDelay(Delay.milliseconds(1000))); - - Assertions.assertThrows(SocketTimeoutException.class, () -> getter.get(this.feedUrl)); - } - - @Test - void connectTimeout() { - Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500)); - this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); - // try to connect to a non-routable address - // https://stackoverflow.com/a/904609 - Exception e = Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1")); - Assertions.assertTrue(e instanceof ConnectTimeoutException - // A NoRouteToHostException can also be thrown in some cases - // depending on the underlying network configuration - // https://github.com/Athou/commafeed/issues/1876 - || e instanceof NoRouteToHostException, - "Expected ConnectTimeoutException or NoRouteToHostException, but got: " + e.getClass().getName()); - } - - @Test - void userAgent() throws Exception { - this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.USER_AGENT, "http-getter-test")) - .respond(HttpResponse.response().withBody("ok")); - - HttpResult result = getter.get(this.feedUrl); - Assertions.assertEquals("ok", new String(result.content())); - } - - @Test - void lastModifiedReturns304() { - this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456")) - .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); - - Assertions.assertThrows(NotModifiedException.class, - () -> getter.get(HttpGetter.HttpRequest.builder(this.feedUrl).lastModified("123456").build())); - } - - @Test - void eTagReturns304() { - this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_NONE_MATCH, "78910")) - .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); - - Assertions.assertThrows(NotModifiedException.class, - () -> getter.get(HttpGetter.HttpRequest.builder(this.feedUrl).eTag("78910").build())); - } - - @Test - void ignoreCookie() { - AtomicInteger calls = new AtomicInteger(); - - this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> { - calls.incrementAndGet(); - - if (req.containsHeader(HttpHeaders.COOKIE)) { - throw new Exception("cookie should not be sent by the client"); - } - - return HttpResponse.response().withBody("ok").withHeader(HttpHeaders.SET_COOKIE, "foo=bar"); - }); - - Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl)); - Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl + "?foo=bar")); - Assertions.assertEquals(2, calls.get()); - } - - @Test - void cacheSubsequentCalls() throws Exception { - AtomicInteger calls = new AtomicInteger(); - - this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> { - calls.incrementAndGet(); - return HttpResponse.response().withBody("ok"); - }); - - HttpResult result = getter.get(this.feedUrl); - Assertions.assertEquals(result, getter.get(this.feedUrl)); - Assertions.assertEquals(1, calls.get()); - } - - @Test - void largeFeedWithContentLengthHeader() { - byte[] bytes = new byte[100000]; - Arrays.fill(bytes, (byte) 1); - this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody(bytes)); - - IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl)); - Assertions.assertEquals("Response size (100000 bytes) exceeds the maximum allowed size (10000 bytes)", e.getMessage()); - } - - @Test - void largeFeedWithoutContentLengthHeader() { - byte[] bytes = new byte[100000]; - Arrays.fill(bytes, (byte) 1); - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response() - .withBody(bytes) - .withConnectionOptions(ConnectionOptions.connectionOptions().withSuppressContentLengthHeader(true))); - - IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl)); - Assertions.assertEquals("Response size exceeds the maximum allowed size (10000 bytes)", e.getMessage()); - } - - @Test - void ignoreInvalidSsl() throws Exception { - this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody("ok")); - - HttpResult result = getter.get("https://localhost:" + this.mockServerClient.getPort()); - Assertions.assertEquals("ok", new String(result.content())); - } - - @Test - void doesNotUseUpgradeProtocolHeader() { - AtomicInteger calls = new AtomicInteger(); - - this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> { - calls.incrementAndGet(); - - if (req.containsHeader(HttpHeaders.UPGRADE)) { - throw new Exception("upgrade header should not be sent by the client"); - } - - return HttpResponse.response().withBody("ok"); - }); - - Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl)); - Assertions.assertEquals(1, calls.get()); - } - - @Nested - class Compression { - - private static final String ACCEPT_ENCODING = "gzip, deflate, br"; - - @Test - void gzip() throws Exception { - supportsCompression("gzip", GZIPOutputStream::new); - } - - @Test - void deflate() throws Exception { - supportsCompression("deflate", DeflaterOutputStream::new); - } - - void supportsCompression(String encoding, CompressionFunction compressionFunction) throws Exception { - String body = "my body"; - - HttpGetterTest.this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> { - String acceptEncodingHeader = req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING); - if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) { - throw new Exception("Wrong value in the Accept-Encoding header, should be '%s' but was '%s'".formatted(ACCEPT_ENCODING, - acceptEncodingHeader)); - } - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - try (OutputStream compressionOutputStream = compressionFunction.apply(output)) { - compressionOutputStream.write(body.getBytes()); - } - - return HttpResponse.response().withBody(output.toByteArray()).withHeader(HttpHeaders.CONTENT_ENCODING, encoding); - }); - - HttpResult result = getter.get(HttpGetterTest.this.feedUrl); - Assertions.assertEquals(body, new String(result.content())); - } - - @FunctionalInterface - public interface CompressionFunction { - OutputStream apply(OutputStream input) throws IOException; - } - - } - - @Nested - class SchemeNotAllowed { - @Test - void file() { - Assertions.assertThrows(HttpGetter.SchemeNotAllowedException.class, () -> getter.get("file://localhost")); - } - - @Test - void ftp() { - Assertions.assertThrows(HttpGetter.SchemeNotAllowedException.class, () -> getter.get("ftp://localhost")); - } - } - - @Nested - class HostNotAllowed { - - @BeforeEach - void init() { - Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true); - getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); - } - - @Test - void localhost() { - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://localhost")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://127.0.0.1")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://2130706433")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01")); - } - - @Test - void zero() { - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0.0.0.0")); - } - - @Test - void linkLocal() { - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://169.254.12.34")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://169.254.169.254")); - } - - @Test - void multicast() { - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://224.2.3.4")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://239.255.255.254")); - } - - @Test - void privateIpv4Ranges() { - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://10.0.0.1")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://172.16.0.1")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://192.168.0.1")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://100.64.0.1")); - } - - @Test - void privateIpv6Ranges() { - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://[fe80::215:5dff:fe15:102]")); - Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]")); - } - } - -} \ No newline at end of file + private static final Instant NOW = Instant.now(); + + private MockServerClient mockServerClient; + private String feedUrl; + private byte[] feedContent; + + private CommaFeedConfiguration config; + + private HttpClientFactory provider; + private HttpGetter getter; + + @BeforeEach + void init() throws IOException { + this.mockServerClient = ClientAndServer.startClientAndServer(0); + this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/"; + this.feedContent = + IOUtils.toByteArray( + Objects.requireNonNull(getClass().getResource("/feed/rss.xml"))); + + this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(config.httpClient().userAgent()).thenReturn(Optional.of("http-getter-test")); + Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(config.httpClient().maxResponseSize()) + .thenReturn(new MemorySize(new BigInteger("10000"))); + Mockito.when(config.httpClient().cache().enabled()).thenReturn(true); + Mockito.when(config.httpClient().cache().maximumMemorySize()) + .thenReturn(new MemorySize(new BigInteger("100000"))); + Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1)); + Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); + + this.provider = new HttpClientFactory(config, Mockito.mock(CommaFeedVersion.class)); + this.getter = + new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); + } + + @AfterEach + void tearDown() { + if (this.mockServerClient != null) { + this.mockServerClient.stop(); + } + } + + @ParameterizedTest + @ValueSource( + ints = { + HttpStatus.SC_UNAUTHORIZED, + HttpStatus.SC_FORBIDDEN, + HttpStatus.SC_NOT_FOUND, + HttpStatus.SC_INTERNAL_SERVER_ERROR + }) + void errorCodes(int code) { + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond(HttpResponse.response().withStatusCode(code)); + + HttpResponseException e = + Assertions.assertThrows( + HttpResponseException.class, () -> getter.get(this.feedUrl)); + Assertions.assertEquals(code, e.getCode()); + } + + @Test + void validFeed() throws Exception { + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + HttpResponse.response() + .withBody(feedContent) + .withContentType(MediaType.APPLICATION_ATOM_XML) + .withHeader(HttpHeaders.LAST_MODIFIED, "123456") + .withHeader(HttpHeaders.ETAG, "78910") + .withHeader( + HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate") + .withHeader(HttpHeaders.RETRY_AFTER, "120")); + + HttpResult result = getter.get(this.feedUrl); + Assertions.assertArrayEquals(feedContent, result.content()); + Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.contentType()); + Assertions.assertEquals("123456", result.lastModifiedSince()); + Assertions.assertEquals("78910", result.eTag()); + Assertions.assertEquals(Duration.ofSeconds(60), result.validFor()); + Assertions.assertEquals(this.feedUrl, result.urlAfterRedirect()); + } + + @Test + void ignoreInvalidCacheControlValue() throws Exception { + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + HttpResponse.response() + .withBody(feedContent) + .withContentType(MediaType.APPLICATION_ATOM_XML) + .withHeader( + HttpHeaders.CACHE_CONTROL, "max-age=60; must-revalidate")); + + HttpResult result = getter.get(this.feedUrl); + Assertions.assertEquals(Duration.ZERO, result.validFor()); + } + + @Test + void tooManyRequestsExceptionSeconds() { + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + HttpResponse.response() + .withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS) + .withHeader(HttpHeaders.RETRY_AFTER, "120")); + + TooManyRequestsException e = + Assertions.assertThrows( + TooManyRequestsException.class, () -> getter.get(this.feedUrl)); + Assertions.assertEquals(NOW.plusSeconds(120), e.getRetryAfter()); + } + + @Test + void tooManyRequestsExceptionDate() { + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + HttpResponse.response() + .withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS) + .withHeader( + HttpHeaders.RETRY_AFTER, "Wed, 21 Oct 2015 07:28:00 GMT")); + + TooManyRequestsException e = + Assertions.assertThrows( + TooManyRequestsException.class, () -> getter.get(this.feedUrl)); + Assertions.assertEquals(Instant.parse("2015-10-21T07:28:00Z"), e.getRetryAfter()); + } + + @ParameterizedTest + @ValueSource( + ints = { + HttpStatus.SC_MOVED_PERMANENTLY, + HttpStatus.SC_MOVED_TEMPORARILY, + HttpStatus.SC_TEMPORARY_REDIRECT, + HttpStatus.SC_PERMANENT_REDIRECT + }) + void followRedirects(int code) throws Exception { + // first redirect + this.mockServerClient + .when(HttpRequest.request().withMethod("GET").withPath("/")) + .respond( + HttpResponse.response() + .withStatusCode(code) + .withHeader( + HttpHeaders.LOCATION, + "http://localhost:" + + this.mockServerClient.getPort() + + "/redirected")); + + // second redirect + this.mockServerClient + .when(HttpRequest.request().withMethod("GET").withPath("/redirected")) + .respond( + HttpResponse.response() + .withStatusCode(code) + .withHeader( + HttpHeaders.LOCATION, + "http://localhost:" + + this.mockServerClient.getPort() + + "/redirected-2")); + + // final destination + this.mockServerClient + .when(HttpRequest.request().withMethod("GET").withPath("/redirected-2")) + .respond( + HttpResponse.response() + .withBody(feedContent) + .withContentType(MediaType.APPLICATION_ATOM_XML)); + + HttpResult result = getter.get(this.feedUrl); + Assertions.assertEquals( + "http://localhost:" + this.mockServerClient.getPort() + "/redirected-2", + result.urlAfterRedirect()); + } + + @Test + void dataTimeout() { + Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500)); + this.getter = + new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); + + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond(HttpResponse.response().withDelay(Delay.milliseconds(1000))); + + Assertions.assertThrows(SocketTimeoutException.class, () -> getter.get(this.feedUrl)); + } + + @Test + void connectTimeout() { + Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500)); + this.getter = + new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); + // try to connect to a non-routable address + // https://stackoverflow.com/a/904609 + Exception e = + Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1")); + Assertions.assertTrue( + e instanceof ConnectTimeoutException + // A NoRouteToHostException can also be thrown in some cases + // depending on the underlying network configuration + // https://github.com/Athou/commafeed/issues/1876 + || e instanceof NoRouteToHostException, + "Expected ConnectTimeoutException or NoRouteToHostException, but got: " + + e.getClass().getName()); + } + + @Test + void userAgent() throws Exception { + this.mockServerClient + .when( + HttpRequest.request() + .withMethod("GET") + .withHeader(HttpHeaders.USER_AGENT, "http-getter-test")) + .respond(HttpResponse.response().withBody("ok")); + + HttpResult result = getter.get(this.feedUrl); + Assertions.assertEquals("ok", new String(result.content())); + } + + @Test + void lastModifiedReturns304() { + this.mockServerClient + .when( + HttpRequest.request() + .withMethod("GET") + .withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456")) + .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); + + Assertions.assertThrows( + NotModifiedException.class, + () -> + getter.get( + HttpGetter.HttpRequest.builder(this.feedUrl) + .lastModified("123456") + .build())); + } + + @Test + void eTagReturns304() { + this.mockServerClient + .when( + HttpRequest.request() + .withMethod("GET") + .withHeader(HttpHeaders.IF_NONE_MATCH, "78910")) + .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); + + Assertions.assertThrows( + NotModifiedException.class, + () -> + getter.get( + HttpGetter.HttpRequest.builder(this.feedUrl) + .eTag("78910") + .build())); + } + + @Test + void ignoreCookie() { + AtomicInteger calls = new AtomicInteger(); + + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + req -> { + calls.incrementAndGet(); + + if (req.containsHeader(HttpHeaders.COOKIE)) { + throw new Exception("cookie should not be sent by the client"); + } + + return HttpResponse.response() + .withBody("ok") + .withHeader(HttpHeaders.SET_COOKIE, "foo=bar"); + }); + + Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl)); + Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl + "?foo=bar")); + Assertions.assertEquals(2, calls.get()); + } + + @Test + void cacheSubsequentCalls() throws Exception { + AtomicInteger calls = new AtomicInteger(); + + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + req -> { + calls.incrementAndGet(); + return HttpResponse.response().withBody("ok"); + }); + + HttpResult result = getter.get(this.feedUrl); + Assertions.assertEquals(result, getter.get(this.feedUrl)); + Assertions.assertEquals(1, calls.get()); + } + + @Test + void largeFeedWithContentLengthHeader() { + byte[] bytes = new byte[100000]; + Arrays.fill(bytes, (byte) 1); + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond(HttpResponse.response().withBody(bytes)); + + IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl)); + Assertions.assertEquals( + "Response size (100000 bytes) exceeds the maximum allowed size (10000 bytes)", + e.getMessage()); + } + + @Test + void largeFeedWithoutContentLengthHeader() { + byte[] bytes = new byte[100000]; + Arrays.fill(bytes, (byte) 1); + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + HttpResponse.response() + .withBody(bytes) + .withConnectionOptions( + ConnectionOptions.connectionOptions() + .withSuppressContentLengthHeader(true))); + + IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl)); + Assertions.assertEquals( + "Response size exceeds the maximum allowed size (10000 bytes)", e.getMessage()); + } + + @Test + void ignoreInvalidSsl() throws Exception { + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond(HttpResponse.response().withBody("ok")); + + HttpResult result = getter.get("https://localhost:" + this.mockServerClient.getPort()); + Assertions.assertEquals("ok", new String(result.content())); + } + + @Test + void doesNotUseUpgradeProtocolHeader() { + AtomicInteger calls = new AtomicInteger(); + + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + req -> { + calls.incrementAndGet(); + + if (req.containsHeader(HttpHeaders.UPGRADE)) { + throw new Exception( + "upgrade header should not be sent by the client"); + } + + return HttpResponse.response().withBody("ok"); + }); + + Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl)); + Assertions.assertEquals(1, calls.get()); + } + + @Nested + class Compression { + + private static final String ACCEPT_ENCODING = "gzip, deflate, br"; + + @Test + void gzip() throws Exception { + supportsCompression("gzip", GZIPOutputStream::new); + } + + @Test + void deflate() throws Exception { + supportsCompression("deflate", DeflaterOutputStream::new); + } + + void supportsCompression(String encoding, CompressionFunction compressionFunction) + throws Exception { + String body = "my body"; + + HttpGetterTest.this + .mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + req -> { + String acceptEncodingHeader = + req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING); + if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) { + throw new Exception( + "Wrong value in the Accept-Encoding header, should be '%s' but was '%s'" + .formatted( + ACCEPT_ENCODING, acceptEncodingHeader)); + } + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream compressionOutputStream = + compressionFunction.apply(output)) { + compressionOutputStream.write(body.getBytes()); + } + + return HttpResponse.response() + .withBody(output.toByteArray()) + .withHeader(HttpHeaders.CONTENT_ENCODING, encoding); + }); + + HttpResult result = getter.get(HttpGetterTest.this.feedUrl); + Assertions.assertEquals(body, new String(result.content())); + } + + @FunctionalInterface + public interface CompressionFunction { + OutputStream apply(OutputStream input) throws IOException; + } + } + + @Nested + class SchemeNotAllowed { + @Test + void file() { + Assertions.assertThrows( + HttpGetter.SchemeNotAllowedException.class, + () -> getter.get("file://localhost")); + } + + @Test + void ftp() { + Assertions.assertThrows( + HttpGetter.SchemeNotAllowedException.class, + () -> getter.get("ftp://localhost")); + } + } + + @Nested + class HostNotAllowed { + + @BeforeEach + void init() { + Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true); + getter = + new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class)); + } + + @Test + void localhost() { + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://localhost")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://127.0.0.1")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://2130706433")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01")); + } + + @Test + void zero() { + Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0.0.0.0")); + } + + @Test + void linkLocal() { + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://169.254.12.34")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://169.254.169.254")); + } + + @Test + void multicast() { + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://224.2.3.4")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://239.255.255.254")); + } + + @Test + void privateIpv4Ranges() { + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://10.0.0.1")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://172.16.0.1")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://192.168.0.1")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://100.64.0.1")); + } + + @Test + void privateIpv6Ranges() { + Assertions.assertThrows( + UnknownHostException.class, + () -> getter.get("http://[fe80::215:5dff:fe15:102]")); + Assertions.assertThrows( + UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]")); + } + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/UrlsTest.java b/commafeed-server/src/test/java/com/commafeed/backend/UrlsTest.java index 6b0cacfa..d7077e75 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/UrlsTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/UrlsTest.java @@ -5,81 +5,105 @@ import org.junit.jupiter.api.Test; class UrlsTest { - @Test - void testNormalization() { - String urla1 = "http://example.com/hello?a=1&b=2"; - String urla2 = "http://www.example.com/hello?a=1&b=2"; - String urla3 = "http://EXAmPLe.com/HELLo?a=1&b=2"; - String urla4 = "http://example.com/hello?b=2&a=1"; - String urla5 = "https://example.com/hello?a=1&b=2"; + @Test + void testNormalization() { + String urla1 = "http://example.com/hello?a=1&b=2"; + String urla2 = "http://www.example.com/hello?a=1&b=2"; + String urla3 = "http://EXAmPLe.com/HELLo?a=1&b=2"; + String urla4 = "http://example.com/hello?b=2&a=1"; + String urla5 = "https://example.com/hello?a=1&b=2"; - String urlb1 = "http://ftr.fivefilters.org/makefulltextfeed.php?url=http%3A%2F%2Ffeeds.howtogeek.com%2FHowToGeek&max=10&summary=1"; - String urlb2 = "http://ftr.fivefilters.org/makefulltextfeed.php?url=http://feeds.howtogeek.com/HowToGeek&max=10&summary=1"; + String urlb1 = + "http://ftr.fivefilters.org/makefulltextfeed.php?url=http%3A%2F%2Ffeeds.howtogeek.com%2FHowToGeek&max=10&summary=1"; + String urlb2 = + "http://ftr.fivefilters.org/makefulltextfeed.php?url=http://feeds.howtogeek.com/HowToGeek&max=10&summary=1"; - String urlc1 = "http://feeds.feedburner.com/Frandroid"; - String urlc2 = "http://feeds2.feedburner.com/frandroid"; - String urlc3 = "http://feedproxy.google.com/frandroid"; - String urlc4 = "http://feeds.feedburner.com/Frandroid/"; - String urlc5 = "http://feeds.feedburner.com/Frandroid?format=rss"; + String urlc1 = "http://feeds.feedburner.com/Frandroid"; + String urlc2 = "http://feeds2.feedburner.com/frandroid"; + String urlc3 = "http://feedproxy.google.com/frandroid"; + String urlc4 = "http://feeds.feedburner.com/Frandroid/"; + String urlc5 = "http://feeds.feedburner.com/Frandroid?format=rss"; - String urld1 = "http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds.feedburner.com/Frandroid"; - String urld2 = "http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds2.feedburner.com/Frandroid"; + String urld1 = + "http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds.feedburner.com/Frandroid"; + String urld2 = + "http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds2.feedburner.com/Frandroid"; - Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla2)); - Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla3)); - Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla4)); - Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla5)); + Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla2)); + Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla3)); + Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla4)); + Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla5)); - Assertions.assertEquals(Urls.normalize(urlb1), Urls.normalize(urlb2)); + Assertions.assertEquals(Urls.normalize(urlb1), Urls.normalize(urlb2)); - Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc2)); - Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc3)); - Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc4)); - Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc5)); + Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc2)); + Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc3)); + Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc4)); + Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc5)); - Assertions.assertNotEquals(Urls.normalize(urld1), Urls.normalize(urld2)); + Assertions.assertNotEquals(Urls.normalize(urld1), Urls.normalize(urld2)); + } - } + @Test + void testToAbsoluteUrl() { + String expected = "http://a.com/blog/entry/1"; - @Test - void testToAbsoluteUrl() { - String expected = "http://a.com/blog/entry/1"; + // usual cases + Assertions.assertEquals( + expected, + Urls.toAbsolute( + "http://a.com/blog/entry/1", "http://a.com/feed/", "http://a.com/feed/")); + Assertions.assertEquals( + expected, + Urls.toAbsolute( + "http://a.com/blog/entry/1", "http://a.com/feed", "http://a.com/feed")); - // usual cases - Assertions.assertEquals(expected, Urls.toAbsolute("http://a.com/blog/entry/1", "http://a.com/feed/", "http://a.com/feed/")); - Assertions.assertEquals(expected, Urls.toAbsolute("http://a.com/blog/entry/1", "http://a.com/feed", "http://a.com/feed")); + // relative links + Assertions.assertEquals( + expected, + Urls.toAbsolute("../blog/entry/1", "http://a.com/feed/", "http://a.com/feed/")); + Assertions.assertEquals( + expected, + Urls.toAbsolute("../blog/entry/1", "feed.xml", "http://a.com/feed/feed.xml")); - // relative links - Assertions.assertEquals(expected, Urls.toAbsolute("../blog/entry/1", "http://a.com/feed/", "http://a.com/feed/")); - Assertions.assertEquals(expected, Urls.toAbsolute("../blog/entry/1", "feed.xml", "http://a.com/feed/feed.xml")); + // root-relative links + Assertions.assertEquals( + expected, Urls.toAbsolute("/blog/entry/1", "/feed", "http://a.com/feed")); - // root-relative links - Assertions.assertEquals(expected, Urls.toAbsolute("/blog/entry/1", "/feed", "http://a.com/feed")); + // real cases + Assertions.assertEquals( + "https://github.com/erusev/parsedown/releases/tag/1.3.0", + Urls.toAbsolute( + "/erusev/parsedown/releases/tag/1.3.0", + "/erusev/parsedown/releases", + "https://github.com/erusev/parsedown/tags.atom")); + Assertions.assertEquals( + "http://ergoemacs.org/emacs/elisp_all_about_lines.html", + Urls.toAbsolute( + "elisp_all_about_lines.html", + "blog.xml", + "http://ergoemacs.org/emacs/blog.xml")); - // real cases - Assertions.assertEquals("https://github.com/erusev/parsedown/releases/tag/1.3.0", Urls.toAbsolute( - "/erusev/parsedown/releases/tag/1.3.0", "/erusev/parsedown/releases", "https://github.com/erusev/parsedown/tags.atom")); - Assertions.assertEquals("http://ergoemacs.org/emacs/elisp_all_about_lines.html", - Urls.toAbsolute("elisp_all_about_lines.html", "blog.xml", "http://ergoemacs.org/emacs/blog.xml")); + // invalid relative urls + Assertions.assertEquals( + "title:10001280", + Urls.toAbsolute( + "title:10001280", + "https://www.berliner-zeitung.de", + "https://www.berliner-zeitung.de/feed.xml")); + } - // invalid relative urls - Assertions.assertEquals("title:10001280", - Urls.toAbsolute("title:10001280", "https://www.berliner-zeitung.de", "https://www.berliner-zeitung.de/feed.xml")); + @Test + void testRemoveTrailingSlash() { + final String url = "http://localhost/"; + final String result = Urls.removeTrailingSlash(url); + Assertions.assertEquals("http://localhost", result); + } - } - - @Test - void testRemoveTrailingSlash() { - final String url = "http://localhost/"; - final String result = Urls.removeTrailingSlash(url); - Assertions.assertEquals("http://localhost", result); - } - - @Test - void testRemoveTrailingSlashLastSlashOnly() { - final String url = "http://localhost//"; - final String result = Urls.removeTrailingSlash(url); - Assertions.assertEquals("http://localhost/", result); - } - -} \ No newline at end of file + @Test + void testRemoveTrailingSlashLastSlashOnly() { + final String url = "http://localhost//"; + final String result = Urls.removeTrailingSlash(url); + Assertions.assertEquals("http://localhost/", result); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/favicon/FacebookFaviconFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/favicon/FacebookFaviconFetcherTest.java index 3c9bf6de..5317e872 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/favicon/FacebookFaviconFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/favicon/FacebookFaviconFetcherTest.java @@ -1,9 +1,10 @@ package com.commafeed.backend.favicon; -import java.time.Duration; - +import com.commafeed.backend.HttpGetter; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.model.Feed; import jakarta.ws.rs.core.MediaType; - +import java.time.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,67 +13,68 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.commafeed.backend.HttpGetter; -import com.commafeed.backend.HttpGetter.HttpResult; -import com.commafeed.backend.model.Feed; - @ExtendWith(MockitoExtension.class) class FacebookFaviconFetcherTest { - @Mock - private HttpGetter httpGetter; + @Mock private HttpGetter httpGetter; - private FacebookFaviconFetcher faviconFetcher; + private FacebookFaviconFetcher faviconFetcher; - @BeforeEach - void init() { - faviconFetcher = new FacebookFaviconFetcher(httpGetter); - } + @BeforeEach + void init() { + faviconFetcher = new FacebookFaviconFetcher(httpGetter); + } - @Test - void testFetchWithValidFacebookUrl() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://www.facebook.com/something?id=validUserId"); + @Test + void testFetchWithValidFacebookUrl() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://www.facebook.com/something?id=validUserId"); - byte[] iconBytes = new byte[1000]; - String contentType = "image/png"; + byte[] iconBytes = new byte[1000]; + String contentType = "image/png"; - HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://graph.facebook.com/validUserId/picture?type=square&height=16")).thenReturn(httpResult); + HttpResult httpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when( + httpGetter.get( + "https://graph.facebook.com/validUserId/picture?type=square&height=16")) + .thenReturn(httpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchWithNonFacebookUrl() { - Feed feed = new Feed(); - feed.setUrl("https://example.com"); + @Test + void testFetchWithNonFacebookUrl() { + Feed feed = new Feed(); + feed.setUrl("https://example.com"); - Assertions.assertNull(faviconFetcher.fetch(feed)); - Mockito.verifyNoInteractions(httpGetter); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + Mockito.verifyNoInteractions(httpGetter); + } - @Test - void testFetchWithFacebookUrlButNoUserId() { - Feed feed = new Feed(); - feed.setUrl("https://www.facebook.com/something"); + @Test + void testFetchWithFacebookUrlButNoUserId() { + Feed feed = new Feed(); + feed.setUrl("https://www.facebook.com/something"); - Assertions.assertNull(faviconFetcher.fetch(feed)); - Mockito.verifyNoInteractions(httpGetter); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + Mockito.verifyNoInteractions(httpGetter); + } - @Test - void testFetchWithHttpGetterException() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://www.facebook.com/something?id=validUserId"); + @Test + void testFetchWithHttpGetterException() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://www.facebook.com/something?id=validUserId"); - Mockito.when(httpGetter.get("https://graph.facebook.com/validUserId/picture?type=square&height=16")) - .thenThrow(new RuntimeException("Network error")); + Mockito.when( + httpGetter.get( + "https://graph.facebook.com/validUserId/picture?type=square&height=16")) + .thenThrow(new RuntimeException("Network error")); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } -} \ No newline at end of file + Assertions.assertNull(faviconFetcher.fetch(feed)); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/favicon/FeedFaviconFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/favicon/FeedFaviconFetcherTest.java index 7a543b7b..0d0757d5 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/favicon/FeedFaviconFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/favicon/FeedFaviconFetcherTest.java @@ -1,9 +1,10 @@ package com.commafeed.backend.favicon; -import java.time.Duration; - +import com.commafeed.backend.HttpGetter; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.model.Feed; import jakarta.ws.rs.core.MediaType; - +import java.time.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,58 +13,55 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.commafeed.backend.HttpGetter; -import com.commafeed.backend.HttpGetter.HttpResult; -import com.commafeed.backend.model.Feed; - @ExtendWith(MockitoExtension.class) class FeedFaviconFetcherTest { - @Mock - private HttpGetter httpGetter; + @Mock private HttpGetter httpGetter; - private FeedFaviconFetcher faviconFetcher; + private FeedFaviconFetcher faviconFetcher; - @BeforeEach - void init() { - faviconFetcher = new FeedFaviconFetcher(httpGetter); - } + @BeforeEach + void init() { + faviconFetcher = new FeedFaviconFetcher(httpGetter); + } - @Test - void testFetchWithNullIconUrl() { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); + @Test + void testFetchWithNullIconUrl() { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); - Assertions.assertNull(faviconFetcher.fetch(feed)); - Mockito.verifyNoInteractions(httpGetter); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + Mockito.verifyNoInteractions(httpGetter); + } - @Test - void testFetchWithValidIconUrl() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setIconUrl("https://example.com/icon.png"); + @Test + void testFetchWithValidIconUrl() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setIconUrl("https://example.com/icon.png"); - byte[] iconBytes = new byte[1000]; - String contentType = "image/png"; - HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(httpResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/png"; + HttpResult httpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(httpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchWithHttpGetterException() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setIconUrl("https://example.com/icon.png"); + @Test + void testFetchWithHttpGetterException() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setIconUrl("https://example.com/icon.png"); - Mockito.when(httpGetter.get("https://example.com/icon.png")).thenThrow(new RuntimeException("Network error")); + Mockito.when(httpGetter.get("https://example.com/icon.png")) + .thenThrow(new RuntimeException("Network error")); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/favicon/HtmlFaviconFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/favicon/HtmlFaviconFetcherTest.java index 5b3615e4..de95d7f3 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/favicon/HtmlFaviconFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/favicon/HtmlFaviconFetcherTest.java @@ -1,9 +1,10 @@ package com.commafeed.backend.favicon; -import java.time.Duration; - +import com.commafeed.backend.HttpGetter; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.model.Feed; import jakarta.ws.rs.core.MediaType; - +import java.time.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,111 +13,118 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.commafeed.backend.HttpGetter; -import com.commafeed.backend.HttpGetter.HttpResult; -import com.commafeed.backend.model.Feed; - @ExtendWith(MockitoExtension.class) class HtmlFaviconFetcherTest { - @Mock - private HttpGetter httpGetter; + @Mock private HttpGetter httpGetter; - private HtmlFaviconFetcher faviconFetcher; + private HtmlFaviconFetcher faviconFetcher; - @BeforeEach - void init() { - faviconFetcher = new HtmlFaviconFetcher(httpGetter); - } + @BeforeEach + void init() { + faviconFetcher = new HtmlFaviconFetcher(httpGetter); + } - @Test - void testFetchWithNullLink() { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); + @Test + void testFetchWithNullLink() { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); - Assertions.assertNull(faviconFetcher.fetch(feed)); - Mockito.verifyNoInteractions(httpGetter); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + Mockito.verifyNoInteractions(httpGetter); + } - @Test - void testFetchWithValidIconLink() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setLink("https://example.com"); + @Test + void testFetchWithValidIconLink() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setLink("https://example.com"); - String html = ""; - HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); + String html = + ""; + HttpResult pageResult = + new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); - byte[] iconBytes = new byte[1000]; - String contentType = "image/png"; - HttpResult iconResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/favicon.png")).thenReturn(iconResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/png"; + HttpResult iconResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/favicon.png")).thenReturn(iconResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchWithShortcutIconLink() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setLink("https://example.com"); + @Test + void testFetchWithShortcutIconLink() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setLink("https://example.com"); - String html = ""; - HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); + String html = + ""; + HttpResult pageResult = + new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); - byte[] iconBytes = new byte[1000]; - String contentType = "image/x-icon"; - HttpResult iconResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/shortcut-favicon.ico")).thenReturn(iconResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/x-icon"; + HttpResult iconResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/shortcut-favicon.ico")) + .thenReturn(iconResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchWithNoIconInPage() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setLink("https://example.com"); + @Test + void testFetchWithNoIconInPage() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setLink("https://example.com"); - String html = ""; - HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); + String html = ""; + HttpResult pageResult = + new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + } - @Test - void testFetchWithPageFetchException() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setLink("https://example.com"); + @Test + void testFetchWithPageFetchException() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setLink("https://example.com"); - Mockito.when(httpGetter.get("https://example.com")).thenThrow(new RuntimeException("Network error")); + Mockito.when(httpGetter.get("https://example.com")) + .thenThrow(new RuntimeException("Network error")); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + } - @Test - void testFetchWithIconFetchException() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); - feed.setLink("https://example.com"); + @Test + void testFetchWithIconFetchException() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); + feed.setLink("https://example.com"); - String html = ""; - HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); - Mockito.when(httpGetter.get("https://example.com/favicon.png")).thenThrow(new RuntimeException("Network error")); + String html = + ""; + HttpResult pageResult = + new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult); + Mockito.when(httpGetter.get("https://example.com/favicon.png")) + .thenThrow(new RuntimeException("Network error")); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/favicon/RootFaviconFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/favicon/RootFaviconFetcherTest.java index e44aefd0..0f77eb55 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/favicon/RootFaviconFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/favicon/RootFaviconFetcherTest.java @@ -1,9 +1,10 @@ package com.commafeed.backend.favicon; -import java.time.Duration; - +import com.commafeed.backend.HttpGetter; +import com.commafeed.backend.HttpGetter.HttpResult; +import com.commafeed.backend.model.Feed; import jakarta.ws.rs.core.MediaType; - +import java.time.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,66 +13,64 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.commafeed.backend.HttpGetter; -import com.commafeed.backend.HttpGetter.HttpResult; -import com.commafeed.backend.model.Feed; - @ExtendWith(MockitoExtension.class) class RootFaviconFetcherTest { - @Mock - private HttpGetter httpGetter; + @Mock private HttpGetter httpGetter; - private RootFaviconFetcher faviconFetcher; + private RootFaviconFetcher faviconFetcher; - @BeforeEach - void init() { - faviconFetcher = new RootFaviconFetcher(httpGetter); - } + @BeforeEach + void init() { + faviconFetcher = new RootFaviconFetcher(httpGetter); + } - @Test - void testFetchUsesLinkWhenAvailable() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://feeds.example.com/feed"); - feed.setLink("https://www.example.com/blog"); + @Test + void testFetchUsesLinkWhenAvailable() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://feeds.example.com/feed"); + feed.setLink("https://www.example.com/blog"); - byte[] iconBytes = new byte[1000]; - String contentType = "image/x-icon"; - HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://www.example.com/favicon.ico")).thenReturn(httpResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/x-icon"; + HttpResult httpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://www.example.com/favicon.ico")).thenReturn(httpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchFallsBackToUrlWhenLinkIsNull() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed.xml"); + @Test + void testFetchFallsBackToUrlWhenLinkIsNull() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed.xml"); - byte[] iconBytes = new byte[1000]; - String contentType = "image/x-icon"; - HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/favicon.ico")).thenReturn(httpResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/x-icon"; + HttpResult httpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/favicon.ico")).thenReturn(httpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchWithHttpGetterException() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed.xml"); - feed.setLink("https://example.com"); + @Test + void testFetchWithHttpGetterException() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed.xml"); + feed.setLink("https://example.com"); - Mockito.when(httpGetter.get("https://example.com/favicon.ico")).thenThrow(new RuntimeException("Network error")); + Mockito.when(httpGetter.get("https://example.com/favicon.ico")) + .thenThrow(new RuntimeException("Network error")); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/favicon/YoutubeFaviconFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/favicon/YoutubeFaviconFetcherTest.java index d9525a9b..821fee6a 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/favicon/YoutubeFaviconFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/favicon/YoutubeFaviconFetcherTest.java @@ -1,19 +1,5 @@ package com.commafeed.backend.favicon; -import java.io.IOException; -import java.time.Duration; -import java.util.Optional; - -import jakarta.ws.rs.core.MediaType; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; @@ -21,170 +7,208 @@ 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.ws.rs.core.MediaType; +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class YoutubeFaviconFetcherTest { - @Mock - private HttpGetter httpGetter; + @Mock private HttpGetter httpGetter; - @Mock - private CommaFeedConfiguration config; + @Mock private CommaFeedConfiguration config; - @Mock - private ObjectMapper objectMapper; + @Mock private ObjectMapper objectMapper; - private YoutubeFaviconFetcher faviconFetcher; + private YoutubeFaviconFetcher faviconFetcher; - @BeforeEach - void init() { - faviconFetcher = new YoutubeFaviconFetcher(httpGetter, config, objectMapper); - } + @BeforeEach + void init() { + faviconFetcher = new YoutubeFaviconFetcher(httpGetter, config, objectMapper); + } - @Test - void testFetchWithNonYoutubeUrl() { - Feed feed = new Feed(); - feed.setUrl("https://example.com/feed"); + @Test + void testFetchWithNonYoutubeUrl() { + Feed feed = new Feed(); + feed.setUrl("https://example.com/feed"); - Assertions.assertNull(faviconFetcher.fetch(feed)); - Mockito.verifyNoInteractions(httpGetter, objectMapper); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + Mockito.verifyNoInteractions(httpGetter, objectMapper); + } - @Test - void testFetchWithNoGoogleAuthKey() { - Feed feed = new Feed(); - feed.setUrl("https://youtube.com/feeds/videos.xml?user=someUser"); + @Test + void testFetchWithNoGoogleAuthKey() { + Feed feed = new Feed(); + feed.setUrl("https://youtube.com/feeds/videos.xml?user=someUser"); - Mockito.when(config.googleAuthKey()).thenReturn(Optional.empty()); + Mockito.when(config.googleAuthKey()).thenReturn(Optional.empty()); - Assertions.assertNull(faviconFetcher.fetch(feed)); - Mockito.verify(config).googleAuthKey(); - Mockito.verifyNoInteractions(httpGetter, objectMapper); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + Mockito.verify(config).googleAuthKey(); + Mockito.verifyNoInteractions(httpGetter, objectMapper); + } - @Test - void testFetchForUser() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser"); + @Test + void testFetchForUser() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser"); - Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); + Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); - byte[] apiResponse = """ - {"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""".getBytes(); - HttpResult apiHttpResult = new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser")) - .thenReturn(apiHttpResult); + byte[] apiResponse = + """ + {"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""" + .getBytes(); + HttpResult apiHttpResult = + new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO); + Mockito.when( + httpGetter.get( + "https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser")) + .thenReturn(apiHttpResult); - JsonNode jsonNode = new ObjectMapper().readTree(apiResponse); - Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode); + JsonNode jsonNode = new ObjectMapper().readTree(apiResponse); + Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode); - byte[] iconBytes = new byte[1000]; - String contentType = "image/png"; - HttpResult iconHttpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/png"; + HttpResult iconHttpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchForChannel() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://youtube.com/feeds/videos.xml?channel_id=testChannelId"); + @Test + void testFetchForChannel() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://youtube.com/feeds/videos.xml?channel_id=testChannelId"); - Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); + Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); - byte[] apiResponse = """ - {"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""".getBytes(); - HttpResult apiHttpResult = new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId")) - .thenReturn(apiHttpResult); + byte[] apiResponse = + """ + {"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""" + .getBytes(); + HttpResult apiHttpResult = + new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO); + Mockito.when( + httpGetter.get( + "https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId")) + .thenReturn(apiHttpResult); - JsonNode jsonNode = new ObjectMapper().readTree(apiResponse); - Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode); + JsonNode jsonNode = new ObjectMapper().readTree(apiResponse); + Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode); - byte[] iconBytes = new byte[1000]; - String contentType = "image/png"; - HttpResult iconHttpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/png"; + HttpResult iconHttpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchForPlaylist() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://youtube.com/feeds/videos.xml?playlist_id=testPlaylistId"); + @Test + void testFetchForPlaylist() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://youtube.com/feeds/videos.xml?playlist_id=testPlaylistId"); - Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); + Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); - byte[] playlistResponse = """ - {"items":[{"snippet":{"channelId":"testChannelId"}}]}""".getBytes(); - HttpResult playlistHttpResult = new HttpResult(playlistResponse, "application/json", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/playlists?part=snippet&key=test-api-key&id=testPlaylistId")) - .thenReturn(playlistHttpResult); + byte[] playlistResponse = + """ + {"items":[{"snippet":{"channelId":"testChannelId"}}]}""" + .getBytes(); + HttpResult playlistHttpResult = + new HttpResult( + playlistResponse, "application/json", null, null, null, Duration.ZERO); + Mockito.when( + httpGetter.get( + "https://www.googleapis.com/youtube/v3/playlists?part=snippet&key=test-api-key&id=testPlaylistId")) + .thenReturn(playlistHttpResult); - JsonNode playlistJsonNode = new ObjectMapper().readTree(playlistResponse); - Mockito.when(objectMapper.readTree(playlistResponse)).thenReturn(playlistJsonNode); + JsonNode playlistJsonNode = new ObjectMapper().readTree(playlistResponse); + Mockito.when(objectMapper.readTree(playlistResponse)).thenReturn(playlistJsonNode); - byte[] channelResponse = """ - {"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""".getBytes(); - HttpResult channelHttpResult = new HttpResult(channelResponse, "application/json", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId")) - .thenReturn(channelHttpResult); + byte[] channelResponse = + """ + {"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""" + .getBytes(); + HttpResult channelHttpResult = + new HttpResult( + channelResponse, "application/json", null, null, null, Duration.ZERO); + Mockito.when( + httpGetter.get( + "https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId")) + .thenReturn(channelHttpResult); - JsonNode channelJsonNode = new ObjectMapper().readTree(channelResponse); - Mockito.when(objectMapper.readTree(channelResponse)).thenReturn(channelJsonNode); + JsonNode channelJsonNode = new ObjectMapper().readTree(channelResponse); + Mockito.when(objectMapper.readTree(channelResponse)).thenReturn(channelJsonNode); - byte[] iconBytes = new byte[1000]; - String contentType = "image/png"; - HttpResult iconHttpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult); + byte[] iconBytes = new byte[1000]; + String contentType = "image/png"; + HttpResult iconHttpResult = + new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO); + Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult); - Favicon result = faviconFetcher.fetch(feed); + Favicon result = faviconFetcher.fetch(feed); - Assertions.assertNotNull(result); - Assertions.assertEquals(iconBytes, result.icon()); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); - } + Assertions.assertNotNull(result); + Assertions.assertEquals(iconBytes, result.icon()); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType))); + } - @Test - void testFetchWithHttpGetterException() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser"); + @Test + void testFetchWithHttpGetterException() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser"); - Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); + Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); - Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser")) - .thenThrow(new IOException("Network error")); + Mockito.when( + httpGetter.get( + "https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser")) + .thenThrow(new IOException("Network error")); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } + Assertions.assertNull(faviconFetcher.fetch(feed)); + } - @Test - void testFetchWithEmptyApiResponse() throws Exception { - Feed feed = new Feed(); - feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser"); + @Test + void testFetchWithEmptyApiResponse() throws Exception { + Feed feed = new Feed(); + feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser"); - Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); + Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key")); - byte[] apiResponse = "{}".getBytes(); - HttpResult apiHttpResult = new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO); - Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser")) - .thenReturn(apiHttpResult); + byte[] apiResponse = "{}".getBytes(); + HttpResult apiHttpResult = + new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO); + Mockito.when( + httpGetter.get( + "https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser")) + .thenReturn(apiHttpResult); - JsonNode jsonNode = Mockito.mock(JsonNode.class); - Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode); - Mockito.when(jsonNode.at(Mockito.any(JsonPointer.class))).thenReturn(jsonNode); - Mockito.when(jsonNode.isMissingNode()).thenReturn(true); + JsonNode jsonNode = Mockito.mock(JsonNode.class); + Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode); + Mockito.when(jsonNode.at(Mockito.any(JsonPointer.class))).thenReturn(jsonNode); + Mockito.when(jsonNode.isMissingNode()).thenReturn(true); - Assertions.assertNull(faviconFetcher.fetch(feed)); - } -} \ No newline at end of file + Assertions.assertNull(faviconFetcher.fetch(feed)); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java index 44b1b4fd..7298d930 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java @@ -1,17 +1,5 @@ package com.commafeed.backend.feed; -import java.time.Duration; -import java.time.Instant; -import java.util.List; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import com.commafeed.backend.Digests; import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; @@ -21,63 +9,94 @@ 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 java.time.Duration; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class FeedFetcherTest { - @Mock - private FeedParser parser; + @Mock private FeedParser parser; - @Mock - private HttpGetter getter; + @Mock private HttpGetter getter; - @Mock - private FeedURLProvider urlProvider; + @Mock private FeedURLProvider urlProvider; - private FeedFetcher fetcher; + private FeedFetcher fetcher; - @BeforeEach - void init() { - fetcher = new FeedFetcher(parser, getter, List.of(urlProvider)); - } + @BeforeEach + void init() { + fetcher = new FeedFetcher(parser, getter, List.of(urlProvider)); + } - @Test - void findsUrlInPage() throws Exception { - String htmlUrl = "https://aaa.com"; - byte[] html = "html".getBytes(); - Mockito.when(getter.get(HttpGetter.HttpRequest.builder(htmlUrl).build())) - .thenReturn(new HttpResult(html, "text/html", null, null, htmlUrl, Duration.ZERO)); - Mockito.when(parser.parse(htmlUrl, html)).thenThrow(new FeedParsingException("invalid feed")); + @Test + void findsUrlInPage() throws Exception { + String htmlUrl = "https://aaa.com"; + byte[] html = "html".getBytes(); + Mockito.when(getter.get(HttpGetter.HttpRequest.builder(htmlUrl).build())) + .thenReturn(new HttpResult(html, "text/html", null, null, htmlUrl, Duration.ZERO)); + Mockito.when(parser.parse(htmlUrl, html)) + .thenThrow(new FeedParsingException("invalid feed")); - String feedUrl = "https://bbb.com/feed"; - byte[] feed = "feed".getBytes(); - Mockito.when(getter.get(HttpGetter.HttpRequest.builder(feedUrl).build())) - .thenReturn(new HttpResult(feed, "application/atom+xml", null, null, feedUrl, Duration.ZERO)); - Mockito.when(parser.parse(feedUrl, feed)).thenReturn(new FeedParserResult("title", "link", "iconUrl", null, null, null, null)); + String feedUrl = "https://bbb.com/feed"; + byte[] feed = "feed".getBytes(); + Mockito.when(getter.get(HttpGetter.HttpRequest.builder(feedUrl).build())) + .thenReturn( + new HttpResult( + feed, "application/atom+xml", null, null, feedUrl, Duration.ZERO)); + Mockito.when(parser.parse(feedUrl, feed)) + .thenReturn( + new FeedParserResult("title", "link", "iconUrl", null, null, null, null)); - Mockito.when(urlProvider.get(htmlUrl, new String(html))).thenReturn(List.of(feedUrl)); + Mockito.when(urlProvider.get(htmlUrl, new String(html))).thenReturn(List.of(feedUrl)); - FeedFetcherResult result = fetcher.fetch(htmlUrl, true, null, null, null, null); - Assertions.assertEquals("title", result.feed().title()); - } + FeedFetcherResult result = fetcher.fetch(htmlUrl, true, null, null, null, null); + Assertions.assertEquals("title", result.feed().title()); + } - @Test - void updatesHeaderWhenContentDitNotChange() throws Exception { - String url = "https://aaa.com"; - String lastModified = "last-modified-1"; - String etag = "etag-1"; - byte[] content = "content".getBytes(); - String lastContentHash = Digests.sha1Hex(content); + @Test + void updatesHeaderWhenContentDitNotChange() throws Exception { + String url = "https://aaa.com"; + String lastModified = "last-modified-1"; + String etag = "etag-1"; + byte[] content = "content".getBytes(); + String lastContentHash = Digests.sha1Hex(content); - Mockito.when(getter.get(HttpGetter.HttpRequest.builder(url).lastModified(lastModified).eTag(etag).build())) - .thenReturn(new HttpResult(content, "content-type", "last-modified-2", "etag-2", null, Duration.ZERO)); + Mockito.when( + getter.get( + HttpGetter.HttpRequest.builder(url) + .lastModified(lastModified) + .eTag(etag) + .build())) + .thenReturn( + new HttpResult( + content, + "content-type", + "last-modified-2", + "etag-2", + null, + Duration.ZERO)); - NotModifiedException e = Assertions.assertThrows(NotModifiedException.class, - () -> fetcher.fetch(url, false, lastModified, etag, Instant.now(), lastContentHash)); - - Assertions.assertEquals("last-modified-2", e.getNewLastModifiedHeader()); - Assertions.assertEquals("etag-2", e.getNewEtagHeader()); - - } + NotModifiedException e = + Assertions.assertThrows( + NotModifiedException.class, + () -> + fetcher.fetch( + url, + false, + lastModified, + etag, + Instant.now(), + lastContentHash)); + Assertions.assertEquals("last-modified-2", e.getNewLastModifiedHeader()); + Assertions.assertEquals("etag-2", e.getNewEtagHeader()); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculatorTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculatorTest.java index 37a22a89..fdd0e134 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculatorTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculatorTest.java @@ -1,9 +1,10 @@ package com.commafeed.backend.feed; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling; import java.time.Duration; import java.time.Instant; import java.time.InstantSource; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -15,264 +16,289 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling; - @ExtendWith(MockitoExtension.class) class FeedRefreshIntervalCalculatorTest { - private static final Instant NOW = Instant.now(); - private static final Duration DEFAULT_INTERVAL = Duration.ofHours(1); - private static final Duration MAX_INTERVAL = Duration.ofDays(1); + private static final Instant NOW = Instant.now(); + private static final Duration DEFAULT_INTERVAL = Duration.ofHours(1); + private static final Duration MAX_INTERVAL = Duration.ofDays(1); - @Mock - private InstantSource instantSource; + @Mock private InstantSource instantSource; - @Mock - private CommaFeedConfiguration config; + @Mock private CommaFeedConfiguration config; - @Mock - private FeedRefreshErrorHandling errorHandling; + @Mock private FeedRefreshErrorHandling errorHandling; - private FeedRefreshIntervalCalculator calculator; + private FeedRefreshIntervalCalculator calculator; - @BeforeEach - void setUp() { - Mockito.when(instantSource.instant()).thenReturn(NOW); - Mockito.when(config.feedRefresh()).thenReturn(Mockito.mock(CommaFeedConfiguration.FeedRefresh.class)); - Mockito.when(config.feedRefresh().interval()).thenReturn(DEFAULT_INTERVAL); - Mockito.when(config.feedRefresh().maxInterval()).thenReturn(MAX_INTERVAL); - Mockito.when(config.feedRefresh().errors()).thenReturn(errorHandling); + @BeforeEach + void setUp() { + Mockito.when(instantSource.instant()).thenReturn(NOW); + Mockito.when(config.feedRefresh()) + .thenReturn(Mockito.mock(CommaFeedConfiguration.FeedRefresh.class)); + Mockito.when(config.feedRefresh().interval()).thenReturn(DEFAULT_INTERVAL); + Mockito.when(config.feedRefresh().maxInterval()).thenReturn(MAX_INTERVAL); + Mockito.when(config.feedRefresh().errors()).thenReturn(errorHandling); - calculator = new FeedRefreshIntervalCalculator(config, instantSource); - } + calculator = new FeedRefreshIntervalCalculator(config, instantSource); + } - @Nested - class FetchSuccess { + @Nested + class FetchSuccess { - @Nested - class EmpiricalDisabled { - @ParameterizedTest - @ValueSource(longs = { 0, 1, 300, 86400000L }) - void withoutValidFor(long averageEntryInterval) { - // averageEntryInterval is ignored when empirical is disabled - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), averageEntryInterval, Duration.ZERO); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } + @Nested + class EmpiricalDisabled { + @ParameterizedTest + @ValueSource(longs = {0, 1, 300, 86400000L}) + void withoutValidFor(long averageEntryInterval) { + // averageEntryInterval is ignored when empirical is disabled + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), averageEntryInterval, Duration.ZERO); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } - @Test - void withValidForGreaterThanMaxInterval() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.plusDays(1)); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } + @Test + void withValidForGreaterThanMaxInterval() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.plusDays(1)); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } - @Test - void withValidForLowerThanMaxInterval() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.minusSeconds(1)); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL).minusSeconds(1), result); - } - } + @Test + void withValidForLowerThanMaxInterval() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.minusSeconds(1)); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL).minusSeconds(1), result); + } + } - @Nested - class EmpiricalEnabled { - @BeforeEach - void setUp() { - Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true); - calculator = new FeedRefreshIntervalCalculator(config, instantSource); - } + @Nested + class EmpiricalEnabled { + @BeforeEach + void setUp() { + Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true); + calculator = new FeedRefreshIntervalCalculator(config, instantSource); + } - @Test - void withNullPublishedDate() { - Instant result = calculator.onFetchSuccess(null, 1L, Duration.ZERO); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } + @Test + void withNullPublishedDate() { + Instant result = calculator.onFetchSuccess(null, 1L, Duration.ZERO); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } - @Test - void with31DaysOldPublishedDate() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(31)), 1L, Duration.ZERO); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } + @Test + void with31DaysOldPublishedDate() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(31)), 1L, Duration.ZERO); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } - @Test - void with15DaysOldPublishedDate() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(15)), 1L, Duration.ZERO); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result); - } + @Test + void with15DaysOldPublishedDate() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(15)), 1L, Duration.ZERO); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result); + } - @Test - void with8DaysOldPublishedDate() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(8)), 1L, Duration.ZERO); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); - } + @Test + void with8DaysOldPublishedDate() { + Instant result = + calculator.onFetchSuccess(NOW.minus(Duration.ofDays(8)), 1L, Duration.ZERO); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); + } - @Nested - class FiveDaysOld { - @Test - void averageBetweenBounds() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis(), - Duration.ZERO); - Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result); - } + @Nested + class FiveDaysOld { + @Test + void averageBetweenBounds() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), + Duration.ofHours(4).toMillis(), + Duration.ZERO); + Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result); + } - @Test - void averageBelowMinimum() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 10L, Duration.ZERO); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } + @Test + void averageBelowMinimum() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), 10L, Duration.ZERO); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } - @Test - void averageAboveMaximum() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE, Duration.ZERO); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); - } + @Test + void averageAboveMaximum() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE, Duration.ZERO); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); + } - @Test - void noAverage() { - Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), null, Duration.ZERO); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } - } - } - } + @Test + void noAverage() { + Instant result = + calculator.onFetchSuccess( + NOW.minus(Duration.ofDays(5)), null, Duration.ZERO); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } + } + } + } - @Nested - class FeedNotModified { + @Nested + class FeedNotModified { - @Nested - class EmpiricalDisabled { - @ParameterizedTest - @ValueSource(longs = { 0, 1, 300, 86400000L }) - void withoutValidFor(long averageEntryInterval) { - // averageEntryInterval is ignored when empirical is disabled - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), averageEntryInterval); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } - } + @Nested + class EmpiricalDisabled { + @ParameterizedTest + @ValueSource(longs = {0, 1, 300, 86400000L}) + void withoutValidFor(long averageEntryInterval) { + // averageEntryInterval is ignored when empirical is disabled + Instant result = + calculator.onFeedNotModified( + NOW.minus(Duration.ofDays(5)), averageEntryInterval); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } + } - @Nested - class EmpiricalEnabled { - @BeforeEach - void setUp() { - Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true); - calculator = new FeedRefreshIntervalCalculator(config, instantSource); - } + @Nested + class EmpiricalEnabled { + @BeforeEach + void setUp() { + Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true); + calculator = new FeedRefreshIntervalCalculator(config, instantSource); + } - @Test - void withNullPublishedDate() { - Instant result = calculator.onFeedNotModified(null, 1L); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } + @Test + void withNullPublishedDate() { + Instant result = calculator.onFeedNotModified(null, 1L); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } - @Test - void with31DaysOldPublishedDate() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(31)), 1L); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } + @Test + void with31DaysOldPublishedDate() { + Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(31)), 1L); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } - @Test - void with15DaysOldPublishedDate() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(15)), 1L); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result); - } + @Test + void with15DaysOldPublishedDate() { + Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(15)), 1L); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result); + } - @Test - void with8DaysOldPublishedDate() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(8)), 1L); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); - } + @Test + void with8DaysOldPublishedDate() { + Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(8)), 1L); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); + } - @Nested - class FiveDaysOld { - @Test - void averageBetweenBounds() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis()); - Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result); - } + @Nested + class FiveDaysOld { + @Test + void averageBetweenBounds() { + Instant result = + calculator.onFeedNotModified( + NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis()); + Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result); + } - @Test - void averageBelowMinimum() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), 10L); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } + @Test + void averageBelowMinimum() { + Instant result = + calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), 10L); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } - @Test - void averageAboveMaximum() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); - } + @Test + void averageAboveMaximum() { + Instant result = + calculator.onFeedNotModified( + NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result); + } - @Test - void noAverage() { - Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), null); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } - } - } - } + @Test + void noAverage() { + Instant result = + calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), null); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } + } + } + } - @Nested - class FetchError { - @BeforeEach - void setUp() { - Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3); - } + @Nested + class FetchError { + @BeforeEach + void setUp() { + Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3); + } - @Test - void lowErrorCount() { - Instant result = calculator.onFetchError(1); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } + @Test + void lowErrorCount() { + Instant result = calculator.onFetchError(1); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } - @Test - void highErrorCount() { - Mockito.when(config.feedRefresh().errors().backoffInterval()).thenReturn(Duration.ofHours(1)); + @Test + void highErrorCount() { + Mockito.when(config.feedRefresh().errors().backoffInterval()) + .thenReturn(Duration.ofHours(1)); - Instant result = calculator.onFetchError(5); - Assertions.assertEquals(NOW.plus(Duration.ofHours(3)), result); - } + Instant result = calculator.onFetchError(5); + Assertions.assertEquals(NOW.plus(Duration.ofHours(3)), result); + } - @Test - void veryHighErrorCount() { - Mockito.when(config.feedRefresh().errors().backoffInterval()).thenReturn(Duration.ofHours(1)); + @Test + void veryHighErrorCount() { + Mockito.when(config.feedRefresh().errors().backoffInterval()) + .thenReturn(Duration.ofHours(1)); - Instant result = calculator.onFetchError(100000); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } - } + Instant result = calculator.onFetchError(100000); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } + } - @Nested - class TooManyRequests { + @Nested + class TooManyRequests { - @BeforeEach - void setUp() { - Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3); - } + @BeforeEach + void setUp() { + Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3); + } - @Test - void withRetryAfterZero() { - Instant result = calculator.onTooManyRequests(NOW, 1); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } + @Test + void withRetryAfterZero() { + Instant result = calculator.onTooManyRequests(NOW, 1); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } - @Test - void withRetryAfterLowerThanInterval() { - Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.minusSeconds(10)); - Instant result = calculator.onTooManyRequests(retryAfter, 1); - Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); - } + @Test + void withRetryAfterLowerThanInterval() { + Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.minusSeconds(10)); + Instant result = calculator.onTooManyRequests(retryAfter, 1); + Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result); + } - @Test - void withRetryAfterBetweenBounds() { - Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.plusSeconds(10)); - Instant result = calculator.onTooManyRequests(retryAfter, 1); - Assertions.assertEquals(retryAfter, result); - } + @Test + void withRetryAfterBetweenBounds() { + Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.plusSeconds(10)); + Instant result = calculator.onTooManyRequests(retryAfter, 1); + Assertions.assertEquals(retryAfter, result); + } - @Test - void withRetryAfterGreaterThanMaxInterval() { - Instant retryAfter = NOW.plus(MAX_INTERVAL.plusSeconds(10)); - Instant result = calculator.onTooManyRequests(retryAfter, 1); - Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); - } - } -} \ No newline at end of file + @Test + void withRetryAfterGreaterThanMaxInterval() { + Instant retryAfter = NOW.plus(MAX_INTERVAL.plusSeconds(10)); + Instant result = calculator.onTooManyRequests(retryAfter, 1); + Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result); + } + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java index 3e31d9f0..4297af31 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedUtilsTest.java @@ -1,41 +1,41 @@ package com.commafeed.backend.feed; -import java.time.Instant; -import java.util.Date; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - import com.commafeed.frontend.model.Entry; import com.rometools.rome.feed.synd.SyndEntry; +import java.time.Instant; +import java.util.Date; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; class FeedUtilsTest { - @Test - void asRss() { - Entry entry = new Entry(); - entry.setId("1"); - entry.setGuid("guid-1"); - entry.setTitle("Test Entry"); - entry.setContent("This is a test entry content."); - entry.setCategories("test,example"); - entry.setRtl(false); - entry.setAuthor("Author Name"); - entry.setEnclosureUrl("http://example.com/enclosure.mp3"); - entry.setEnclosureType("audio/mpeg"); - entry.setDate(Instant.ofEpochSecond(1)); - entry.setUrl("http://example.com/test-entry"); + @Test + void asRss() { + Entry entry = new Entry(); + entry.setId("1"); + entry.setGuid("guid-1"); + entry.setTitle("Test Entry"); + entry.setContent("This is a test entry content."); + entry.setCategories("test,example"); + entry.setRtl(false); + entry.setAuthor("Author Name"); + entry.setEnclosureUrl("http://example.com/enclosure.mp3"); + entry.setEnclosureType("audio/mpeg"); + entry.setDate(Instant.ofEpochSecond(1)); + entry.setUrl("http://example.com/test-entry"); - SyndEntry syndEntry = FeedUtils.asRss(entry); - Assertions.assertEquals("guid-1", syndEntry.getUri()); - Assertions.assertEquals("Test Entry", syndEntry.getTitle()); - Assertions.assertEquals("Author Name", syndEntry.getAuthor()); - Assertions.assertEquals(1, syndEntry.getContents().size()); - Assertions.assertEquals("This is a test entry content.", syndEntry.getContents().getFirst().getValue()); - Assertions.assertEquals(1, syndEntry.getEnclosures().size()); - Assertions.assertEquals("http://example.com/enclosure.mp3", syndEntry.getEnclosures().getFirst().getUrl()); - Assertions.assertEquals("audio/mpeg", syndEntry.getEnclosures().getFirst().getType()); - Assertions.assertEquals("http://example.com/test-entry", syndEntry.getLink()); - Assertions.assertEquals(Date.from(Instant.ofEpochSecond(1)), syndEntry.getPublishedDate()); - } -} \ No newline at end of file + SyndEntry syndEntry = FeedUtils.asRss(entry); + Assertions.assertEquals("guid-1", syndEntry.getUri()); + Assertions.assertEquals("Test Entry", syndEntry.getTitle()); + Assertions.assertEquals("Author Name", syndEntry.getAuthor()); + Assertions.assertEquals(1, syndEntry.getContents().size()); + Assertions.assertEquals( + "This is a test entry content.", syndEntry.getContents().getFirst().getValue()); + Assertions.assertEquals(1, syndEntry.getEnclosures().size()); + Assertions.assertEquals( + "http://example.com/enclosure.mp3", syndEntry.getEnclosures().getFirst().getUrl()); + Assertions.assertEquals("audio/mpeg", syndEntry.getEnclosures().getFirst().getType()); + Assertions.assertEquals("http://example.com/test-entry", syndEntry.getLink()); + Assertions.assertEquals(Date.from(Instant.ofEpochSecond(1)), syndEntry.getPublishedDate()); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/ImageProxyUrlTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/ImageProxyUrlTest.java index 283e00ff..4c7cb0e5 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/ImageProxyUrlTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/ImageProxyUrlTest.java @@ -8,56 +8,56 @@ import org.junit.jupiter.params.provider.ValueSource; class ImageProxyUrlTest { - @BeforeAll - static void init() { - ImageProxyUrl.generateKey(); - } + @BeforeAll + static void init() { + ImageProxyUrl.generateKey(); + } - @ParameterizedTest - @ValueSource( - strings = { - // simple URL - "http://example.com/image.jpg", + @ParameterizedTest + @ValueSource( + strings = { + // simple URL + "http://example.com/image.jpg", - // URL with query parameters - "https://example.com/image.png?width=100&height=200", + // URL with query parameters + "https://example.com/image.png?width=100&height=200", - // URL with special characters - "https://example.com/path/image.gif?param=value&other=test#fragment", + // URL with special characters + "https://example.com/path/image.gif?param=value&other=test#fragment", - // URL with non-ascii characters - "https://example.com/image-ñáéíóú.jpg", + // URL with non-ascii characters + "https://example.com/image-ñáéíóú.jpg", - // blank URL - "", + // blank URL + "", - // URL with port number - "http://localhost:8080/images/photo.jpg", + // URL with port number + "http://localhost:8080/images/photo.jpg", - // long URL - "https://very-long-domain-name-example.com/very/long/path/to/image/file/with/many/segments/image.jpg?param1=value1¶m2=value2¶m3=value3", + // long URL + "https://very-long-domain-name-example.com/very/long/path/to/image/file/with/many/segments/image.jpg?param1=value1¶m2=value2¶m3=value3", - // URL with mixed case - "HTTPS://EXAMPLE.COM/Image.JPG", + // URL with mixed case + "HTTPS://EXAMPLE.COM/Image.JPG", - // URL with port number - "https://example123.com/image123.jpg?id=456789", }) - void testEncodingDecoding(String originalUrl) { - String encoded = ImageProxyUrl.encode(originalUrl); - String decoded = ImageProxyUrl.decode(encoded); + // URL with port number + "https://example123.com/image123.jpg?id=456789", + }) + void testEncodingDecoding(String originalUrl) { + String encoded = ImageProxyUrl.encode(originalUrl); + String decoded = ImageProxyUrl.decode(encoded); - Assertions.assertEquals(originalUrl, decoded); - } + Assertions.assertEquals(originalUrl, decoded); + } - @Test - void encodeProducesDifferentResultForDifferentUrls() { - String url1 = "https://example.com/image1.jpg"; - String url2 = "https://example.com/image2.jpg"; + @Test + void encodeProducesDifferentResultForDifferentUrls() { + String url1 = "https://example.com/image1.jpg"; + String url2 = "https://example.com/image2.jpg"; - String encoded1 = ImageProxyUrl.encode(url1); - String encoded2 = ImageProxyUrl.encode(url2); + String encoded1 = ImageProxyUrl.encode(url1); + String encoded2 = ImageProxyUrl.encode(url2); - Assertions.assertNotEquals(encoded1, encoded2); - } - -} \ No newline at end of file + Assertions.assertNotEquals(encoded1, encoded2); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/EncodingDetectorTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/EncodingDetectorTest.java index 44f5b351..31a6769e 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/EncodingDetectorTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/EncodingDetectorTest.java @@ -5,15 +5,20 @@ import org.junit.jupiter.api.Test; class EncodingDetectorTest { - EncodingDetector encodingDetector = new EncodingDetector(); + EncodingDetector encodingDetector = new EncodingDetector(); - @Test - void testExtractDeclaredEncoding() { - Assertions.assertNull(encodingDetector.extractDeclaredEncoding("".getBytes())); - Assertions.assertNull(encodingDetector.extractDeclaredEncoding("".getBytes())); - Assertions.assertEquals("UTF-8", encodingDetector.extractDeclaredEncoding("".getBytes())); - Assertions.assertEquals("UTF-8", encodingDetector.extractDeclaredEncoding("".getBytes())); - Assertions.assertEquals("UTF-8", encodingDetector.extractDeclaredEncoding("".getBytes())); - } - -} \ No newline at end of file + @Test + void testExtractDeclaredEncoding() { + Assertions.assertNull(encodingDetector.extractDeclaredEncoding("".getBytes())); + Assertions.assertNull(encodingDetector.extractDeclaredEncoding("".getBytes())); + Assertions.assertEquals( + "UTF-8", + encodingDetector.extractDeclaredEncoding("".getBytes())); + Assertions.assertEquals( + "UTF-8", + encodingDetector.extractDeclaredEncoding("".getBytes())); + Assertions.assertEquals( + "UTF-8", + encodingDetector.extractDeclaredEncoding("".getBytes())); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/TextDirectionDetectorTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/TextDirectionDetectorTest.java index 97f3c5f2..a1282923 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/TextDirectionDetectorTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/TextDirectionDetectorTest.java @@ -5,49 +5,93 @@ import org.junit.jupiter.api.Test; class TextDirectionDetectorTest { - @Test - void testEstimateDirection() { - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("")); - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect(" ")); - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("! (...)")); - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("Pure Ascii content")); - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("-17.0%")); - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("http://foo/bar/")); - Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, - TextDirectionDetector.detect("http://foo/bar/?s=\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0" - + "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0" + "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, TextDirectionDetector.detect("\u05d0")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, TextDirectionDetector.detect("\u05d0")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("http://foo/bar/ \u05d0 http://foo2/bar2/ http://foo3/bar3/")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("\u05d0\u05d9\u05df \u05de\u05de\u05e9 " + "\u05de\u05d4 \u05dc\u05e8\u05d0\u05d5\u05ea: " - + "\u05dc\u05d0 \u05e6\u05d9\u05dc\u05de\u05ea\u05d9 " + "\u05d4\u05e8\u05d1\u05d4 \u05d5\u05d2\u05dd \u05d0" - + "\u05dd \u05d4\u05d9\u05d9\u05ea\u05d9 \u05de\u05e6\u05dc" + "\u05dd, \u05d4\u05d9\u05d4 \u05e9\u05dd")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("\u05db\u05d0\u05df - http://geek.co.il/gallery/v/2007-06" - + " - \u05d0\u05d9\u05df \u05de\u05de\u05e9 \u05de\u05d4 " + "\u05dc\u05e8\u05d0\u05d5\u05ea: \u05dc\u05d0 \u05e6" - + "\u05d9\u05dc\u05de\u05ea\u05d9 \u05d4\u05e8\u05d1\u05d4 " - + "\u05d5\u05d2\u05dd \u05d0\u05dd \u05d4\u05d9\u05d9\u05ea" - + "\u05d9 \u05de\u05e6\u05dc\u05dd, \u05d4\u05d9\u05d4 " - + "\u05e9\u05dd \u05d1\u05e2\u05d9\u05e7\u05e8 \u05d4\u05e8" + "\u05d1\u05d4 \u05d0\u05e0\u05e9\u05d9\u05dd. \u05de" - + "\u05d4 \u05e9\u05db\u05df - \u05d0\u05e4\u05e9\u05e8 " + "\u05dc\u05e0\u05e6\u05dc \u05d0\u05ea \u05d4\u05d4 " - + "\u05d3\u05d6\u05de\u05e0\u05d5\u05ea \u05dc\u05d4\u05e1" + "\u05ea\u05db\u05dc \u05e2\u05dc \u05db\u05de\u05d4 " - + "\u05ea\u05de\u05d5\u05e0\u05d5\u05ea \u05de\u05e9\u05e2" - + "\u05e9\u05e2\u05d5\u05ea \u05d9\u05e9\u05e0\u05d5\u05ea " + "\u05d9\u05d5\u05ea\u05e8 \u05e9\u05d9\u05e9 \u05dc" - + "\u05d9 \u05d1\u05d0\u05ea\u05e8")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("CAPTCHA \u05de\u05e9\u05d5\u05db\u05dc\u05dc " + "\u05de\u05d3\u05d9?")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("Yes Prime Minister \u05e2\u05d3\u05db\u05d5\u05df. " - + "\u05e9\u05d0\u05dc\u05d5 \u05d0\u05d5\u05ea\u05d9 " + "\u05de\u05d4 \u05d0\u05e0\u05d9 \u05e8\u05d5\u05e6" - + "\u05d4 \u05de\u05ea\u05e0\u05d4 \u05dc\u05d7\u05d2")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, TextDirectionDetector - .detect("17.4.02 \u05e9\u05e2\u05d4:13-20 .15-00 .\u05dc\u05d0 " + "\u05d4\u05d9\u05d9\u05ea\u05d9 \u05db\u05d0\u05df.")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("5710 5720 5730. \u05d4\u05d3\u05dc\u05ea. " + "\u05d4\u05e0\u05e9\u05d9\u05e7\u05d4")); - Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, - TextDirectionDetector.detect("\u05d4\u05d3\u05dc\u05ea http://www.google.com " + "http://www.gmail.com")); - } - -} \ No newline at end of file + @Test + void testEstimateDirection() { + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("")); + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect(" ")); + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, + TextDirectionDetector.detect("! (...)")); + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, + TextDirectionDetector.detect("Pure Ascii content")); + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, + TextDirectionDetector.detect("-17.0%")); + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, + TextDirectionDetector.detect("http://foo/bar/")); + Assertions.assertEquals( + TextDirectionDetector.Direction.LEFT_TO_RIGHT, + TextDirectionDetector.detect( + "http://foo/bar/?s=\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0" + + "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0" + + "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect("\u05d0")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect("\u05d0")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "http://foo/bar/ \u05d0 http://foo2/bar2/ http://foo3/bar3/")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "\u05d0\u05d9\u05df \u05de\u05de\u05e9 " + + "\u05de\u05d4 \u05dc\u05e8\u05d0\u05d5\u05ea: " + + "\u05dc\u05d0 \u05e6\u05d9\u05dc\u05de\u05ea\u05d9 " + + "\u05d4\u05e8\u05d1\u05d4 \u05d5\u05d2\u05dd \u05d0" + + "\u05dd \u05d4\u05d9\u05d9\u05ea\u05d9 \u05de\u05e6\u05dc" + + "\u05dd, \u05d4\u05d9\u05d4 \u05e9\u05dd")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "\u05db\u05d0\u05df - http://geek.co.il/gallery/v/2007-06" + + " - \u05d0\u05d9\u05df \u05de\u05de\u05e9 \u05de\u05d4 " + + "\u05dc\u05e8\u05d0\u05d5\u05ea: \u05dc\u05d0 \u05e6" + + "\u05d9\u05dc\u05de\u05ea\u05d9 \u05d4\u05e8\u05d1\u05d4 " + + "\u05d5\u05d2\u05dd \u05d0\u05dd \u05d4\u05d9\u05d9\u05ea" + + "\u05d9 \u05de\u05e6\u05dc\u05dd, \u05d4\u05d9\u05d4 " + + "\u05e9\u05dd \u05d1\u05e2\u05d9\u05e7\u05e8 \u05d4\u05e8" + + "\u05d1\u05d4 \u05d0\u05e0\u05e9\u05d9\u05dd. \u05de" + + "\u05d4 \u05e9\u05db\u05df - \u05d0\u05e4\u05e9\u05e8 " + + "\u05dc\u05e0\u05e6\u05dc \u05d0\u05ea \u05d4\u05d4 " + + "\u05d3\u05d6\u05de\u05e0\u05d5\u05ea \u05dc\u05d4\u05e1" + + "\u05ea\u05db\u05dc \u05e2\u05dc \u05db\u05de\u05d4 " + + "\u05ea\u05de\u05d5\u05e0\u05d5\u05ea \u05de\u05e9\u05e2" + + "\u05e9\u05e2\u05d5\u05ea \u05d9\u05e9\u05e0\u05d5\u05ea " + + "\u05d9\u05d5\u05ea\u05e8 \u05e9\u05d9\u05e9 \u05dc" + + "\u05d9 \u05d1\u05d0\u05ea\u05e8")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "CAPTCHA \u05de\u05e9\u05d5\u05db\u05dc\u05dc " + "\u05de\u05d3\u05d9?")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "Yes Prime Minister \u05e2\u05d3\u05db\u05d5\u05df. " + + "\u05e9\u05d0\u05dc\u05d5 \u05d0\u05d5\u05ea\u05d9 " + + "\u05de\u05d4 \u05d0\u05e0\u05d9 \u05e8\u05d5\u05e6" + + "\u05d4 \u05de\u05ea\u05e0\u05d4 \u05dc\u05d7\u05d2")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "17.4.02 \u05e9\u05e2\u05d4:13-20 .15-00 .\u05dc\u05d0 " + + "\u05d4\u05d9\u05d9\u05ea\u05d9 \u05db\u05d0\u05df.")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "5710 5720 5730. \u05d4\u05d3\u05dc\u05ea. " + + "\u05d4\u05e0\u05e9\u05d9\u05e7\u05d4")); + Assertions.assertEquals( + TextDirectionDetector.Direction.RIGHT_TO_LEFT, + TextDirectionDetector.detect( + "\u05d4\u05d3\u05dc\u05ea http://www.google.com " + + "http://www.gmail.com")); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/XMLCleanerTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/XMLCleanerTest.java index a1cba3b8..b6ae35af 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/XMLCleanerTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/parser/XMLCleanerTest.java @@ -6,266 +6,297 @@ import org.junit.jupiter.api.Test; class XMLCleanerTest { - XMLCleaner xmlCleaner = new XMLCleaner(); + XMLCleaner xmlCleaner = new XMLCleaner(); - @Nested - class RemoveCharactersBeforeFirstXmlTag { - @Test - void removesWhitespaceBeforeXmlTag() { - String xml = " \n\tcontent"; - Assertions.assertEquals("content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); - } + @Nested + class RemoveCharactersBeforeFirstXmlTag { + @Test + void removesWhitespaceBeforeXmlTag() { + String xml = " \n\tcontent"; + Assertions.assertEquals( + "content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); + } - @Test - void removesTextBeforeXmlTag() { - String xml = "some text herecontent"; - Assertions.assertEquals("content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); - } + @Test + void removesTextBeforeXmlTag() { + String xml = "some text herecontent"; + Assertions.assertEquals( + "content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); + } - @Test - void returnsUnchangedWhenStartsWithXmlTag() { - String xml = "content"; - Assertions.assertEquals("content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); - } + @Test + void returnsUnchangedWhenStartsWithXmlTag() { + String xml = "content"; + Assertions.assertEquals( + "content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); + } - @Test - void returnsNullWhenNoXmlTagFound() { - String xml = "no xml tags here"; - Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); - } + @Test + void returnsNullWhenNoXmlTagFound() { + String xml = "no xml tags here"; + Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); + } - @Test - void returnsNullWhenInputIsNull() { - Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(null)); - } + @Test + void returnsNullWhenInputIsNull() { + Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(null)); + } - @Test - void returnsNullWhenInputIsEmpty() { - Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag("")); - } + @Test + void returnsNullWhenInputIsEmpty() { + Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag("")); + } - @Test - void returnsNullWhenInputIsBlank() { - Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(" \n\t ")); - } + @Test + void returnsNullWhenInputIsBlank() { + Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(" \n\t ")); + } - @Test - void preservesMultipleXmlTags() { - String xml = "garbagecontent"; - Assertions.assertEquals("content", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); - } - } + @Test + void preservesMultipleXmlTags() { + String xml = "garbagecontent"; + Assertions.assertEquals( + "content", + xmlCleaner.removeCharactersBeforeFirstXmlTag(xml)); + } + } - @Nested - class RemoveInvalidXmlCharacters { - @Test - void removesNullCharacter() { - String xml = "content\u0000here"; - Assertions.assertEquals("contenthere", xmlCleaner.removeInvalidXmlCharacters(xml)); - } + @Nested + class RemoveInvalidXmlCharacters { + @Test + void removesNullCharacter() { + String xml = "content\u0000here"; + Assertions.assertEquals( + "contenthere", xmlCleaner.removeInvalidXmlCharacters(xml)); + } - @Test - void removesInvalidControlCharacters() { - String xml = "content\u0001\u0002\u0003here"; - Assertions.assertEquals("contenthere", xmlCleaner.removeInvalidXmlCharacters(xml)); - } + @Test + void removesInvalidControlCharacters() { + String xml = "content\u0001\u0002\u0003here"; + Assertions.assertEquals( + "contenthere", xmlCleaner.removeInvalidXmlCharacters(xml)); + } - @Test - void preservesValidXmlCharacters() { - String xml = "content with\ttab\nand newline"; - Assertions.assertEquals("content with\ttab\nand newline", xmlCleaner.removeInvalidXmlCharacters(xml)); - } + @Test + void preservesValidXmlCharacters() { + String xml = "content with\ttab\nand newline"; + Assertions.assertEquals( + "content with\ttab\nand newline", + xmlCleaner.removeInvalidXmlCharacters(xml)); + } - @Test - void preservesUnicodeCharacters() { - String xml = "café résumé 中文 العربية"; - Assertions.assertEquals("café résumé 中文 العربية", xmlCleaner.removeInvalidXmlCharacters(xml)); - } + @Test + void preservesUnicodeCharacters() { + String xml = "café résumé 中文 العربية"; + Assertions.assertEquals( + "café résumé 中文 العربية", + xmlCleaner.removeInvalidXmlCharacters(xml)); + } - @Test - void preservesEmojiCharacters() { - String xml = "🎮💪✅"; - Assertions.assertEquals("🎮💪✅", xmlCleaner.removeInvalidXmlCharacters(xml)); - } + @Test + void preservesEmojiCharacters() { + String xml = "🎮💪✅"; + Assertions.assertEquals( + "🎮💪✅", xmlCleaner.removeInvalidXmlCharacters(xml)); + } - @Test - void removesMultipleInvalidCharacters() { - String xml = "test\u0000test\u0001test\u0002test"; - Assertions.assertEquals("testtesttesttest", xmlCleaner.removeInvalidXmlCharacters(xml)); - } + @Test + void removesMultipleInvalidCharacters() { + String xml = "test\u0000test\u0001test\u0002test"; + Assertions.assertEquals("testtesttesttest", xmlCleaner.removeInvalidXmlCharacters(xml)); + } - @Test - void returnsNullWhenInputIsNull() { - Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(null)); - } + @Test + void returnsNullWhenInputIsNull() { + Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(null)); + } - @Test - void returnsNullWhenInputIsEmpty() { - Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters("")); - } + @Test + void returnsNullWhenInputIsEmpty() { + Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters("")); + } - @Test - void returnsNullWhenInputIsBlank() { - Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(" ")); - } + @Test + void returnsNullWhenInputIsBlank() { + Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(" ")); + } - @Test - void handlesStringWithOnlyInvalidCharacters() { - String xml = "\u0000\u0001\u0002"; - Assertions.assertEquals("", xmlCleaner.removeInvalidXmlCharacters(xml)); - } - } + @Test + void handlesStringWithOnlyInvalidCharacters() { + String xml = "\u0000\u0001\u0002"; + Assertions.assertEquals("", xmlCleaner.removeInvalidXmlCharacters(xml)); + } + } - @Nested - class Entities { - @Test - void testReplaceHtmlEntitiesWithNumericEntities() { - String source = "T´l´phone ′"; - Assertions.assertEquals("T´l´phone ′", - xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Nested + class Entities { + @Test + void testReplaceHtmlEntitiesWithNumericEntities() { + String source = "T´l´phone ′"; + Assertions.assertEquals( + "T´l´phone ′", + xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void replacesMultipleOccurrencesOfSameEntity() { - String source = "   "; - Assertions.assertEquals("   ", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Test + void replacesMultipleOccurrencesOfSameEntity() { + String source = "   "; + Assertions.assertEquals( + "   ", + xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void preservesTextWithoutEntities() { - String source = "regular content"; - Assertions.assertEquals("regular content", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Test + void preservesTextWithoutEntities() { + String source = "regular content"; + Assertions.assertEquals( + "regular content", + xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void preservesNumericEntities() { - String source = "´′"; - Assertions.assertEquals("´′", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Test + void preservesNumericEntities() { + String source = "´′"; + Assertions.assertEquals( + "´′", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void replacesCommonHtmlEntities() { - String source = "&""; - Assertions.assertEquals("&"", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Test + void replacesCommonHtmlEntities() { + String source = "&""; + Assertions.assertEquals( + "&"", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void handlesPartialEntityMatches() { - String source = "&lifier"; - String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source); - Assertions.assertTrue(result.startsWith("&") || result.equals("&lifier")); - } + @Test + void handlesPartialEntityMatches() { + String source = "&lifier"; + String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source); + Assertions.assertTrue(result.startsWith("&") || result.equals("&lifier")); + } - @Test - void returnsNullWhenInputIsNull() { - Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(null)); - } + @Test + void returnsNullWhenInputIsNull() { + Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(null)); + } - @Test - void returnsNullWhenInputIsEmpty() { - Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities("")); - } + @Test + void returnsNullWhenInputIsEmpty() { + Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities("")); + } - @Test - void returnsNullWhenInputIsBlank() { - Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(" ")); - } + @Test + void returnsNullWhenInputIsBlank() { + Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(" ")); + } - @Test - void handlesEntityAtStartOfString() { - String source = "&test"; - Assertions.assertEquals("&test", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Test + void handlesEntityAtStartOfString() { + String source = "&test"; + Assertions.assertEquals( + "&test", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void handlesEntityAtEndOfString() { - String source = "test&"; - Assertions.assertEquals("test&", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); - } + @Test + void handlesEntityAtEndOfString() { + String source = "test&"; + Assertions.assertEquals( + "test&", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source)); + } - @Test - void handlesMixedEntitiesAndText() { - String source = "Hello World! Test."; - String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source); - Assertions.assertTrue(result.contains("&#")); - } - } + @Test + void handlesMixedEntitiesAndText() { + String source = "Hello World! Test."; + String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source); + Assertions.assertTrue(result.contains("&#")); + } + } - @Nested - class Doctype { - @Test - void testRemoveDoctype() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Nested + class Doctype { + @Test + void testRemoveDoctype() { + String source = ""; + Assertions.assertEquals( + "", + xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void testRemoveMultilineDoctype() { - String source = """ + @Test + void testRemoveMultilineDoctype() { + String source = + """ """; - Assertions.assertEquals(""" + Assertions.assertEquals( + """ - """, xmlCleaner.removeDoctypeDeclarations(source)); - } + """, + xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void removesComplexDoctypeWithSystemId() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Test + void removesComplexDoctypeWithSystemId() { + String source = + ""; + Assertions.assertEquals( + "", xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void removesComplexDoctypeWithPublicId() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Test + void removesComplexDoctypeWithPublicId() { + String source = + ""; + Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void removesCaseInsensitiveDoctype() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Test + void removesCaseInsensitiveDoctype() { + String source = ""; + Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void removesMixedCaseDoctype() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Test + void removesMixedCaseDoctype() { + String source = ""; + Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void removesMultipleDoctypeDeclarations() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Test + void removesMultipleDoctypeDeclarations() { + String source = ""; + Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void preservesContentWithoutDoctype() { - String source = "No doctype here"; - Assertions.assertEquals("No doctype here", xmlCleaner.removeDoctypeDeclarations(source)); - } + @Test + void preservesContentWithoutDoctype() { + String source = "No doctype here"; + Assertions.assertEquals( + "No doctype here", + xmlCleaner.removeDoctypeDeclarations(source)); + } - @Test - void returnsNullWhenInputIsNull() { - Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(null)); - } + @Test + void returnsNullWhenInputIsNull() { + Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(null)); + } - @Test - void returnsNullWhenInputIsEmpty() { - Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations("")); - } + @Test + void returnsNullWhenInputIsEmpty() { + Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations("")); + } - @Test - void returnsNullWhenInputIsBlank() { - Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(" ")); - } + @Test + void returnsNullWhenInputIsBlank() { + Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(" ")); + } - @Test - void handlesDoctypeWithExtraWhitespace() { - String source = ""; - Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); - } - } + @Test + void handlesDoctypeWithExtraWhitespace() { + String source = ""; + Assertions.assertEquals("", xmlCleaner.removeDoctypeDeclarations(source)); + } + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/model/FeedEntryContentTest.java b/commafeed-server/src/test/java/com/commafeed/backend/model/FeedEntryContentTest.java index c665cb69..c89fa253 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/model/FeedEntryContentTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/model/FeedEntryContentTest.java @@ -6,135 +6,363 @@ import org.junit.jupiter.api.Test; class FeedEntryContentTest { - @Nested - class EquivalentTo { + @Nested + class EquivalentTo { - @Test - void shouldReturnFalseWhenComparedWithNull() { - Assertions.assertFalse(new FeedEntryContent().equivalentTo(null)); - } + @Test + void shouldReturnFalseWhenComparedWithNull() { + Assertions.assertFalse(new FeedEntryContent().equivalentTo(null)); + } - @Test - void shouldReturnTrueWhenComparedWithIdenticalContent() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertTrue(content1.equivalentTo(content2)); - } + @Test + void shouldReturnTrueWhenComparedWithIdenticalContent() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertTrue(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenTitleDiffers() { - FeedEntryContent content1 = createContent("title1", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title2", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenTitleDiffers() { + FeedEntryContent content1 = + createContent( + "title1", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title2", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenContentDiffers() { - FeedEntryContent content1 = createContent("title", "content1", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content2", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenContentDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content1", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content2", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenAuthorDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author1", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author2", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenAuthorDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author1", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author2", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenCategoriesDiffer() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories1", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories2", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenCategoriesDiffer() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories1", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories2", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenEnclosureUrlDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl1", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl2", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenEnclosureUrlDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl1", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl2", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenEnclosureTypeDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType1", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType2", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenEnclosureTypeDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType1", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType2", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenMediaDescriptionDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription1", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription2", "mediaThumbnailUrl", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenMediaDescriptionDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription1", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription2", + "mediaThumbnailUrl", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenMediaThumbnailUrlDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl1", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl2", 10, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenMediaThumbnailUrlDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl1", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl2", + 10, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenMediaThumbnailWidthDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 15, 20); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenMediaThumbnailWidthDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 15, + 20); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnFalseWhenMediaThumbnailHeightDiffers() { - FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 20); - FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType", - "mediaDescription", "mediaThumbnailUrl", 10, 25); - Assertions.assertFalse(content1.equivalentTo(content2)); - } + @Test + void shouldReturnFalseWhenMediaThumbnailHeightDiffers() { + FeedEntryContent content1 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 20); + FeedEntryContent content2 = + createContent( + "title", + "content", + "author", + "categories", + "enclosureUrl", + "enclosureType", + "mediaDescription", + "mediaThumbnailUrl", + 10, + 25); + Assertions.assertFalse(content1.equivalentTo(content2)); + } - @Test - void shouldReturnTrueWhenNullFieldsAreEqual() { - FeedEntryContent content1 = new FeedEntryContent(); - FeedEntryContent content2 = new FeedEntryContent(); - Assertions.assertTrue(content1.equivalentTo(content2)); - } + @Test + void shouldReturnTrueWhenNullFieldsAreEqual() { + FeedEntryContent content1 = new FeedEntryContent(); + FeedEntryContent content2 = new FeedEntryContent(); + Assertions.assertTrue(content1.equivalentTo(content2)); + } - private FeedEntryContent createContent(String title, String content, String author, String categories, String enclosureUrl, - String enclosureType, String mediaDescription, String mediaThumbnailUrl, Integer mediaThumbnailWidth, - Integer mediaThumbnailHeight) { - FeedEntryContent feedEntryContent = new FeedEntryContent(); - feedEntryContent.setTitle(title); - feedEntryContent.setContent(content); - feedEntryContent.setAuthor(author); - feedEntryContent.setCategories(categories); - feedEntryContent.setEnclosureUrl(enclosureUrl); - feedEntryContent.setEnclosureType(enclosureType); - feedEntryContent.setMediaDescription(mediaDescription); - feedEntryContent.setMediaThumbnailUrl(mediaThumbnailUrl); - feedEntryContent.setMediaThumbnailWidth(mediaThumbnailWidth); - feedEntryContent.setMediaThumbnailHeight(mediaThumbnailHeight); - return feedEntryContent; - } - } -} \ No newline at end of file + private FeedEntryContent createContent( + String title, + String content, + String author, + String categories, + String enclosureUrl, + String enclosureType, + String mediaDescription, + String mediaThumbnailUrl, + Integer mediaThumbnailWidth, + Integer mediaThumbnailHeight) { + FeedEntryContent feedEntryContent = new FeedEntryContent(); + feedEntryContent.setTitle(title); + feedEntryContent.setContent(content); + feedEntryContent.setAuthor(author); + feedEntryContent.setCategories(categories); + feedEntryContent.setEnclosureUrl(enclosureUrl); + feedEntryContent.setEnclosureType(enclosureType); + feedEntryContent.setMediaDescription(mediaDescription); + feedEntryContent.setMediaThumbnailUrl(mediaThumbnailUrl); + feedEntryContent.setMediaThumbnailWidth(mediaThumbnailWidth); + feedEntryContent.setMediaThumbnailHeight(mediaThumbnailHeight); + return feedEntryContent; + } + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java index ee047efa..af7b8c4d 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLExporterTest.java @@ -1,16 +1,5 @@ package com.commafeed.backend.opml; -import java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.model.Feed; @@ -19,115 +8,124 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Outline; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class OPMLExporterTest { - @Mock - private FeedCategoryDAO feedCategoryDAO; - @Mock - private FeedSubscriptionDAO feedSubscriptionDAO; + @Mock private FeedCategoryDAO feedCategoryDAO; + @Mock private FeedSubscriptionDAO feedSubscriptionDAO; - private final User user = new User(); + private final User user = new User(); - private final FeedCategory cat1 = new FeedCategory(); - private final FeedCategory cat2 = new FeedCategory(); + private final FeedCategory cat1 = new FeedCategory(); + private final FeedCategory cat2 = new FeedCategory(); - private final FeedSubscription rootFeed = newFeedSubscription("rootFeed", "rootFeed.com"); - private final FeedSubscription cat1Feed = newFeedSubscription("cat1Feed", "cat1Feed.com"); - private final FeedSubscription cat2Feed = newFeedSubscription("cat2Feed", "cat2Feed.com"); + private final FeedSubscription rootFeed = newFeedSubscription("rootFeed", "rootFeed.com"); + private final FeedSubscription cat1Feed = newFeedSubscription("cat1Feed", "cat1Feed.com"); + private final FeedSubscription cat2Feed = newFeedSubscription("cat2Feed", "cat2Feed.com"); - private final List categories = new ArrayList<>(); - private final List subscriptions = new ArrayList<>(); + private final List categories = new ArrayList<>(); + private final List subscriptions = new ArrayList<>(); - @BeforeEach - void init() { - user.setName("John Doe"); + @BeforeEach + void init() { + user.setName("John Doe"); - cat1.setId(1L); - cat1.setName("cat1"); - cat1.setParent(null); + cat1.setId(1L); + cat1.setName("cat1"); + cat1.setParent(null); - cat2.setId(2L); - cat2.setName("cat2"); - cat2.setParent(cat1); + cat2.setId(2L); + cat2.setName("cat2"); + cat2.setParent(cat1); - rootFeed.setCategory(null); - cat1Feed.setCategory(cat1); - cat2Feed.setCategory(cat2); + rootFeed.setCategory(null); + cat1Feed.setCategory(cat1); + cat2Feed.setCategory(cat2); - categories.add(cat1); - categories.add(cat2); + categories.add(cat1); + categories.add(cat2); - subscriptions.add(rootFeed); - subscriptions.add(cat1Feed); - subscriptions.add(cat2Feed); - } + subscriptions.add(rootFeed); + subscriptions.add(cat1Feed); + subscriptions.add(cat2Feed); + } - private Feed newFeed(String url) { - Feed feed = new Feed(); - feed.setUrl(url); - return feed; - } + private Feed newFeed(String url) { + Feed feed = new Feed(); + feed.setUrl(url); + return feed; + } - private FeedSubscription newFeedSubscription(String title, String url) { - FeedSubscription feedSubscription = new FeedSubscription(); - feedSubscription.setTitle(title); - feedSubscription.setFeed(newFeed(url)); - return feedSubscription; - } + private FeedSubscription newFeedSubscription(String title, String url) { + FeedSubscription feedSubscription = new FeedSubscription(); + feedSubscription.setTitle(title); + feedSubscription.setFeed(newFeed(url)); + return feedSubscription; + } - @Test - void generatesOpmlCorrectly() { - Mockito.when(feedCategoryDAO.findAll(user)).thenReturn(categories); - Mockito.when(feedSubscriptionDAO.findAll(user)).thenReturn(subscriptions); + @Test + void generatesOpmlCorrectly() { + Mockito.when(feedCategoryDAO.findAll(user)).thenReturn(categories); + Mockito.when(feedSubscriptionDAO.findAll(user)).thenReturn(subscriptions); - Opml opml = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO).export(user); + Opml opml = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO).export(user); - List rootOutlines = opml.getOutlines(); - Assertions.assertEquals(2, rootOutlines.size()); - Assertions.assertTrue(containsCategory(rootOutlines, "cat1")); - Assertions.assertTrue(containsFeed(rootOutlines, "rootFeed", "rootFeed.com")); + List rootOutlines = opml.getOutlines(); + Assertions.assertEquals(2, rootOutlines.size()); + Assertions.assertTrue(containsCategory(rootOutlines, "cat1")); + Assertions.assertTrue(containsFeed(rootOutlines, "rootFeed", "rootFeed.com")); - Outline cat1Outline = getCategoryOutline(rootOutlines, "cat1"); - List cat1Children = cat1Outline.getChildren(); - Assertions.assertEquals(2, cat1Children.size()); - Assertions.assertTrue(containsCategory(cat1Children, "cat2")); - Assertions.assertTrue(containsFeed(cat1Children, "cat1Feed", "cat1Feed.com")); + Outline cat1Outline = getCategoryOutline(rootOutlines, "cat1"); + List cat1Children = cat1Outline.getChildren(); + Assertions.assertEquals(2, cat1Children.size()); + Assertions.assertTrue(containsCategory(cat1Children, "cat2")); + Assertions.assertTrue(containsFeed(cat1Children, "cat1Feed", "cat1Feed.com")); - Outline cat2Outline = getCategoryOutline(cat1Children, "cat2"); - List cat2Children = cat2Outline.getChildren(); - Assertions.assertEquals(1, cat2Children.size()); - Assertions.assertTrue(containsFeed(cat2Children, "cat2Feed", "cat2Feed.com")); - } + Outline cat2Outline = getCategoryOutline(cat1Children, "cat2"); + List cat2Children = cat2Outline.getChildren(); + Assertions.assertEquals(1, cat2Children.size()); + Assertions.assertTrue(containsFeed(cat2Children, "cat2Feed", "cat2Feed.com")); + } - private boolean containsCategory(List outlines, String category) { - for (Outline o : outlines) { - if (!"rss".equals(o.getType()) && category.equals(o.getTitle())) { - return true; - } - } + private boolean containsCategory(List outlines, String category) { + for (Outline o : outlines) { + if (!"rss".equals(o.getType()) && category.equals(o.getTitle())) { + return true; + } + } - return false; - } + return false; + } - private boolean containsFeed(List outlines, String title, String url) { - for (Outline o : outlines) { - if ("rss".equals(o.getType()) && title.equals(o.getTitle()) && o.getAttributeValue("xmlUrl").equals(url)) { - return true; - } - } + private boolean containsFeed(List outlines, String title, String url) { + for (Outline o : outlines) { + if ("rss".equals(o.getType()) + && title.equals(o.getTitle()) + && o.getAttributeValue("xmlUrl").equals(url)) { + return true; + } + } - return false; - } + return false; + } - private Outline getCategoryOutline(List outlines, String title) { - for (Outline o : outlines) { - if (o.getTitle().equals(title)) { - return o; - } - } + private Outline getCategoryOutline(List outlines, String title) { + for (Outline o : outlines) { + if (o.getTitle().equals(title)) { + return o; + } + } - return null; - } -} \ No newline at end of file + return null; + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java index 87101820..e3637f30 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/opml/OPMLImporterTest.java @@ -1,55 +1,62 @@ package com.commafeed.backend.opml; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.feed.parser.XMLCleaner; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedSubscriptionService; import com.rometools.rome.io.FeedException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class OPMLImporterTest { - @Test - void testOpmlV10() throws IOException, IllegalArgumentException, FeedException { - testOpmlVersion("/opml/opml_v1.0.xml"); - } + @Test + void testOpmlV10() throws IOException, IllegalArgumentException, FeedException { + testOpmlVersion("/opml/opml_v1.0.xml"); + } - @Test - void testOpmlV11() throws IOException, IllegalArgumentException, FeedException { - testOpmlVersion("/opml/opml_v1.1.xml"); - } + @Test + void testOpmlV11() throws IOException, IllegalArgumentException, FeedException { + testOpmlVersion("/opml/opml_v1.1.xml"); + } - @Test - void testOpmlV20() throws IOException, IllegalArgumentException, FeedException { - testOpmlVersion("/opml/opml_v2.0.xml"); - } + @Test + void testOpmlV20() throws IOException, IllegalArgumentException, FeedException { + testOpmlVersion("/opml/opml_v2.0.xml"); + } - @Test - void testOpmlNoVersion() throws IOException, IllegalArgumentException, FeedException { - testOpmlVersion("/opml/opml_noversion.xml"); - } + @Test + void testOpmlNoVersion() throws IOException, IllegalArgumentException, FeedException { + testOpmlVersion("/opml/opml_noversion.xml"); + } - private void testOpmlVersion(String fileName) throws IOException, IllegalArgumentException, FeedException { - XMLCleaner xmlCleaner = Mockito.mock(XMLCleaner.class); - FeedCategoryDAO feedCategoryDAO = Mockito.mock(FeedCategoryDAO.class); - FeedSubscriptionService feedSubscriptionService = Mockito.mock(FeedSubscriptionService.class); - User user = Mockito.mock(User.class); + private void testOpmlVersion(String fileName) + throws IOException, IllegalArgumentException, FeedException { + XMLCleaner xmlCleaner = Mockito.mock(XMLCleaner.class); + FeedCategoryDAO feedCategoryDAO = Mockito.mock(FeedCategoryDAO.class); + FeedSubscriptionService feedSubscriptionService = + Mockito.mock(FeedSubscriptionService.class); + User user = Mockito.mock(User.class); - Mockito.when(xmlCleaner.clean(Mockito.anyString())).thenAnswer(invocation -> invocation.getArgument(0)); - String xml = IOUtils.toString(getClass().getResourceAsStream(fileName), StandardCharsets.UTF_8); + Mockito.when(xmlCleaner.clean(Mockito.anyString())) + .thenAnswer(invocation -> invocation.getArgument(0)); + String xml = + IOUtils.toString(getClass().getResourceAsStream(fileName), StandardCharsets.UTF_8); - OPMLImporter importer = new OPMLImporter(xmlCleaner, feedCategoryDAO, feedSubscriptionService); - importer.importOpml(user, xml); - - Mockito.verify(feedSubscriptionService) - .subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), Mockito.any(FeedCategory.class), Mockito.anyInt()); - } + OPMLImporter importer = + new OPMLImporter(xmlCleaner, feedCategoryDAO, feedSubscriptionService); + importer.importOpml(user, xml); + Mockito.verify(feedSubscriptionService) + .subscribe( + Mockito.eq(user), + Mockito.anyString(), + Mockito.anyString(), + Mockito.any(FeedCategory.class), + Mockito.anyInt()); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryContentCleaningServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryContentCleaningServiceTest.java index a5600962..fbd9aea7 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryContentCleaningServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryContentCleaningServiceTest.java @@ -5,27 +5,31 @@ import org.junit.jupiter.api.Test; class FeedEntryContentCleaningServiceTest { - private final FeedEntryContentCleaningService feedEntryContentCleaningService = new FeedEntryContentCleaningService(); + private final FeedEntryContentCleaningService feedEntryContentCleaningService = + new FeedEntryContentCleaningService(); - @Test - void testClean() { - String content = """ + @Test + void testClean() { + String content = + """

Some text alt-desc aaa """; - String result = feedEntryContentCleaningService.clean(content, "baseUri", false); + String result = feedEntryContentCleaningService.clean(content, "baseUri", false); - Assertions.assertLinesMatch(""" + Assertions.assertLinesMatch( + """

Some text alt-desc aaa

- """.lines(), result.lines()); - } - -} \ No newline at end of file + """ + .lines(), + result.lines()); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java index 99ad513f..654b5f8b 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryFilteringServiceTest.java @@ -1,267 +1,321 @@ package com.commafeed.backend.service; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedEntryContent; +import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; import java.time.Duration; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.model.FeedEntry; -import com.commafeed.backend.model.FeedEntryContent; -import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; - class FeedEntryFilteringServiceTest { - private CommaFeedConfiguration config; + private CommaFeedConfiguration config; - private FeedEntryFilteringService service; + private FeedEntryFilteringService service; - private FeedEntry entry; + private FeedEntry entry; - @BeforeEach - void init() { - config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); - Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofSeconds(30)); + @BeforeEach + void init() { + config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()) + .thenReturn(Duration.ofSeconds(30)); - service = new FeedEntryFilteringService(config); + service = new FeedEntryFilteringService(config); - entry = new FeedEntry(); - entry.setUrl("https://github.com/Athou/commafeed"); + entry = new FeedEntry(); + entry.setUrl("https://github.com/Athou/commafeed"); - FeedEntryContent content = new FeedEntryContent(); - content.setAuthor("Athou"); - content.setTitle("Merge pull request #662 from Athou/dw8"); - content.setContent("Merge pull request #662 from Athou/dw8"); - entry.setContent(content); + FeedEntryContent content = new FeedEntryContent(); + content.setAuthor("Athou"); + content.setTitle("Merge pull request #662 from Athou/dw8"); + content.setContent("Merge pull request #662 from Athou/dw8"); + entry.setContent(content); + } - } + @Test + void emptyFilterMatchesFilter() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry(null, entry)); + } - @Test - void emptyFilterMatchesFilter() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry(null, entry)); - } + @Test + void blankFilterMatchesFilter() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("", entry)); + } - @Test - void blankFilterMatchesFilter() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("", entry)); - } + @Test + void simpleEqualsExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\"", entry)); + } - @Test - void simpleEqualsExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\"", entry)); - } + @Test + void simpleNotEqualsExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("author != \"other\"", entry)); + } - @Test - void simpleNotEqualsExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("author != \"other\"", entry)); - } + @Test + void containsExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("author.contains(\"Athou\")", entry)); + } - @Test - void containsExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("author.contains(\"Athou\")", entry)); - } + @Test + void titleContainsExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("title.contains(\"Merge\")", entry)); + } - @Test - void titleContainsExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("title.contains(\"Merge\")", entry)); - } + @Test + void urlContainsExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("url.contains(\"github\")", entry)); + } - @Test - void urlContainsExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("url.contains(\"github\")", entry)); - } + @Test + void andExpression() throws FeedEntryFilterException { + Assertions.assertTrue( + service.filterMatchesEntry( + "author == \"Athou\" && url.contains(\"github\")", entry)); + } - @Test - void andExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\" && url.contains(\"github\")", entry)); - } + @Test + void orExpression() throws FeedEntryFilterException { + Assertions.assertTrue( + service.filterMatchesEntry( + "author == \"other\" || url.contains(\"github\")", entry)); + } - @Test - void orExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("author == \"other\" || url.contains(\"github\")", entry)); - } + @Test + void notExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("!(author == \"other\")", entry)); + } - @Test - void notExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("!(author == \"other\")", entry)); - } + @Test + void incorrectExpressionThrowsException() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("not valid cel", entry)); + } - @Test - void incorrectExpressionThrowsException() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("not valid cel", entry)); - } + @Test + void falseValueReturnsFalse() throws FeedEntryFilterException { + Assertions.assertFalse(service.filterMatchesEntry("false", entry)); + } - @Test - void falseValueReturnsFalse() throws FeedEntryFilterException { - Assertions.assertFalse(service.filterMatchesEntry("false", entry)); - } + @Test + void trueValueReturnsTrue() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("true", entry)); + } - @Test - void trueValueReturnsTrue() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("true", entry)); - } + @Test + void startsWithExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("title.startsWith(\"Merge\")", entry)); + } - @Test - void startsWithExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("title.startsWith(\"Merge\")", entry)); - } + @Test + void endsWithExpression() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("url.endsWith(\"commafeed\")", entry)); + } - @Test - void endsWithExpression() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("url.endsWith(\"commafeed\")", entry)); - } + @Test + void categoriesContainsExpression() throws FeedEntryFilterException { + FeedEntryContent content = entry.getContent(); + content.setCategories("tech, programming, java"); + entry.setContent(content); + Assertions.assertTrue( + service.filterMatchesEntry("categories.contains(\"programming\")", entry)); + } - @Test - void categoriesContainsExpression() throws FeedEntryFilterException { - FeedEntryContent content = entry.getContent(); - content.setCategories("tech, programming, java"); - entry.setContent(content); - Assertions.assertTrue(service.filterMatchesEntry("categories.contains(\"programming\")", entry)); - } + @Test + void caseInsensitiveAuthorMatchUsingLowerVariable() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("authorLower == \"athou\"", entry)); + } - @Test - void caseInsensitiveAuthorMatchUsingLowerVariable() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("authorLower == \"athou\"", entry)); - } + @Test + void caseInsensitiveTitleMatchUsingLowerVariable() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("titleLower.contains(\"merge\")", entry)); + } - @Test - void caseInsensitiveTitleMatchUsingLowerVariable() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("titleLower.contains(\"merge\")", entry)); - } + @Test + void caseInsensitiveUrlMatchUsingLowerVariable() throws FeedEntryFilterException { + Assertions.assertTrue(service.filterMatchesEntry("urlLower.contains(\"github\")", entry)); + } - @Test - void caseInsensitiveUrlMatchUsingLowerVariable() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("urlLower.contains(\"github\")", entry)); - } + @Test + void caseInsensitiveContentMatchUsingLowerVariable() throws FeedEntryFilterException { + Assertions.assertTrue( + service.filterMatchesEntry("contentLower.contains(\"merge\")", entry)); + } - @Test - void caseInsensitiveContentMatchUsingLowerVariable() throws FeedEntryFilterException { - Assertions.assertTrue(service.filterMatchesEntry("contentLower.contains(\"merge\")", entry)); - } + @Test + void caseInsensitiveCategoriesMatchUsingLowerVariable() throws FeedEntryFilterException { + FeedEntryContent content = entry.getContent(); + content.setCategories("Tech, Programming, Java"); + entry.setContent(content); + Assertions.assertTrue( + service.filterMatchesEntry("categoriesLower.contains(\"tech\")", entry)); + } - @Test - void caseInsensitiveCategoriesMatchUsingLowerVariable() throws FeedEntryFilterException { - FeedEntryContent content = entry.getContent(); - content.setCategories("Tech, Programming, Java"); - entry.setContent(content); - Assertions.assertTrue(service.filterMatchesEntry("categoriesLower.contains(\"tech\")", entry)); - } + @Nested + class Sandbox { - @Nested - class Sandbox { + @Test + void sandboxBlocksSystemPropertyAccess() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "java.lang.System.getProperty(\"user.home\")", entry)); + } - @Test - void sandboxBlocksSystemPropertyAccess() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("java.lang.System.getProperty(\"user.home\")", entry)); - } + @Test + void sandboxBlocksRuntimeExec() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "java.lang.Runtime.getRuntime().exec(\"calc\")", entry)); + } - @Test - void sandboxBlocksRuntimeExec() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("java.lang.Runtime.getRuntime().exec(\"calc\")", entry)); - } + @Test + void sandboxBlocksProcessBuilder() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "new java.lang.ProcessBuilder(\"cmd\").start()", entry)); + } - @Test - void sandboxBlocksProcessBuilder() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("new java.lang.ProcessBuilder(\"cmd\").start()", entry)); - } + @Test + void sandboxBlocksClassLoading() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "java.lang.Class.forName(\"java.lang.Runtime\")", entry)); + } - @Test - void sandboxBlocksClassLoading() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("java.lang.Class.forName(\"java.lang.Runtime\")", entry)); - } + @Test + void sandboxBlocksReflection() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("title.getClass().getMethods()", entry)); + } - @Test - void sandboxBlocksReflection() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("title.getClass().getMethods()", entry)); - } + @Test + void sandboxBlocksFileAccess() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "new java.io.File(\"/etc/passwd\").exists()", entry)); + } - @Test - void sandboxBlocksFileAccess() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("new java.io.File(\"/etc/passwd\").exists()", entry)); - } + @Test + void sandboxBlocksFileRead() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "java.nio.file.Files.readString(java.nio.file.Paths.get(\"/etc/passwd\"))", + entry)); + } - @Test - void sandboxBlocksFileRead() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("java.nio.file.Files.readString(java.nio.file.Paths.get(\"/etc/passwd\"))", entry)); - } + @Test + void sandboxBlocksNetworkAccess() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "new java.net.URL(\"http://evil.com\").openConnection()", + entry)); + } - @Test - void sandboxBlocksNetworkAccess() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("new java.net.URL(\"http://evil.com\").openConnection()", entry)); - } + @Test + void sandboxBlocksScriptEngine() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> + service.filterMatchesEntry( + "new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"1+1\")", + entry)); + } - @Test - void sandboxBlocksScriptEngine() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service - .filterMatchesEntry("new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"1+1\")", entry)); - } + @Test + void sandboxBlocksThreadCreation() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("new java.lang.Thread().start()", entry)); + } - @Test - void sandboxBlocksThreadCreation() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("new java.lang.Thread().start()", entry)); - } + @Test + void sandboxBlocksEnvironmentVariableAccess() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("java.lang.System.getenv(\"PATH\")", entry)); + } - @Test - void sandboxBlocksEnvironmentVariableAccess() { - Assertions.assertThrows(FeedEntryFilterException.class, - () -> service.filterMatchesEntry("java.lang.System.getenv(\"PATH\")", entry)); - } + @Test + void sandboxBlocksExitCall() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("java.lang.System.exit(0)", entry)); + } - @Test - void sandboxBlocksExitCall() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("java.lang.System.exit(0)", entry)); - } + @Test + void sandboxBlocksUndeclaredVariables() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("unknownVariable == \"test\"", entry)); + } - @Test - void sandboxBlocksUndeclaredVariables() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("unknownVariable == \"test\"", entry)); - } + @Test + void sandboxBlocksMethodInvocationOnStrings() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("title.toCharArray()", entry)); + } - @Test - void sandboxBlocksMethodInvocationOnStrings() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("title.toCharArray()", entry)); - } + @Test + void sandboxBlocksArbitraryJavaMethodCalls() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("title.getBytes()", entry)); + } - @Test - void sandboxBlocksArbitraryJavaMethodCalls() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("title.getBytes()", entry)); - } + @Test + void sandboxOnlyAllowsDeclaredVariables() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("System", entry)); + } - @Test - void sandboxOnlyAllowsDeclaredVariables() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("System", entry)); - } + @Test + void sandboxBlocksConstructorCalls() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("new String(\"test\")", entry)); + } - @Test - void sandboxBlocksConstructorCalls() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("new String(\"test\")", entry)); - } + @Test + void sandboxBlocksStaticMethodCalls() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("String.valueOf(123)", entry)); + } - @Test - void sandboxBlocksStaticMethodCalls() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("String.valueOf(123)", entry)); - } - - @Test - void sandboxBlocksLambdaExpressions() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("() -> true", entry)); - } - - @Test - void sandboxBlocksObjectInstantiation() { - Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("java.util.HashMap{}", entry)); - } - } + @Test + void sandboxBlocksLambdaExpressions() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("() -> true", entry)); + } + @Test + void sandboxBlocksObjectInstantiation() { + Assertions.assertThrows( + FeedEntryFilterException.class, + () -> service.filterMatchesEntry("java.util.HashMap{}", entry)); + } + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/FeedFaviconServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedFaviconServiceTest.java index 7ce6f24f..0bb7f264 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/FeedFaviconServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedFaviconServiceTest.java @@ -1,10 +1,11 @@ package com.commafeed.backend.service; +import com.commafeed.backend.favicon.Favicon; +import com.commafeed.backend.favicon.FaviconFetcher; +import com.commafeed.backend.model.Feed; +import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.util.List; - -import jakarta.ws.rs.core.MediaType; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -13,139 +14,133 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.commafeed.backend.favicon.Favicon; -import com.commafeed.backend.favicon.FaviconFetcher; -import com.commafeed.backend.model.Feed; - @ExtendWith(MockitoExtension.class) class FeedFaviconServiceTest { - @Mock - private FaviconFetcher fetcher1; + @Mock private FaviconFetcher fetcher1; - @Mock - private FaviconFetcher fetcher2; + @Mock private FaviconFetcher fetcher2; - private FeedFaviconService service; - private Feed feed; + private FeedFaviconService service; + private Feed feed; - @BeforeEach - void init() throws IOException { - service = new FeedFaviconService(List.of(fetcher1, fetcher2)); - feed = new Feed(); - feed.setUrl("https://example.com/feed"); - } + @BeforeEach + void init() throws IOException { + service = new FeedFaviconService(List.of(fetcher1, fetcher2)); + feed = new Feed(); + feed.setUrl("https://example.com/feed"); + } - @Test - void testReturnsFirstValidFavicon() { - byte[] iconBytes = new byte[1000]; - Favicon validFavicon = new Favicon(iconBytes, "image/png"); + @Test + void testReturnsFirstValidFavicon() { + byte[] iconBytes = new byte[1000]; + Favicon validFavicon = new Favicon(iconBytes, "image/png"); - Mockito.when(fetcher1.fetch(feed)).thenReturn(validFavicon); + Mockito.when(fetcher1.fetch(feed)).thenReturn(validFavicon); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertEquals(validFavicon, result); - Mockito.verify(fetcher1).fetch(feed); - Mockito.verifyNoInteractions(fetcher2); - } + Assertions.assertEquals(validFavicon, result); + Mockito.verify(fetcher1).fetch(feed); + Mockito.verifyNoInteractions(fetcher2); + } - @Test - void testFallsBackToNextFetcherWhenFirstReturnsNull() { - byte[] iconBytes = new byte[1000]; - Favicon validFavicon = new Favicon(iconBytes, "image/png"); + @Test + void testFallsBackToNextFetcherWhenFirstReturnsNull() { + byte[] iconBytes = new byte[1000]; + Favicon validFavicon = new Favicon(iconBytes, "image/png"); - Mockito.when(fetcher1.fetch(feed)).thenReturn(null); - Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); + Mockito.when(fetcher1.fetch(feed)).thenReturn(null); + Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertEquals(validFavicon, result); - } + Assertions.assertEquals(validFavicon, result); + } - @Test - void testFallsBackToNextFetcherWhenFirstReturnsTooSmallIcon() { - byte[] tinyIcon = new byte[50]; - Favicon tinyFavicon = new Favicon(tinyIcon, "image/png"); + @Test + void testFallsBackToNextFetcherWhenFirstReturnsTooSmallIcon() { + byte[] tinyIcon = new byte[50]; + Favicon tinyFavicon = new Favicon(tinyIcon, "image/png"); - byte[] validIcon = new byte[1000]; - Favicon validFavicon = new Favicon(validIcon, "image/png"); + byte[] validIcon = new byte[1000]; + Favicon validFavicon = new Favicon(validIcon, "image/png"); - Mockito.when(fetcher1.fetch(feed)).thenReturn(tinyFavicon); - Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); + Mockito.when(fetcher1.fetch(feed)).thenReturn(tinyFavicon); + Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertEquals(validFavicon, result); - } + Assertions.assertEquals(validFavicon, result); + } - @Test - void testFallsBackToNextFetcherWhenFirstReturnsTooLargeIcon() { - byte[] hugeIcon = new byte[100001]; - Favicon hugeFavicon = new Favicon(hugeIcon, "image/png"); + @Test + void testFallsBackToNextFetcherWhenFirstReturnsTooLargeIcon() { + byte[] hugeIcon = new byte[100001]; + Favicon hugeFavicon = new Favicon(hugeIcon, "image/png"); - byte[] validIcon = new byte[1000]; - Favicon validFavicon = new Favicon(validIcon, "image/png"); + byte[] validIcon = new byte[1000]; + Favicon validFavicon = new Favicon(validIcon, "image/png"); - Mockito.when(fetcher1.fetch(feed)).thenReturn(hugeFavicon); - Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); + Mockito.when(fetcher1.fetch(feed)).thenReturn(hugeFavicon); + Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertEquals(validFavicon, result); - } + Assertions.assertEquals(validFavicon, result); + } - @Test - void testFallsBackToNextFetcherWhenFirstReturnsBlacklistedContentType() { - byte[] iconBytes = new byte[1000]; - Favicon xmlFavicon = new Favicon(iconBytes, "application/xml"); + @Test + void testFallsBackToNextFetcherWhenFirstReturnsBlacklistedContentType() { + byte[] iconBytes = new byte[1000]; + Favicon xmlFavicon = new Favicon(iconBytes, "application/xml"); - byte[] validIcon = new byte[1000]; - Favicon validFavicon = new Favicon(validIcon, "image/png"); + byte[] validIcon = new byte[1000]; + Favicon validFavicon = new Favicon(validIcon, "image/png"); - Mockito.when(fetcher1.fetch(feed)).thenReturn(xmlFavicon); - Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); + Mockito.when(fetcher1.fetch(feed)).thenReturn(xmlFavicon); + Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertEquals(validFavicon, result); - } + Assertions.assertEquals(validFavicon, result); + } - @Test - void testFallsBackToNextFetcherWhenFirstReturnsHtmlContentType() { - byte[] iconBytes = new byte[1000]; - Favicon htmlFavicon = new Favicon(iconBytes, "text/html"); + @Test + void testFallsBackToNextFetcherWhenFirstReturnsHtmlContentType() { + byte[] iconBytes = new byte[1000]; + Favicon htmlFavicon = new Favicon(iconBytes, "text/html"); - byte[] validIcon = new byte[1000]; - Favicon validFavicon = new Favicon(validIcon, "image/png"); + byte[] validIcon = new byte[1000]; + Favicon validFavicon = new Favicon(validIcon, "image/png"); - Mockito.when(fetcher1.fetch(feed)).thenReturn(htmlFavicon); - Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); + Mockito.when(fetcher1.fetch(feed)).thenReturn(htmlFavicon); + Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertEquals(validFavicon, result); - } + Assertions.assertEquals(validFavicon, result); + } - @Test - void testReturnsDefaultFaviconWhenAllFetchersFail() { - Mockito.when(fetcher1.fetch(feed)).thenReturn(null); - Mockito.when(fetcher2.fetch(feed)).thenReturn(null); + @Test + void testReturnsDefaultFaviconWhenAllFetchersFail() { + Mockito.when(fetcher1.fetch(feed)).thenReturn(null); + Mockito.when(fetcher2.fetch(feed)).thenReturn(null); - Favicon result = service.fetchFavicon(feed); + Favicon result = service.fetchFavicon(feed); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif"))); - Assertions.assertTrue(result.icon().length > 0); - } + Assertions.assertNotNull(result); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif"))); + Assertions.assertTrue(result.icon().length > 0); + } - @Test - void testReturnsDefaultFaviconWhenNoFetchersRegistered() throws IOException { - FeedFaviconService emptyService = new FeedFaviconService(List.of()); + @Test + void testReturnsDefaultFaviconWhenNoFetchersRegistered() throws IOException { + FeedFaviconService emptyService = new FeedFaviconService(List.of()); - Favicon result = emptyService.fetchFavicon(feed); + Favicon result = emptyService.fetchFavicon(feed); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif"))); - } + Assertions.assertNotNull(result); + Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif"))); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/PasswordEncryptionServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/PasswordEncryptionServiceTest.java index afe728d6..b3d79eeb 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/PasswordEncryptionServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/PasswordEncryptionServiceTest.java @@ -1,23 +1,24 @@ package com.commafeed.backend.service; import java.util.HexFormat; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class PasswordEncryptionServiceTest { - @Test - void authenticate() { - String password = "password"; - byte[] salt = "abcdefgh".getBytes(); + @Test + void authenticate() { + String password = "password"; + byte[] salt = "abcdefgh".getBytes(); - PasswordEncryptionService passwordEncryptionService = new PasswordEncryptionService(); - byte[] encryptedPassword = passwordEncryptionService.getEncryptedPassword(password, salt); + PasswordEncryptionService passwordEncryptionService = new PasswordEncryptionService(); + byte[] encryptedPassword = passwordEncryptionService.getEncryptedPassword(password, salt); - // make sure the encrypted password is always the same for a fixed salt - Assertions.assertEquals("8b4660158141d9f4f7865718b9a2b940a3e3cea9", HexFormat.of().formatHex(encryptedPassword)); - Assertions.assertTrue(passwordEncryptionService.authenticate(password, encryptedPassword, salt)); - } - -} \ No newline at end of file + // make sure the encrypted password is always the same for a fixed salt + Assertions.assertEquals( + "8b4660158141d9f4f7865718b9a2b940a3e3cea9", + HexFormat.of().formatHex(encryptedPassword)); + Assertions.assertTrue( + passwordEncryptionService.authenticate(password, encryptedPassword, salt)); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java index d736529f..25cc38c4 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/PushNotificationServiceTest.java @@ -1,7 +1,16 @@ package com.commafeed.backend.service; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.commafeed.CommaFeedConfiguration; +import com.commafeed.backend.HttpClientFactory; +import com.commafeed.backend.model.Feed; +import com.commafeed.backend.model.FeedEntry; +import com.commafeed.backend.model.FeedEntryContent; +import com.commafeed.backend.model.FeedSubscription; +import com.commafeed.backend.model.UserSettings.PushNotificationType; +import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings; import java.time.Duration; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,79 +23,79 @@ import org.mockserver.model.HttpResponse; import org.mockserver.model.JsonBody; import org.mockserver.model.MediaType; -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.HttpClientFactory; -import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedEntry; -import com.commafeed.backend.model.FeedEntryContent; -import com.commafeed.backend.model.FeedSubscription; -import com.commafeed.backend.model.UserSettings.PushNotificationType; -import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings; - @ExtendWith(MockServerExtension.class) class PushNotificationServiceTest { - private MockServerClient mockServerClient; - private PushNotificationService pushNotificationService; - private CommaFeedConfiguration config; - private PushNotificationUserSettings userSettings; - private FeedSubscription subscription; - private FeedEntry entry; + private MockServerClient mockServerClient; + private PushNotificationService pushNotificationService; + private CommaFeedConfiguration config; + private PushNotificationUserSettings userSettings; + private FeedSubscription subscription; + private FeedEntry entry; - @BeforeEach - void init(MockServerClient mockServerClient) { - this.mockServerClient = mockServerClient; - this.mockServerClient.reset(); + @BeforeEach + void init(MockServerClient mockServerClient) { + this.mockServerClient = mockServerClient; + this.mockServerClient.reset(); - this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); - Mockito.when(config.pushNotifications().enabled()).thenReturn(true); - Mockito.when(config.pushNotifications().threads()).thenReturn(1); - Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30)); + this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(config.pushNotifications().enabled()).thenReturn(true); + Mockito.when(config.pushNotifications().threads()).thenReturn(1); + Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30)); - HttpClientFactory httpClientFactory = new HttpClientFactory(config, Mockito.mock(com.commafeed.CommaFeedVersion.class)); - MetricRegistry metricRegistry = Mockito.mock(MetricRegistry.class); - Mockito.when(metricRegistry.meter(Mockito.anyString())).thenReturn(Mockito.mock(Meter.class)); + HttpClientFactory httpClientFactory = + new HttpClientFactory(config, Mockito.mock(com.commafeed.CommaFeedVersion.class)); + MetricRegistry metricRegistry = Mockito.mock(MetricRegistry.class); + Mockito.when(metricRegistry.meter(Mockito.anyString())) + .thenReturn(Mockito.mock(Meter.class)); - this.pushNotificationService = new PushNotificationService(httpClientFactory, metricRegistry, config); + this.pushNotificationService = + new PushNotificationService(httpClientFactory, metricRegistry, config); - this.userSettings = new PushNotificationUserSettings(); + this.userSettings = new PushNotificationUserSettings(); - this.subscription = createSubscription("Test Feed"); - this.entry = createEntry("Test Entry", "http://example.com/entry"); - } + this.subscription = createSubscription("Test Feed"); + this.entry = createEntry("Test Entry", "http://example.com/entry"); + } - @Test - void testNtfyNotification() { - userSettings.setType(PushNotificationType.NTFY); - userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort()); - userSettings.setTopic("test-topic"); - userSettings.setUserSecret("test-secret"); + @Test + void testNtfyNotification() { + userSettings.setType(PushNotificationType.NTFY); + userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort()); + userSettings.setTopic("test-topic"); + userSettings.setUserSecret("test-secret"); - mockServerClient.when(HttpRequest.request() - .withMethod("POST") - .withPath("/test-topic") - .withHeader("Title", "Test Feed") - .withHeader("Click", "http://example.com/entry") - .withHeader("Authorization", "Bearer test-secret") - .withBody("Test Entry")).respond(HttpResponse.response().withStatusCode(200)); + mockServerClient + .when( + HttpRequest.request() + .withMethod("POST") + .withPath("/test-topic") + .withHeader("Title", "Test Feed") + .withHeader("Click", "http://example.com/entry") + .withHeader("Authorization", "Bearer test-secret") + .withBody("Test Entry")) + .respond(HttpResponse.response().withStatusCode(200)); - Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry)); - } + Assertions.assertDoesNotThrow( + () -> pushNotificationService.notify(userSettings, subscription, entry)); + } - @Test - void testGotifyNotification() { - userSettings.setType(PushNotificationType.GOTIFY); - userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort()); - userSettings.setUserSecret("gotify-token"); + @Test + void testGotifyNotification() { + userSettings.setType(PushNotificationType.GOTIFY); + userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort()); + userSettings.setUserSecret("gotify-token"); - mockServerClient.when(HttpRequest.request() - .withMethod("POST") - .withPath("/message") - .withHeader("X-Gotify-Key", "gotify-token") - .withContentType(MediaType.APPLICATION_JSON_UTF_8) - .withBody(JsonBody.json(""" + mockServerClient + .when( + HttpRequest.request() + .withMethod("POST") + .withPath("/message") + .withHeader("X-Gotify-Key", "gotify-token") + .withContentType(MediaType.APPLICATION_JSON_UTF_8) + .withBody( + JsonBody.json( + """ { "title": "Test Feed", "message": "Test Entry", @@ -99,38 +108,41 @@ class PushNotificationServiceTest { } } } - """))).respond(HttpResponse.response().withStatusCode(200)); + """))) + .respond(HttpResponse.response().withStatusCode(200)); - Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry)); - } + Assertions.assertDoesNotThrow( + () -> pushNotificationService.notify(userSettings, subscription, entry)); + } - @Test - void testPushNotificationDisabled() { - Mockito.when(config.pushNotifications().enabled()).thenReturn(false); - userSettings.setType(PushNotificationType.NTFY); - userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort()); - userSettings.setTopic("test-topic"); + @Test + void testPushNotificationDisabled() { + Mockito.when(config.pushNotifications().enabled()).thenReturn(false); + userSettings.setType(PushNotificationType.NTFY); + userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort()); + userSettings.setTopic("test-topic"); - Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry)); - mockServerClient.verifyZeroInteractions(); - } + Assertions.assertDoesNotThrow( + () -> pushNotificationService.notify(userSettings, subscription, entry)); + mockServerClient.verifyZeroInteractions(); + } - private static FeedSubscription createSubscription(String title) { - FeedSubscription subscription = new FeedSubscription(); - subscription.setTitle(title); - subscription.setFeed(new Feed()); + private static FeedSubscription createSubscription(String title) { + FeedSubscription subscription = new FeedSubscription(); + subscription.setTitle(title); + subscription.setFeed(new Feed()); - return subscription; - } + return subscription; + } - private static FeedEntry createEntry(String title, String url) { - FeedEntry entry = new FeedEntry(); + private static FeedEntry createEntry(String title, String url) { + FeedEntry entry = new FeedEntry(); - FeedEntryContent content = new FeedEntryContent(); - content.setTitle(title); + FeedEntryContent content = new FeedEntryContent(); + content.setTitle(title); - entry.setContent(content); - entry.setUrl(url); - return entry; - } + entry.setContent(content); + entry.setUrl(url); + return entry; + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/UserServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/UserServiceTest.java index 4841c33d..27c5bd32 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/UserServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/UserServiceTest.java @@ -1,15 +1,5 @@ package com.commafeed.backend.service; -import java.util.Optional; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -18,177 +8,201 @@ import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.internal.PostLoginActivities; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class UserServiceTest { - private static final byte[] SALT = new byte[] { 1, 2, 3 }; - private static final byte[] ENCRYPTED_PASSWORD = new byte[] { 5, 6, 7 }; + private static final byte[] SALT = new byte[] {1, 2, 3}; + private static final byte[] ENCRYPTED_PASSWORD = new byte[] {5, 6, 7}; - @Mock - private CommaFeedConfiguration commaFeedConfiguration; - @Mock - private FeedCategoryDAO feedCategoryDAO; - @Mock - private FeedSubscriptionDAO feedSubscriptionDAO; - @Mock - private UserDAO userDAO; - @Mock - private UserSettingsDAO userSettingsDAO; - @Mock - private UserRoleDAO userRoleDAO; - @Mock - private PasswordEncryptionService passwordEncryptionService; - @Mock - private PostLoginActivities postLoginActivities; + @Mock private CommaFeedConfiguration commaFeedConfiguration; + @Mock private FeedCategoryDAO feedCategoryDAO; + @Mock private FeedSubscriptionDAO feedSubscriptionDAO; + @Mock private UserDAO userDAO; + @Mock private UserSettingsDAO userSettingsDAO; + @Mock private UserRoleDAO userRoleDAO; + @Mock private PasswordEncryptionService passwordEncryptionService; + @Mock private PostLoginActivities postLoginActivities; - private User disabledUser; - private User normalUser; + private User disabledUser; + private User normalUser; - private UserService userService; + private UserService userService; - @BeforeEach - void init() { - userService = new UserService(feedCategoryDAO, feedSubscriptionDAO, userDAO, userRoleDAO, userSettingsDAO, - passwordEncryptionService, commaFeedConfiguration, postLoginActivities); + @BeforeEach + void init() { + userService = + new UserService( + feedCategoryDAO, + feedSubscriptionDAO, + userDAO, + userRoleDAO, + userSettingsDAO, + passwordEncryptionService, + commaFeedConfiguration, + postLoginActivities); - disabledUser = new User(); - disabledUser.setDisabled(true); + disabledUser = new User(); + disabledUser.setDisabled(true); - normalUser = new User(); - normalUser.setDisabled(false); - normalUser.setSalt(SALT); - normalUser.setPassword(ENCRYPTED_PASSWORD); - } + normalUser = new User(); + normalUser.setDisabled(false); + normalUser.setSalt(SALT); + normalUser.setPassword(ENCRYPTED_PASSWORD); + } - @Test - void callingLoginShouldNotReturnUserObjectWhenGivenNullNameOrEmail() { - Optional user = userService.login(null, "password"); - Assertions.assertFalse(user.isPresent()); - } + @Test + void callingLoginShouldNotReturnUserObjectWhenGivenNullNameOrEmail() { + Optional user = userService.login(null, "password"); + Assertions.assertFalse(user.isPresent()); + } - @Test - void callingLoginShouldNotReturnUserObjectWhenGivenNullPassword() { - Optional user = userService.login("testusername", null); - Assertions.assertFalse(user.isPresent()); - } + @Test + void callingLoginShouldNotReturnUserObjectWhenGivenNullPassword() { + Optional user = userService.login("testusername", null); + Assertions.assertFalse(user.isPresent()); + } - @Test - void callingLoginShouldLookupUserByName() { - userService.login("test", "password"); - Mockito.verify(userDAO).findByName("test"); - } + @Test + void callingLoginShouldLookupUserByName() { + userService.login("test", "password"); + Mockito.verify(userDAO).findByName("test"); + } - @Test - void callingLoginShouldLookupUserByEmailIfLookupByNameFailed() { - Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null); - userService.login("test@test.com", "password"); - Mockito.verify(userDAO).findByEmail("test@test.com"); - } + @Test + void callingLoginShouldLookupUserByEmailIfLookupByNameFailed() { + Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null); + userService.login("test@test.com", "password"); + Mockito.verify(userDAO).findByEmail("test@test.com"); + } - @Test - void callingLoginShouldNotReturnUserObjectIfCouldNotFindUserByNameOrEmail() { - Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null); - Mockito.when(userDAO.findByEmail("test@test.com")).thenReturn(null); + @Test + void callingLoginShouldNotReturnUserObjectIfCouldNotFindUserByNameOrEmail() { + Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null); + Mockito.when(userDAO.findByEmail("test@test.com")).thenReturn(null); - Optional user = userService.login("test@test.com", "password"); + Optional user = userService.login("test@test.com", "password"); - Assertions.assertFalse(user.isPresent()); - } + Assertions.assertFalse(user.isPresent()); + } - @Test - void callingLoginShouldNotReturnUserObjectIfUserIsDisabled() { - Mockito.when(userDAO.findByName("test")).thenReturn(disabledUser); - Optional user = userService.login("test", "password"); - Assertions.assertFalse(user.isPresent()); - } + @Test + void callingLoginShouldNotReturnUserObjectIfUserIsDisabled() { + Mockito.when(userDAO.findByName("test")).thenReturn(disabledUser); + Optional user = userService.login("test", "password"); + Assertions.assertFalse(user.isPresent()); + } - @Test - void callingLoginShouldTryToAuthenticateUserWhoIsNotDisabled() { - Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); - Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class))) - .thenReturn(false); + @Test + void callingLoginShouldTryToAuthenticateUserWhoIsNotDisabled() { + Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); + Mockito.when( + passwordEncryptionService.authenticate( + Mockito.anyString(), + Mockito.any(byte[].class), + Mockito.any(byte[].class))) + .thenReturn(false); - userService.login("test", "password"); + userService.login("test", "password"); - Mockito.verify(passwordEncryptionService).authenticate("password", ENCRYPTED_PASSWORD, SALT); - } + Mockito.verify(passwordEncryptionService) + .authenticate("password", ENCRYPTED_PASSWORD, SALT); + } - @Test - void callingLoginShouldNotReturnUserObjectOnUnsuccessfulAuthentication() { - Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); - Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class))) - .thenReturn(false); + @Test + void callingLoginShouldNotReturnUserObjectOnUnsuccessfulAuthentication() { + Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); + Mockito.when( + passwordEncryptionService.authenticate( + Mockito.anyString(), + Mockito.any(byte[].class), + Mockito.any(byte[].class))) + .thenReturn(false); - Optional authenticatedUser = userService.login("test", "password"); + Optional authenticatedUser = userService.login("test", "password"); - Assertions.assertFalse(authenticatedUser.isPresent()); - } + Assertions.assertFalse(authenticatedUser.isPresent()); + } - @Test - void callingLoginShouldExecutePostLoginActivitiesForUserOnSuccessfulAuthentication() { - Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); - Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class))) - .thenReturn(true); - Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class)); + @Test + void callingLoginShouldExecutePostLoginActivitiesForUserOnSuccessfulAuthentication() { + Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); + Mockito.when( + passwordEncryptionService.authenticate( + Mockito.anyString(), + Mockito.any(byte[].class), + Mockito.any(byte[].class))) + .thenReturn(true); + Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class)); - userService.login("test", "password"); + userService.login("test", "password"); - Mockito.verify(postLoginActivities).executeFor(normalUser); - } + Mockito.verify(postLoginActivities).executeFor(normalUser); + } - @Test - void callingLoginShouldReturnUserObjectOnSuccessfulAuthentication() { - Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); - Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class))) - .thenReturn(true); - Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class)); + @Test + void callingLoginShouldReturnUserObjectOnSuccessfulAuthentication() { + Mockito.when(userDAO.findByName("test")).thenReturn(normalUser); + Mockito.when( + passwordEncryptionService.authenticate( + Mockito.anyString(), + Mockito.any(byte[].class), + Mockito.any(byte[].class))) + .thenReturn(true); + Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class)); - Optional authenticatedUser = userService.login("test", "password"); + Optional authenticatedUser = userService.login("test", "password"); - Assertions.assertTrue(authenticatedUser.isPresent()); - Assertions.assertEquals(normalUser, authenticatedUser.get()); - } + Assertions.assertTrue(authenticatedUser.isPresent()); + Assertions.assertEquals(normalUser, authenticatedUser.get()); + } - @Test - void apiLoginShouldNotReturnUserIfApikeyNull() { - Optional user = userService.login(null); - Assertions.assertFalse(user.isPresent()); - } + @Test + void apiLoginShouldNotReturnUserIfApikeyNull() { + Optional user = userService.login(null); + Assertions.assertFalse(user.isPresent()); + } - @Test - void apiLoginShouldLookupUserByApikey() { - Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null); - userService.login("apikey"); - Mockito.verify(userDAO).findByApiKey("apikey"); - } + @Test + void apiLoginShouldLookupUserByApikey() { + Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null); + userService.login("apikey"); + Mockito.verify(userDAO).findByApiKey("apikey"); + } - @Test - void apiLoginShouldNotReturnUserIfUserNotFoundFromLookupByApikey() { - Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null); - Optional user = userService.login("apikey"); - Assertions.assertFalse(user.isPresent()); - } + @Test + void apiLoginShouldNotReturnUserIfUserNotFoundFromLookupByApikey() { + Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null); + Optional user = userService.login("apikey"); + Assertions.assertFalse(user.isPresent()); + } - @Test - void apiLoginShouldNotReturnUserIfUserFoundFromApikeyLookupIsDisabled() { - Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(disabledUser); - Optional user = userService.login("apikey"); - Assertions.assertFalse(user.isPresent()); - } + @Test + void apiLoginShouldNotReturnUserIfUserFoundFromApikeyLookupIsDisabled() { + Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(disabledUser); + Optional user = userService.login("apikey"); + Assertions.assertFalse(user.isPresent()); + } - @Test - void apiLoginShouldPerformPostLoginActivitiesIfUserFoundFromApikeyLookupNotDisabled() { - Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser); - userService.login("apikey"); - Mockito.verify(postLoginActivities).executeFor(normalUser); - } - - @Test - void apiLoginShouldReturnUserIfUserFoundFromApikeyLookupNotDisabled() { - Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser); - Optional returnedUser = userService.login("apikey"); - Assertions.assertEquals(Optional.of(normalUser), returnedUser); - } + @Test + void apiLoginShouldPerformPostLoginActivitiesIfUserFoundFromApikeyLookupNotDisabled() { + Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser); + userService.login("apikey"); + Mockito.verify(postLoginActivities).executeFor(normalUser); + } + @Test + void apiLoginShouldReturnUserIfUserFoundFromApikeyLookupNotDisabled() { + Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser); + Optional returnedUser = userService.login("apikey"); + Assertions.assertEquals(Optional.of(normalUser), returnedUser); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/db/DatabaseCleaningServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/db/DatabaseCleaningServiceTest.java index a1a69fa9..adabed03 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/db/DatabaseCleaningServiceTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/db/DatabaseCleaningServiceTest.java @@ -1,19 +1,5 @@ package com.commafeed.backend.service.db; -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.Callable; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; @@ -24,130 +10,146 @@ import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.Feed; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.Callable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class DatabaseCleaningServiceTest { - private static final int BATCH_SIZE = 100; + private static final int BATCH_SIZE = 100; - @Mock - private CommaFeedConfiguration config; + @Mock private CommaFeedConfiguration config; - @Mock - private CommaFeedConfiguration.Database databaseConfig; + @Mock private CommaFeedConfiguration.Database databaseConfig; - @Mock - private CommaFeedConfiguration.Database.Cleanup cleaningConfig; + @Mock private CommaFeedConfiguration.Database.Cleanup cleaningConfig; - @Mock - private UnitOfWork unitOfWork; + @Mock private UnitOfWork unitOfWork; - @Mock - private FeedDAO feedDAO; + @Mock private FeedDAO feedDAO; - @Mock - private FeedEntryDAO feedEntryDAO; + @Mock private FeedEntryDAO feedEntryDAO; - @Mock - private FeedEntryContentDAO feedEntryContentDAO; + @Mock private FeedEntryContentDAO feedEntryContentDAO; - @Mock - private FeedEntryStatusDAO feedEntryStatusDAO; + @Mock private FeedEntryStatusDAO feedEntryStatusDAO; - @Mock - private MetricRegistry metrics; + @Mock private MetricRegistry metrics; - @Mock - private Meter entriesDeletedMeter; + @Mock private Meter entriesDeletedMeter; - private DatabaseCleaningService service; + private DatabaseCleaningService service; - @BeforeEach - void setUp() { - Mockito.when(config.database()).thenReturn(databaseConfig); - Mockito.when(databaseConfig.cleanup()).thenReturn(cleaningConfig); - Mockito.when(cleaningConfig.batchSize()).thenReturn(BATCH_SIZE); - Mockito.when(metrics.meter(Mockito.anyString())).thenReturn(entriesDeletedMeter); + @BeforeEach + void setUp() { + Mockito.when(config.database()).thenReturn(databaseConfig); + Mockito.when(databaseConfig.cleanup()).thenReturn(cleaningConfig); + Mockito.when(cleaningConfig.batchSize()).thenReturn(BATCH_SIZE); + Mockito.when(metrics.meter(Mockito.anyString())).thenReturn(entriesDeletedMeter); - Mockito.when(unitOfWork.call(Mockito.any())).thenAnswer(invocation -> ((Callable) invocation.getArgument(0)).call()); + Mockito.when(unitOfWork.call(Mockito.any())) + .thenAnswer(invocation -> ((Callable) invocation.getArgument(0)).call()); - service = new DatabaseCleaningService(config, unitOfWork, feedDAO, feedEntryDAO, feedEntryContentDAO, feedEntryStatusDAO, metrics); - } + service = + new DatabaseCleaningService( + config, + unitOfWork, + feedDAO, + feedEntryDAO, + feedEntryContentDAO, + feedEntryStatusDAO, + metrics); + } - @Test - void cleanFeedsWithoutSubscriptionsDeletesFeedsAndEntries() { - Feed feed1 = Mockito.mock(Feed.class); - Feed feed2 = Mockito.mock(Feed.class); - Mockito.when(feed1.getId()).thenReturn(1L); - Mockito.when(feed2.getId()).thenReturn(2L); + @Test + void cleanFeedsWithoutSubscriptionsDeletesFeedsAndEntries() { + Feed feed1 = Mockito.mock(Feed.class); + Feed feed2 = Mockito.mock(Feed.class); + Mockito.when(feed1.getId()).thenReturn(1L); + Mockito.when(feed2.getId()).thenReturn(2L); - // First iteration returns feeds, second returns empty list to terminate loop - Mockito.when(feedDAO.findWithoutSubscriptions(Mockito.anyInt())) - .thenReturn(Arrays.asList(feed1, feed2)) - .thenReturn(Collections.emptyList()); + // First iteration returns feeds, second returns empty list to terminate loop + Mockito.when(feedDAO.findWithoutSubscriptions(Mockito.anyInt())) + .thenReturn(Arrays.asList(feed1, feed2)) + .thenReturn(Collections.emptyList()); - Mockito.when(feedEntryDAO.delete(1L, BATCH_SIZE)).thenReturn(10, 0); - Mockito.when(feedEntryDAO.delete(2L, BATCH_SIZE)).thenReturn(5, 0); - Mockito.when(feedDAO.delete(Mockito.anyList())).thenReturn(2, 0); + Mockito.when(feedEntryDAO.delete(1L, BATCH_SIZE)).thenReturn(10, 0); + Mockito.when(feedEntryDAO.delete(2L, BATCH_SIZE)).thenReturn(5, 0); + Mockito.when(feedDAO.delete(Mockito.anyList())).thenReturn(2, 0); - service.cleanFeedsWithoutSubscriptions(); + service.cleanFeedsWithoutSubscriptions(); - Mockito.verify(entriesDeletedMeter, Mockito.times(4)).mark(Mockito.anyLong()); - Mockito.verify(feedDAO, Mockito.times(2)).delete(Mockito.anyList()); - } + Mockito.verify(entriesDeletedMeter, Mockito.times(4)).mark(Mockito.anyLong()); + Mockito.verify(feedDAO, Mockito.times(2)).delete(Mockito.anyList()); + } - @Test - void cleanContentsWithoutEntriesDeletesContents() { - Mockito.when(feedEntryContentDAO.deleteWithoutEntries(Mockito.anyInt())).thenReturn(50L, 30L, 0L); + @Test + void cleanContentsWithoutEntriesDeletesContents() { + Mockito.when(feedEntryContentDAO.deleteWithoutEntries(Mockito.anyInt())) + .thenReturn(50L, 30L, 0L); - service.cleanContentsWithoutEntries(); + service.cleanContentsWithoutEntries(); - Mockito.verify(feedEntryContentDAO, Mockito.times(3)).deleteWithoutEntries(Mockito.anyInt()); - } + Mockito.verify(feedEntryContentDAO, Mockito.times(3)) + .deleteWithoutEntries(Mockito.anyInt()); + } - @Test - void cleanEntriesForFeedsExceedingCapacityDeletesOldEntries() { - FeedCapacity feed1 = Mockito.mock(FeedCapacity.class); - Mockito.when(feed1.id()).thenReturn(1L); - Mockito.when(feed1.capacity()).thenReturn(180L); + @Test + void cleanEntriesForFeedsExceedingCapacityDeletesOldEntries() { + FeedCapacity feed1 = Mockito.mock(FeedCapacity.class); + Mockito.when(feed1.id()).thenReturn(1L); + Mockito.when(feed1.capacity()).thenReturn(180L); - FeedCapacity feed2 = Mockito.mock(FeedCapacity.class); - Mockito.when(feed2.id()).thenReturn(2L); - Mockito.when(feed2.capacity()).thenReturn(120L); + FeedCapacity feed2 = Mockito.mock(FeedCapacity.class); + Mockito.when(feed2.id()).thenReturn(2L); + Mockito.when(feed2.capacity()).thenReturn(120L); - Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE, false)) - .thenReturn(Arrays.asList(feed1, feed2)) - .thenReturn(Collections.emptyList()); + Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE, false)) + .thenReturn(Arrays.asList(feed1, feed2)) + .thenReturn(Collections.emptyList()); - Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100, false)).thenReturn(80); - Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50, false)).thenReturn(50); - Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70, false)).thenReturn(70); + Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100, false)).thenReturn(80); + Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50, false)).thenReturn(50); + Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70, false)).thenReturn(70); - service.cleanEntriesForFeedsExceedingCapacity(50); + service.cleanEntriesForFeedsExceedingCapacity(50); - Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong()); - } + Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong()); + } - @Test - void cleanEntriesOlderThanDeletesOldEntries() { - Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC); + @Test + void cleanEntriesOlderThanDeletesOldEntries() { + Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC); - Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false)).thenReturn(100, 50, 0); + Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false)) + .thenReturn(100, 50, 0); - service.cleanEntriesOlderThan(cutoff); + service.cleanEntriesOlderThan(cutoff); - Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE, false); - Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong()); - } + Mockito.verify(feedEntryDAO, Mockito.times(3)) + .deleteEntriesOlderThan(cutoff, BATCH_SIZE, false); + Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong()); + } - @Test - void cleanStatusesOlderThanDeletesOldStatuses() { - Instant cutoff = LocalDate.now().minusDays(60).atStartOfDay().toInstant(ZoneOffset.UTC); + @Test + void cleanStatusesOlderThanDeletesOldStatuses() { + Instant cutoff = LocalDate.now().minusDays(60).atStartOfDay().toInstant(ZoneOffset.UTC); - Mockito.when(feedEntryStatusDAO.deleteOldStatuses(cutoff, BATCH_SIZE)).thenReturn(200L, 100L, 0L); + Mockito.when(feedEntryStatusDAO.deleteOldStatuses(cutoff, BATCH_SIZE)) + .thenReturn(200L, 100L, 0L); - service.cleanStatusesOlderThan(cutoff); + service.cleanStatusesOlderThan(cutoff); - Mockito.verify(feedEntryStatusDAO, Mockito.times(3)).deleteOldStatuses(cutoff, BATCH_SIZE); - } -} \ No newline at end of file + Mockito.verify(feedEntryStatusDAO, Mockito.times(3)).deleteOldStatuses(cutoff, BATCH_SIZE); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProviderTest.java b/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProviderTest.java index 6286afe2..20b0a269 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProviderTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProviderTest.java @@ -1,18 +1,18 @@ package com.commafeed.backend.urlprovider; import java.util.List; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class InPageReferenceFeedURLProviderTest { - private final InPageReferenceFeedURLProvider provider = new InPageReferenceFeedURLProvider(); + private final InPageReferenceFeedURLProvider provider = new InPageReferenceFeedURLProvider(); - @Test - void extractUrls() { - String url = "http://example.com"; - String html = """ + @Test + void extractUrls() { + String url = "http://example.com"; + String html = + """ @@ -22,24 +22,28 @@ class InPageReferenceFeedURLProviderTest { """; - Assertions.assertIterableEquals(List.of("http://example.com/feed.atom", "http://example.com/feed.rss"), provider.get(url, html)); - } + Assertions.assertIterableEquals( + List.of("http://example.com/feed.atom", "http://example.com/feed.rss"), + provider.get(url, html)); + } - @Test - void returnsEmptyListForNonHtmlContent() { - String url = "http://example.com"; - String html = """ + @Test + void returnsEmptyListForNonHtmlContent() { + String url = "http://example.com"; + String html = + """ """; - Assertions.assertTrue(provider.get(url, html).isEmpty()); - } + Assertions.assertTrue(provider.get(url, html).isEmpty()); + } - @Test - void returnsEmptyListForHtmlWithoutFeedLinks() { - String url = "http://example.com"; - String html = """ + @Test + void returnsEmptyListForHtmlWithoutFeedLinks() { + String url = "http://example.com"; + String html = + """ @@ -48,6 +52,6 @@ class InPageReferenceFeedURLProviderTest { """; - Assertions.assertTrue(provider.get(url, html).isEmpty()); - } -} \ No newline at end of file + Assertions.assertTrue(provider.get(url, html).isEmpty()); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProviderTest.java b/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProviderTest.java index fec3778a..c3b926cf 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProviderTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProviderTest.java @@ -1,24 +1,24 @@ package com.commafeed.backend.urlprovider; import java.util.List; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class YoutubeFeedURLProviderTest { - private final YoutubeFeedURLProvider provider = new YoutubeFeedURLProvider(); + private final YoutubeFeedURLProvider provider = new YoutubeFeedURLProvider(); - @Test - void matchesYoutubeChannelURL() { - Assertions.assertIterableEquals(List.of("https://www.youtube.com/feeds/videos.xml?channel_id=abc"), - provider.get("https://www.youtube.com/channel/abc", null)); - } + @Test + void matchesYoutubeChannelURL() { + Assertions.assertIterableEquals( + List.of("https://www.youtube.com/feeds/videos.xml?channel_id=abc"), + provider.get("https://www.youtube.com/channel/abc", null)); + } - @Test - void doesNotmatchYoutubeChannelURL() { - Assertions.assertTrue(provider.get("https://www.anothersite.com/channel/abc", null).isEmpty()); - Assertions.assertTrue(provider.get("https://www.youtube.com/user/abc", null).isEmpty()); - } - -} \ No newline at end of file + @Test + void doesNotmatchYoutubeChannelURL() { + Assertions.assertTrue( + provider.get("https://www.anothersite.com/channel/abc", null).isEmpty()); + Assertions.assertTrue(provider.get("https://www.youtube.com/user/abc", null).isEmpty()); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java index fb5aa520..9a288b7c 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java @@ -1,62 +1,60 @@ package com.commafeed.e2e; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.commafeed.TestConstants; import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.Page; import com.microsoft.playwright.assertions.PlaywrightAssertions; import com.microsoft.playwright.options.AriaRole; - import io.quarkiverse.playwright.InjectPlaywright; import io.quarkiverse.playwright.WithPlaywright; import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; @QuarkusTest @WithPlaywright class AuthentificationIT { - @InjectPlaywright - private BrowserContext context; + @InjectPlaywright private BrowserContext context; - @BeforeEach - void setup() { - PlaywrightTestUtils.initialSetup(); - } + @BeforeEach + void setup() { + PlaywrightTestUtils.initialSetup(); + } - @AfterEach - void cleanup() { - context.clearCookies(); - } + @AfterEach + void cleanup() { + context.clearCookies(); + } - @Test - void loginFail() { - Page page = context.newPage(); - page.navigate(getLoginPageUrl()); - PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password"); - PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT)).containsText("wrong username or password"); - } + @Test + void loginFail() { + Page page = context.newPage(); + page.navigate(getLoginPageUrl()); + PlaywrightTestUtils.login(page, TestConstants.ADMIN_USERNAME, "wrong_password"); + PlaywrightAssertions.assertThat(page.getByRole(AriaRole.ALERT)) + .containsText("wrong username or password"); + } - @Test - void loginSuccess() { - Page page = context.newPage(); - page.navigate(getLoginPageUrl()); - PlaywrightTestUtils.login(page); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); - } + @Test + void loginSuccess() { + Page page = context.newPage(); + page.navigate(getLoginPageUrl()); + PlaywrightTestUtils.login(page); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); + } - @Test - void registerSuccess() { - Page page = context.newPage(); - page.navigate(getLoginPageUrl()); - page.getByText("Sign up!").click(); - PlaywrightTestUtils.register(page, "user", "user@domain.com", "MyPassword1!"); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); - } + @Test + void registerSuccess() { + Page page = context.newPage(); + page.navigate(getLoginPageUrl()); + page.getByText("Sign up!").click(); + PlaywrightTestUtils.register(page, "user", "user@domain.com", "MyPassword1!"); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); + } - private String getLoginPageUrl() { - return "http://localhost:8085/#/login"; - } + private String getLoginPageUrl() { + return "http://localhost:8085/#/login"; + } } diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/DocumentationIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/DocumentationIT.java index 4f55c6b7..cdc84917 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/DocumentationIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/DocumentationIT.java @@ -1,27 +1,23 @@ package com.commafeed.e2e; -import org.junit.jupiter.api.Test; - import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.Page; import com.microsoft.playwright.assertions.PlaywrightAssertions; - import io.quarkiverse.playwright.InjectPlaywright; import io.quarkiverse.playwright.WithPlaywright; import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; @QuarkusTest @WithPlaywright class DocumentationIT { - @InjectPlaywright - private BrowserContext context; - - @Test - void documentationAvailable() { - Page page = context.newPage(); - page.navigate("http://localhost:8085/api-documentation"); - PlaywrightAssertions.assertThat(page.getByText("CommaFeed API 1.0.0 OAS")).isVisible(); - } + @InjectPlaywright private BrowserContext context; + @Test + void documentationAvailable() { + Page page = context.newPage(); + page.navigate("http://localhost:8085/api-documentation"); + PlaywrightAssertions.assertThat(page.getByText("CommaFeed API 1.0.0 OAS")).isVisible(); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/InitialSetupIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/InitialSetupIT.java index 69b6879c..102834ed 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/InitialSetupIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/InitialSetupIT.java @@ -1,33 +1,31 @@ package com.commafeed.e2e; -import org.junit.jupiter.api.Test; - import com.commafeed.TestConstants; import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.Page; import com.microsoft.playwright.assertions.PlaywrightAssertions; import com.microsoft.playwright.options.AriaRole; - import io.quarkiverse.playwright.InjectPlaywright; import io.quarkiverse.playwright.WithPlaywright; import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; @QuarkusTest @WithPlaywright class InitialSetupIT { - @InjectPlaywright - private BrowserContext context; + @InjectPlaywright private BrowserContext context; - @Test - void createAdminAccount() { - Page page = context.newPage(); - page.navigate("http://localhost:8085"); + @Test + void createAdminAccount() { + Page page = context.newPage(); + page.navigate("http://localhost:8085"); - page.getByPlaceholder("Admin User Name").fill(TestConstants.ADMIN_USERNAME); - page.getByPlaceholder("Password").fill(TestConstants.ADMIN_PASSWORD); - page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Create Admin Account")).click(); + page.getByPlaceholder("Admin User Name").fill(TestConstants.ADMIN_USERNAME); + page.getByPlaceholder("Password").fill(TestConstants.ADMIN_PASSWORD); + page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Create Admin Account")) + .click(); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); - } + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java index fc4e276f..f9f8bcd2 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java @@ -5,7 +5,6 @@ import com.commafeed.frontend.model.request.InitialSetupRequest; import com.microsoft.playwright.Page; import com.microsoft.playwright.Page.GetByRoleOptions; import com.microsoft.playwright.options.AriaRole; - import io.restassured.RestAssured; import io.restassured.http.ContentType; import lombok.experimental.UtilityClass; @@ -13,29 +12,33 @@ import lombok.experimental.UtilityClass; @UtilityClass public class PlaywrightTestUtils { - public static void initialSetup() { - InitialSetupRequest req = new InitialSetupRequest(); - req.setName(TestConstants.ADMIN_USERNAME); - req.setPassword(TestConstants.ADMIN_PASSWORD); + public static void initialSetup() { + InitialSetupRequest req = new InitialSetupRequest(); + req.setName(TestConstants.ADMIN_USERNAME); + req.setPassword(TestConstants.ADMIN_PASSWORD); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/user/initialSetup").then().statusCode(200); - } + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/initialSetup") + .then() + .statusCode(200); + } - public static void login(Page page) { - login(page, TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + public static void login(Page page) { + login(page, TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - public static void login(Page page, String username, String password) { - page.getByPlaceholder("User Name or E-mail").fill(username); - page.getByPlaceholder("Password").fill(password); - page.getByRole(AriaRole.BUTTON, new GetByRoleOptions().setName("Log in")).click(); - } - - public static void register(Page page, String username, String email, String password) { - page.getByPlaceholder("E-mail address").fill(email); - page.getByPlaceholder("User Name").fill(username); - page.getByPlaceholder("Password").fill(password); - page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")).click(); - } + public static void login(Page page, String username, String password) { + page.getByPlaceholder("User Name or E-mail").fill(username); + page.getByPlaceholder("Password").fill(password); + page.getByRole(AriaRole.BUTTON, new GetByRoleOptions().setName("Log in")).click(); + } + public static void register(Page page, String username, String email, String password) { + page.getByPlaceholder("E-mail address").fill(email); + page.getByPlaceholder("User Name").fill(username); + page.getByPlaceholder("Password").fill(password); + page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")).click(); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java index 1eef3793..bab600ca 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java @@ -1,10 +1,20 @@ package com.commafeed.e2e; +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.Entries; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.assertions.PlaywrightAssertions; +import com.microsoft.playwright.options.AriaRole; +import io.quarkiverse.playwright.InjectPlaywright; +import io.quarkiverse.playwright.WithPlaywright; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; - import org.apache.commons.io.IOUtils; import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; @@ -16,97 +26,99 @@ import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.TestConstants; -import com.commafeed.frontend.model.Entries; -import com.microsoft.playwright.BrowserContext; -import com.microsoft.playwright.Locator; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.assertions.PlaywrightAssertions; -import com.microsoft.playwright.options.AriaRole; - -import io.quarkiverse.playwright.InjectPlaywright; -import io.quarkiverse.playwright.WithPlaywright; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - @QuarkusTest @WithPlaywright class ReadingIT { - @InjectPlaywright - private BrowserContext context; + @InjectPlaywright private BrowserContext context; - private MockServerClient mockServerClient; + private MockServerClient mockServerClient; - @BeforeEach - void init() throws IOException { - this.mockServerClient = ClientAndServer.startClientAndServer(0); - this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response() - .withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8)) - .withDelay(TimeUnit.MILLISECONDS, 100)); + @BeforeEach + void init() throws IOException { + this.mockServerClient = ClientAndServer.startClientAndServer(0); + this.mockServerClient + .when(HttpRequest.request().withMethod("GET")) + .respond( + HttpResponse.response() + .withBody( + IOUtils.toString( + getClass().getResource("/feed/rss.xml"), + StandardCharsets.UTF_8)) + .withDelay(TimeUnit.MILLISECONDS, 100)); - PlaywrightTestUtils.initialSetup(); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + PlaywrightTestUtils.initialSetup(); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Test - void scenario() { - Page page = context.newPage(); + @Test + void scenario() { + Page page = context.newPage(); - // login - page.navigate("http://localhost:8085"); - page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Log in")).click(); - PlaywrightTestUtils.login(page); + // login + page.navigate("http://localhost:8085"); + page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Log in")).click(); + PlaywrightTestUtils.login(page); - Locator header = page.getByRole(AriaRole.BANNER); - Locator sidebar = page.getByRole(AriaRole.NAVIGATION); - Locator main = page.getByRole(AriaRole.MAIN); + Locator header = page.getByRole(AriaRole.BANNER); + Locator sidebar = page.getByRole(AriaRole.NAVIGATION); + Locator main = page.getByRole(AriaRole.MAIN); - PlaywrightAssertions.assertThat(main.getByText("You don't have any subscriptions yet.")).hasCount(1); + PlaywrightAssertions.assertThat(main.getByText("You don't have any subscriptions yet.")) + .hasCount(1); - // subscribe - header.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Subscribe")).click(); - main.getByText("Feed URL *").fill("http://localhost:" + this.mockServerClient.getPort()); - main.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Next")).click(); - main.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Subscribe").setExact(true)).click(); + // subscribe + header.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Subscribe")) + .click(); + main.getByText("Feed URL *").fill("http://localhost:" + this.mockServerClient.getPort()); + main.getByRole(AriaRole.BUTTON, new Locator.GetByRoleOptions().setName("Next")).click(); + main.getByRole( + AriaRole.BUTTON, + new Locator.GetByRoleOptions().setName("Subscribe").setExact(true)) + .click(); - // click on subscription - sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click(); + // click on subscription + sidebar.getByText(Pattern.compile("CommaFeed test feed\\d+")).click(); - // we have two unread entries - PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(2); + // we have two unread entries + PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(2); - // click on first entry - main.getByText("Item 1").click(); - PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(1); - PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(0); + // click on first entry + main.getByText("Item 1").click(); + PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(1); + PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(0); - // wait for the entry to be marked as read since the UI is updated immediately while the entry is marked as read in the background - Awaitility.await() - .atMost(15, TimeUnit.SECONDS) - .until(() -> RestAssured.given() - .get("rest/category/entries?id=all&readType=unread") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class), e -> e.getEntries().size() == 1); + // wait for the entry to be marked as read since the UI is updated immediately while the + // entry + // is marked as read in the background + Awaitility.await() + .atMost(15, TimeUnit.SECONDS) + .until( + () -> + RestAssured.given() + .get("rest/category/entries?id=all&readType=unread") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class), + e -> e.getEntries().size() == 1); - // click on subscription - sidebar.getByText(Pattern.compile("CommaFeed test feed\\d*")).click(); + // click on subscription + sidebar.getByText(Pattern.compile("CommaFeed test feed\\d*")).click(); - // only one unread entry now - PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(1); - - // click on second entry - main.getByText("Item 2").click(); - PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(0); - PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(1); - } + // only one unread entry now + PlaywrightAssertions.assertThat(main.getByRole(AriaRole.ARTICLE)).hasCount(1); + // click on second entry + main.getByText("Item 2").click(); + PlaywrightAssertions.assertThat(main.getByText("Item 1 description")).hasCount(0); + PlaywrightAssertions.assertThat(main.getByText("Item 2 description")).hasCount(1); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/ws/WebSocketSessionsTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/ws/WebSocketSessionsTest.java index f26ca314..7c327a3d 100644 --- a/commafeed-server/src/test/java/com/commafeed/frontend/ws/WebSocketSessionsTest.java +++ b/commafeed-server/src/test/java/com/commafeed/frontend/ws/WebSocketSessionsTest.java @@ -1,7 +1,8 @@ package com.commafeed.frontend.ws; +import com.codahale.metrics.MetricRegistry; +import com.commafeed.backend.model.User; import jakarta.websocket.Session; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,73 +11,69 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.codahale.metrics.MetricRegistry; -import com.commafeed.backend.model.User; - @ExtendWith(MockitoExtension.class) class WebSocketSessionsTest { - @Mock - private MetricRegistry metrics; + @Mock private MetricRegistry metrics; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Session session1; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Session session1; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Session session2; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Session session2; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Session session3; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Session session3; - private WebSocketSessions webSocketSessions; + private WebSocketSessions webSocketSessions; - @BeforeEach - void init() { - webSocketSessions = new WebSocketSessions(metrics); - } + @BeforeEach + void init() { + webSocketSessions = new WebSocketSessions(metrics); + } - @Test - void sendsMessageToUser() { - Mockito.when(session1.isOpen()).thenReturn(true); - Mockito.when(session2.isOpen()).thenReturn(true); + @Test + void sendsMessageToUser() { + Mockito.when(session1.isOpen()).thenReturn(true); + Mockito.when(session2.isOpen()).thenReturn(true); - User user1 = newUser(1L); - webSocketSessions.add(user1.getId(), session1); - webSocketSessions.add(user1.getId(), session2); + User user1 = newUser(1L); + webSocketSessions.add(user1.getId(), session1); + webSocketSessions.add(user1.getId(), session2); - User user2 = newUser(2L); - webSocketSessions.add(user2.getId(), session3); + User user2 = newUser(2L); + webSocketSessions.add(user2.getId(), session3); - webSocketSessions.sendMessage(user1, "Hello"); - Mockito.verify(session1).getAsyncRemote(); - Mockito.verify(session2).getAsyncRemote(); - Mockito.verifyNoInteractions(session3); - } + webSocketSessions.sendMessage(user1, "Hello"); + Mockito.verify(session1).getAsyncRemote(); + Mockito.verify(session2).getAsyncRemote(); + Mockito.verifyNoInteractions(session3); + } - @Test - void closedSessionsAreNotNotified() { - Mockito.when(session1.isOpen()).thenReturn(false); + @Test + void closedSessionsAreNotNotified() { + Mockito.when(session1.isOpen()).thenReturn(false); - User user1 = newUser(1L); - webSocketSessions.add(user1.getId(), session1); + User user1 = newUser(1L); + webSocketSessions.add(user1.getId(), session1); - webSocketSessions.sendMessage(user1, "Hello"); - Mockito.verify(session1, Mockito.never()).getAsyncRemote(); - } + webSocketSessions.sendMessage(user1, "Hello"); + Mockito.verify(session1, Mockito.never()).getAsyncRemote(); + } - @Test - void removedSessionsAreNotNotified() { - User user1 = newUser(1L); - webSocketSessions.add(user1.getId(), session1); - webSocketSessions.remove(session1); + @Test + void removedSessionsAreNotNotified() { + User user1 = newUser(1L); + webSocketSessions.add(user1.getId(), session1); + webSocketSessions.remove(session1); - webSocketSessions.sendMessage(user1, "Hello"); - Mockito.verifyNoInteractions(session1); - } + webSocketSessions.sendMessage(user1, "Hello"); + Mockito.verifyNoInteractions(session1); + } - private User newUser(Long userId) { - User user = new User(); - user.setId(userId); - return user; - } -} \ No newline at end of file + private User newUser(Long userId) { + User user = new User(); + user.setId(userId); + return user; + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java index ad23dd9f..7f1a59e5 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java @@ -1,5 +1,19 @@ package com.commafeed.integration; +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.Category; +import com.commafeed.frontend.model.Entries; +import com.commafeed.frontend.model.Subscription; +import com.commafeed.frontend.model.request.AddCategoryRequest; +import com.commafeed.frontend.model.request.InitialSetupRequest; +import com.commafeed.frontend.model.request.SubscribeRequest; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.http.ContentType; +import io.restassured.http.Header; +import io.restassured.mapper.ObjectMapperType; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.HttpHeaders; import java.io.IOException; import java.net.HttpCookie; import java.net.URL; @@ -7,10 +21,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; import java.util.Objects; - -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.core.HttpHeaders; - +import lombok.Getter; import org.apache.commons.io.IOUtils; import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; @@ -21,192 +32,212 @@ import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.TestConstants; -import com.commafeed.frontend.model.Category; -import com.commafeed.frontend.model.Entries; -import com.commafeed.frontend.model.Subscription; -import com.commafeed.frontend.model.request.AddCategoryRequest; -import com.commafeed.frontend.model.request.InitialSetupRequest; -import com.commafeed.frontend.model.request.SubscribeRequest; - -import io.restassured.RestAssured; -import io.restassured.config.ObjectMapperConfig; -import io.restassured.http.ContentType; -import io.restassured.http.Header; -import io.restassured.mapper.ObjectMapperType; -import lombok.Getter; - @Getter public abstract class BaseIT { - private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/"); + private static final HttpRequest FEED_REQUEST = + HttpRequest.request().withMethod("GET").withPath("/"); - @Getter - private MockServerClient mockServerClient; - private Client client; - private String feedUrl; - private String baseUrl; - private String apiBaseUrl; - private String webSocketUrl; + @Getter private MockServerClient mockServerClient; + private Client client; + private String feedUrl; + private String baseUrl; + private String apiBaseUrl; + private String webSocketUrl; - @BeforeEach - void beforeEach() throws IOException { - this.mockServerClient = ClientAndServer.startClientAndServer(0); + @BeforeEach + void beforeEach() throws IOException { + this.mockServerClient = ClientAndServer.startClientAndServer(0); - this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/"; - this.baseUrl = "http://localhost:8085/"; - this.apiBaseUrl = this.baseUrl + "rest/"; - this.webSocketUrl = "ws://localhost:8085/ws"; + this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/"; + this.baseUrl = "http://localhost:8085/"; + this.apiBaseUrl = this.baseUrl + "rest/"; + this.webSocketUrl = "ws://localhost:8085/ws"; - URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml")); - this.mockServerClient.when(FEED_REQUEST) - .respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); + URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml")); + this.mockServerClient + .when(FEED_REQUEST) + .respond( + HttpResponse.response() + .withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); - // remove when Quarkus supports Jackson 3 - RestAssured.config = RestAssured.config().objectMapperConfig(new ObjectMapperConfig(ObjectMapperType.JACKSON_2)); - } + // remove when Quarkus supports Jackson 3 + RestAssured.config = + RestAssured.config() + .objectMapperConfig(new ObjectMapperConfig(ObjectMapperType.JACKSON_2)); + } - @AfterEach - void afterEach() { - if (this.mockServerClient != null) { - this.mockServerClient.close(); - } + @AfterEach + void afterEach() { + if (this.mockServerClient != null) { + this.mockServerClient.close(); + } - if (this.client != null) { - this.client.close(); - } - } + if (this.client != null) { + this.client.close(); + } + } - protected void feedNowReturnsMoreEntries() throws IOException { - mockServerClient.clear(FEED_REQUEST); + protected void feedNowReturnsMoreEntries() throws IOException { + mockServerClient.clear(FEED_REQUEST); - URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss_2.xml")); - mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); - } + URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss_2.xml")); + mockServerClient + .when(FEED_REQUEST) + .respond( + HttpResponse.response() + .withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); + } - protected void initialSetup(String userName, String password) { - InitialSetupRequest req = new InitialSetupRequest(); - req.setName(userName); - req.setPassword(password); - req.setEmail(userName + "@commafeed.com"); + protected void initialSetup(String userName, String password) { + InitialSetupRequest req = new InitialSetupRequest(); + req.setName(userName); + req.setPassword(password); + req.setEmail(userName + "@commafeed.com"); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/user/initialSetup").then().statusCode(200); - } + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/initialSetup") + .then() + .statusCode(200); + } - protected List login() { - List
setCookieHeaders = RestAssured.given() - .auth() - .none() - .formParams("j_username", TestConstants.ADMIN_USERNAME, "j_password", TestConstants.ADMIN_PASSWORD) - .post("j_security_check") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .headers() - .getList(HttpHeaders.SET_COOKIE); - return setCookieHeaders.stream().flatMap(h -> HttpCookie.parse(h.getValue()).stream()).toList(); - } + protected List login() { + List
setCookieHeaders = + RestAssured.given() + .auth() + .none() + .formParams( + "j_username", + TestConstants.ADMIN_USERNAME, + "j_password", + TestConstants.ADMIN_PASSWORD) + .post("j_security_check") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .headers() + .getList(HttpHeaders.SET_COOKIE); + return setCookieHeaders.stream() + .flatMap(h -> HttpCookie.parse(h.getValue()).stream()) + .toList(); + } - protected String createCategory(String name) { - AddCategoryRequest addCategoryRequest = new AddCategoryRequest(); - addCategoryRequest.setName(name); - return RestAssured.given() - .body(addCategoryRequest) - .contentType(ContentType.JSON) - .post("rest/category/add") - .then() - .extract() - .as(String.class); - } + protected String createCategory(String name) { + AddCategoryRequest addCategoryRequest = new AddCategoryRequest(); + addCategoryRequest.setName(name); + return RestAssured.given() + .body(addCategoryRequest) + .contentType(ContentType.JSON) + .post("rest/category/add") + .then() + .extract() + .as(String.class); + } - protected Category getRootCategory() { - return RestAssured.given().get("rest/category/get").then().statusCode(HttpStatus.SC_OK).extract().as(Category.class); - } + protected Category getRootCategory() { + return RestAssured.given() + .get("rest/category/get") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Category.class); + } - protected Long subscribe(String feedUrl) { - return subscribe(feedUrl, null); - } + protected Long subscribe(String feedUrl) { + return subscribe(feedUrl, null); + } - protected Long subscribe(String feedUrl, String categoryId) { - SubscribeRequest subscribeRequest = new SubscribeRequest(); - subscribeRequest.setUrl(feedUrl); - subscribeRequest.setTitle("my title for this feed"); - subscribeRequest.setCategoryId(categoryId); - return RestAssured.given() - .body(subscribeRequest) - .contentType(ContentType.JSON) - .post("rest/feed/subscribe") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Long.class); - } + protected Long subscribe(String feedUrl, String categoryId) { + SubscribeRequest subscribeRequest = new SubscribeRequest(); + subscribeRequest.setUrl(feedUrl); + subscribeRequest.setTitle("my title for this feed"); + subscribeRequest.setCategoryId(categoryId); + return RestAssured.given() + .body(subscribeRequest) + .contentType(ContentType.JSON) + .post("rest/feed/subscribe") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Long.class); + } - protected Long subscribeAndWaitForEntries(String feedUrl) { - return subscribeAndWaitForEntries(feedUrl, null); - } + protected Long subscribeAndWaitForEntries(String feedUrl) { + return subscribeAndWaitForEntries(feedUrl, null); + } - protected Long subscribeAndWaitForEntries(String feedUrl, String categoryId) { - Long subscriptionId = subscribe(feedUrl, categoryId); - Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2); - return subscriptionId; - } + protected Long subscribeAndWaitForEntries(String feedUrl, String categoryId) { + Long subscriptionId = subscribe(feedUrl, categoryId); + Awaitility.await() + .atMost(Duration.ofSeconds(15)) + .until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2); + return subscriptionId; + } - protected Subscription getSubscription(Long subscriptionId) { - return RestAssured.given() - .get("rest/feed/get/{id}", subscriptionId) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Subscription.class); - } + protected Subscription getSubscription(Long subscriptionId) { + return RestAssured.given() + .get("rest/feed/get/{id}", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Subscription.class); + } - protected Entries getFeedEntries(long subscriptionId) { - return RestAssured.given() - .get("rest/feed/entries?id={id}&readType=all", subscriptionId) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - } + protected Entries getFeedEntries(long subscriptionId) { + return RestAssured.given() + .get("rest/feed/entries?id={id}&readType=all", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + } - protected Entries getCategoryEntries(String categoryId) { - return RestAssured.given() - .get("rest/category/entries?id={id}&readType=all", categoryId) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - } + protected Entries getCategoryEntries(String categoryId) { + return RestAssured.given() + .get("rest/category/entries?id={id}&readType=all", categoryId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + } - protected Entries getCategoryEntries(String categoryId, int offset, int limit) { - return RestAssured.given() - .get("rest/category/entries?id={id}&readType=all&offset={offset}&limit={limit}", categoryId, offset, limit) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - } + protected Entries getCategoryEntries(String categoryId, int offset, int limit) { + return RestAssured.given() + .get( + "rest/category/entries?id={id}&readType=all&offset={offset}&limit={limit}", + categoryId, + offset, + limit) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + } - protected Entries getCategoryEntries(String categoryId, String keywords) { - return RestAssured.given() - .get("rest/category/entries?id={id}&readType=all&keywords={keywords}", categoryId, keywords) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - } + protected Entries getCategoryEntries(String categoryId, String keywords) { + return RestAssured.given() + .get( + "rest/category/entries?id={id}&readType=all&keywords={keywords}", + categoryId, + keywords) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + } - protected Entries getTaggedEntries(String tag) { - return RestAssured.given() - .get("rest/category/entries?id=all&readType=all&tag={tag}", tag) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - } + protected Entries getTaggedEntries(String tag) { + return RestAssured.given() + .get("rest/category/entries?id=all&readType=all&tag={tag}", tag) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + } - protected int forceRefreshAllFeeds() { - return RestAssured.given().get("rest/feed/refreshAll").then().extract().statusCode(); - } + protected int forceRefreshAllFeeds() { + return RestAssured.given().get("rest/feed/refreshAll").then().extract().statusCode(); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/CompressionIT.java b/commafeed-server/src/test/java/com/commafeed/integration/CompressionIT.java index 70ed88cd..9a1d069c 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/CompressionIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/CompressionIT.java @@ -1,19 +1,22 @@ package com.commafeed.integration; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - import com.google.common.net.HttpHeaders; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; @QuarkusTest class CompressionIT { - @ParameterizedTest - @ValueSource(strings = { "/rest/server/get", "/" }) - void servedWithCompression(String path) { - RestAssured.given().when().get(path).then().statusCode(200).header(HttpHeaders.CONTENT_ENCODING, "gzip"); - } + @ParameterizedTest + @ValueSource(strings = {"/rest/server/get", "/"}) + void servedWithCompression(String path) { + RestAssured.given() + .when() + .get(path) + .then() + .statusCode(200) + .header(HttpHeaders.CONTENT_ENCODING, "gzip"); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java b/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java index ac4c7546..2f4b424d 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/PushNotificationIT.java @@ -1,8 +1,15 @@ package com.commafeed.integration; +import com.commafeed.TestConstants; +import com.commafeed.backend.model.UserSettings.PushNotificationType; +import com.commafeed.frontend.model.Settings; +import com.commafeed.frontend.model.Subscription; +import com.commafeed.frontend.model.request.FeedModificationRequest; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; import java.io.IOException; import java.time.Duration; - import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; @@ -12,64 +19,69 @@ import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import org.mockserver.verify.VerificationTimes; -import com.commafeed.TestConstants; -import com.commafeed.backend.model.UserSettings.PushNotificationType; -import com.commafeed.frontend.model.Settings; -import com.commafeed.frontend.model.Subscription; -import com.commafeed.frontend.model.request.FeedModificationRequest; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; - @QuarkusTest class PushNotificationIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void tearDown() { - RestAssured.reset(); - } + @AfterEach + void tearDown() { + RestAssured.reset(); + } - @Test - void receivedPushNotifications() throws IOException { - // mock ntfy server - HttpRequest ntfyPost = HttpRequest.request().withMethod("POST").withPath("/ntfy/integration-test"); - getMockServerClient().when(ntfyPost).respond(HttpResponse.response().withStatusCode(200)); + @Test + void receivedPushNotifications() throws IOException { + // mock ntfy server + HttpRequest ntfyPost = + HttpRequest.request().withMethod("POST").withPath("/ntfy/integration-test"); + getMockServerClient().when(ntfyPost).respond(HttpResponse.response().withStatusCode(200)); - // enable push notifications - Settings settings = RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class); - settings.getPushNotificationSettings().setType(PushNotificationType.NTFY); - settings.getPushNotificationSettings().setServerUrl("http://localhost:" + getMockServerClient().getPort() + "/ntfy"); - settings.getPushNotificationSettings().setTopic("integration-test"); - RestAssured.given().body(settings).contentType(ContentType.JSON).post("rest/user/settings").then().statusCode(200); + // enable push notifications + Settings settings = + RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class); + settings.getPushNotificationSettings().setType(PushNotificationType.NTFY); + settings.getPushNotificationSettings() + .setServerUrl("http://localhost:" + getMockServerClient().getPort() + "/ntfy"); + settings.getPushNotificationSettings().setTopic("integration-test"); + RestAssured.given() + .body(settings) + .contentType(ContentType.JSON) + .post("rest/user/settings") + .then() + .statusCode(200); - // subscribe - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Subscription subscription = getSubscription(subscriptionId); + // subscribe + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Subscription subscription = getSubscription(subscriptionId); - // enable push notifications - FeedModificationRequest req = new FeedModificationRequest(); - req.setId(subscriptionId); - req.setName(subscription.getName()); - req.setCategoryId(subscription.getCategoryId()); - req.setPosition(1); - req.setPushNotificationsEnabled(true); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK); + // enable push notifications + FeedModificationRequest req = new FeedModificationRequest(); + req.setId(subscriptionId); + req.setName(subscription.getName()); + req.setCategoryId(subscription.getCategoryId()); + req.setPosition(1); + req.setPushNotificationsEnabled(true); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/feed/modify") + .then() + .statusCode(HttpStatus.SC_OK); - // receive two additional entries, those will trigger two push notifications - feedNowReturnsMoreEntries(); - forceRefreshAllFeeds(); - - // await push notification for the two entries in the feed - Awaitility.await() - .atMost(Duration.ofSeconds(20)) - .untilAsserted(() -> getMockServerClient().verify(ntfyPost, VerificationTimes.exactly(2))); - } + // receive two additional entries, those will trigger two push notifications + feedNowReturnsMoreEntries(); + forceRefreshAllFeeds(); + // await push notification for the two entries in the feed + Awaitility.await() + .atMost(Duration.ofSeconds(20)) + .untilAsserted( + () -> getMockServerClient().verify(ntfyPost, VerificationTimes.exactly(2))); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java index fe17da63..4dc64ec5 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java @@ -1,16 +1,5 @@ package com.commafeed.integration; -import java.net.HttpCookie; -import java.util.List; -import java.util.stream.Collectors; - -import jakarta.ws.rs.core.HttpHeaders; - -import org.apache.hc.core5.http.HttpStatus; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.commafeed.ExceptionMappers.UnauthorizedResponse; import com.commafeed.TestConstants; import com.commafeed.frontend.model.Entries; @@ -18,135 +7,154 @@ import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.SubscribeRequest; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import jakarta.ws.rs.core.HttpHeaders; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; @QuarkusTest class SecurityIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @Test - void notLoggedIn() { - UnauthorizedResponse info = RestAssured.given() - .get("rest/user/profile") - .then() - .statusCode(HttpStatus.SC_UNAUTHORIZED) - .extract() - .as(UnauthorizedResponse.class); - Assertions.assertTrue(info.allowRegistrations()); - } + @Test + void notLoggedIn() { + UnauthorizedResponse info = + RestAssured.given() + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED) + .extract() + .as(UnauthorizedResponse.class); + Assertions.assertTrue(info.allowRegistrations()); + } - @Test - void formLogin() { - List cookies = login(); - cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0)); + @Test + void formLogin() { + List cookies = login(); + cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0)); - RestAssured.given() - .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .get("rest/user/profile") - .then() - .statusCode(HttpStatus.SC_OK); - } + RestAssured.given() + .header( + HttpHeaders.COOKIE, + cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); + } - @Test - void basicAuthLogin() { - RestAssured.given() - .auth() - .preemptive() - .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) - .get("rest/user/profile") - .then() - .statusCode(HttpStatus.SC_OK); - } + @Test + void basicAuthLogin() { + RestAssured.given() + .auth() + .preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); + } - @Test - void wrongPassword() { - RestAssured.given() - .auth() - .preemptive() - .basic(TestConstants.ADMIN_USERNAME, "wrong-password") - .get("rest/user/profile") - .then() - .statusCode(HttpStatus.SC_UNAUTHORIZED); - } + @Test + void wrongPassword() { + RestAssured.given() + .auth() + .preemptive() + .basic(TestConstants.ADMIN_USERNAME, "wrong-password") + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + } - @Test - void missingRole() { - RestAssured.given().auth().preemptive().basic("demo", "demo").get("rest/admin/metrics").then().statusCode(HttpStatus.SC_FORBIDDEN); - } + @Test + void missingRole() { + RestAssured.given() + .auth() + .preemptive() + .basic("demo", "demo") + .get("rest/admin/metrics") + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } - @Test - void apiKey() { - // create api key - ProfileModificationRequest req = new ProfileModificationRequest(); - req.setCurrentPassword(TestConstants.ADMIN_PASSWORD); - req.setNewApiKey(true); - RestAssured.given() - .auth() - .preemptive() - .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) - .body(req) - .contentType(ContentType.JSON) - .post("rest/user/profile") - .then() - .statusCode(HttpStatus.SC_OK); + @Test + void apiKey() { + // create api key + ProfileModificationRequest req = new ProfileModificationRequest(); + req.setCurrentPassword(TestConstants.ADMIN_PASSWORD); + req.setNewApiKey(true); + RestAssured.given() + .auth() + .preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); - // fetch api key - String apiKey = RestAssured.given() - .auth() - .preemptive() - .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) - .get("rest/user/profile") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(UserModel.class) - .getApiKey(); + // fetch api key + String apiKey = + RestAssured.given() + .auth() + .preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(UserModel.class) + .getApiKey(); - // subscribe to a feed - SubscribeRequest subscribeRequest = new SubscribeRequest(); - subscribeRequest.setUrl(getFeedUrl()); - subscribeRequest.setTitle("my title for this feed"); - long subscriptionId = RestAssured.given() - .auth() - .preemptive() - .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) - .body(subscribeRequest) - .contentType(ContentType.JSON) - .post("rest/feed/subscribe") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Long.class); + // subscribe to a feed + SubscribeRequest subscribeRequest = new SubscribeRequest(); + subscribeRequest.setUrl(getFeedUrl()); + subscribeRequest.setTitle("my title for this feed"); + long subscriptionId = + RestAssured.given() + .auth() + .preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD) + .body(subscribeRequest) + .contentType(ContentType.JSON) + .post("rest/feed/subscribe") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Long.class); - // get entries with api key - Entries entries = RestAssured.given() - .queryParam("id", subscriptionId) - .queryParam("readType", "unread") - .queryParam("apiKey", apiKey) - .get("rest/feed/entries") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - Assertions.assertEquals("my title for this feed", entries.getName()); + // get entries with api key + Entries entries = + RestAssured.given() + .queryParam("id", subscriptionId) + .queryParam("readType", "unread") + .queryParam("apiKey", apiKey) + .get("rest/feed/entries") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + Assertions.assertEquals("my title for this feed", entries.getName()); - // mark entry as read and expect it won't work because it's not a GET request - MarkRequest markRequest = new MarkRequest(); - markRequest.setId("1"); - markRequest.setRead(true); - RestAssured.given() - .body(markRequest) - .contentType(ContentType.JSON) - .queryParam("apiKey", apiKey) - .post("rest/entry/mark") - .then() - .statusCode(HttpStatus.SC_UNAUTHORIZED); - } + // mark entry as read and expect it won't work because it's not a GET request + MarkRequest markRequest = new MarkRequest(); + markRequest.setId("1"); + markRequest.setRead(true); + RestAssured.given() + .body(markRequest) + .contentType(ContentType.JSON) + .queryParam("apiKey", apiKey) + .post("rest/entry/mark") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/StaticFilesIT.java b/commafeed-server/src/test/java/com/commafeed/integration/StaticFilesIT.java index 145b3d3a..1e7f2b46 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/StaticFilesIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/StaticFilesIT.java @@ -1,23 +1,32 @@ package com.commafeed.integration; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; @QuarkusTest class StaticFilesIT { - @ParameterizedTest - @ValueSource(strings = { "/", "/openapi" }) - void servedWithoutCache(String path) { - RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "no-cache"); - } + @ParameterizedTest + @ValueSource(strings = {"/", "/openapi"}) + void servedWithoutCache(String path) { + RestAssured.given() + .when() + .get(path) + .then() + .statusCode(200) + .header("Cache-Control", "no-cache"); + } - @ParameterizedTest - @ValueSource(strings = { "/favicon.ico" }) - void servedWithCache(String path) { - RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=31536000"); - } + @ParameterizedTest + @ValueSource(strings = {"/favicon.ico"}) + void servedWithCache(String path) { + RestAssured.given() + .when() + .get(path) + .then() + .statusCode(200) + .header("Cache-Control", "public, immutable, max-age=31536000"); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java index 492dab6e..a9df1588 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java @@ -1,5 +1,18 @@ package com.commafeed.integration; +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.request.FeedModificationRequest; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.CloseReason; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import jakarta.ws.rs.core.HttpHeaders; import java.io.IOException; import java.net.HttpCookie; import java.net.URI; @@ -10,16 +23,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; - -import jakarta.websocket.ClientEndpointConfig; -import jakarta.websocket.CloseReason; -import jakarta.websocket.ContainerProvider; -import jakarta.websocket.DeploymentException; -import jakarta.websocket.Endpoint; -import jakarta.websocket.EndpointConfig; -import jakarta.websocket.Session; -import jakarta.ws.rs.core.HttpHeaders; - +import lombok.extern.slf4j.Slf4j; import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; @@ -27,137 +31,163 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.commafeed.TestConstants; -import com.commafeed.frontend.model.request.FeedModificationRequest; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import lombok.extern.slf4j.Slf4j; - @QuarkusTest @Slf4j class WebSocketIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void tearDown() { - RestAssured.reset(); - } + @AfterEach + void tearDown() { + RestAssured.reset(); + } - @Test - void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException { - AtomicBoolean connected = new AtomicBoolean(); - AtomicReference closeReasonRef = new AtomicReference<>(); - try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { - @Override - public void onOpen(Session session, EndpointConfig config) { - connected.set(true); - } + @Test + void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException { + AtomicBoolean connected = new AtomicBoolean(); + AtomicReference closeReasonRef = new AtomicReference<>(); + try (Session session = + ContainerProvider.getWebSocketContainer() + .connectToServer( + new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig config) { + connected.set(true); + } - @Override - public void onClose(Session session, CloseReason closeReason) { - closeReasonRef.set(closeReason); - } - }, buildConfig(List.of()), URI.create(getWebSocketUrl()))) { - Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); - log.info("connected to {}", session.getRequestURI()); + @Override + public void onClose(Session session, CloseReason closeReason) { + closeReasonRef.set(closeReason); + } + }, + buildConfig(List.of()), + URI.create(getWebSocketUrl()))) { + Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); + log.info("connected to {}", session.getRequestURI()); - Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> closeReasonRef.get() != null); - } - } + Awaitility.await() + .atMost(15, TimeUnit.SECONDS) + .until(() -> closeReasonRef.get() != null); + } + } - @Test - void subscribeAndGetsNotified() throws DeploymentException, IOException { - List cookies = login(); + @Test + void subscribeAndGetsNotified() throws DeploymentException, IOException { + List cookies = login(); - AtomicBoolean connected = new AtomicBoolean(); - AtomicReference messageRef = new AtomicReference<>(); - try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { - @Override - public void onOpen(Session session, EndpointConfig config) { - session.addMessageHandler(String.class, messageRef::set); - connected.set(true); - } - }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { - Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); - log.info("connected to {}", session.getRequestURI()); + AtomicBoolean connected = new AtomicBoolean(); + AtomicReference messageRef = new AtomicReference<>(); + try (Session session = + ContainerProvider.getWebSocketContainer() + .connectToServer( + new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(String.class, messageRef::set); + connected.set(true); + } + }, + buildConfig(cookies), + URI.create(getWebSocketUrl()))) { + Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); + log.info("connected to {}", session.getRequestURI()); - Long subscriptionId = subscribe(getFeedUrl()); + Long subscriptionId = subscribe(getFeedUrl()); - Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null); - Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":2", messageRef.get()); - } - } + Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null); + Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":2", messageRef.get()); + } + } - @Test - void notNotifiedForFilteredEntries() throws DeploymentException, IOException { - List cookies = login(); - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void notNotifiedForFilteredEntries() throws DeploymentException, IOException { + List cookies = login(); + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - FeedModificationRequest req = new FeedModificationRequest(); - req.setId(subscriptionId); - req.setName("feed-name"); - req.setFilter("!titleLower.contains('item 4')"); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK); + FeedModificationRequest req = new FeedModificationRequest(); + req.setId(subscriptionId); + req.setName("feed-name"); + req.setFilter("!titleLower.contains('item 4')"); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/feed/modify") + .then() + .statusCode(HttpStatus.SC_OK); - AtomicBoolean connected = new AtomicBoolean(); - AtomicReference messageRef = new AtomicReference<>(); - try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { - @Override - public void onOpen(Session session, EndpointConfig config) { - session.addMessageHandler(String.class, messageRef::set); - connected.set(true); - } - }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { - Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); - log.info("connected to {}", session.getRequestURI()); + AtomicBoolean connected = new AtomicBoolean(); + AtomicReference messageRef = new AtomicReference<>(); + try (Session session = + ContainerProvider.getWebSocketContainer() + .connectToServer( + new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(String.class, messageRef::set); + connected.set(true); + } + }, + buildConfig(cookies), + URI.create(getWebSocketUrl()))) { + Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); + log.info("connected to {}", session.getRequestURI()); - feedNowReturnsMoreEntries(); - forceRefreshAllFeeds(); + feedNowReturnsMoreEntries(); + forceRefreshAllFeeds(); - Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null); - Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":1", messageRef.get()); - } + Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null); + Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":1", messageRef.get()); + } + } - } + @Test + void pingPong() throws DeploymentException, IOException { + List cookies = login(); - @Test - void pingPong() throws DeploymentException, IOException { - List cookies = login(); + AtomicBoolean connected = new AtomicBoolean(); + AtomicReference messageRef = new AtomicReference<>(); + try (Session session = + ContainerProvider.getWebSocketContainer() + .connectToServer( + new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(String.class, messageRef::set); + connected.set(true); + } + }, + buildConfig(cookies), + URI.create(getWebSocketUrl()))) { + Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); + log.info("connected to {}", session.getRequestURI()); - AtomicBoolean connected = new AtomicBoolean(); - AtomicReference messageRef = new AtomicReference<>(); - try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { - @Override - public void onOpen(Session session, EndpointConfig config) { - session.addMessageHandler(String.class, messageRef::set); - connected.set(true); - } - }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { - Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); - log.info("connected to {}", session.getRequestURI()); + session.getAsyncRemote().sendText("ping"); - session.getAsyncRemote().sendText("ping"); - - Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null); - Assertions.assertEquals("pong", messageRef.get()); - } - } - - private ClientEndpointConfig buildConfig(List cookies) { - return ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() { - @Override - public void beforeRequest(Map> headers) { - headers.put(HttpHeaders.COOKIE, - Collections.singletonList(cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))); - } - }).build(); - } + Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null); + Assertions.assertEquals("pong", messageRef.get()); + } + } + private ClientEndpointConfig buildConfig(List cookies) { + return ClientEndpointConfig.Builder.create() + .configurator( + new ClientEndpointConfig.Configurator() { + @Override + public void beforeRequest(Map> headers) { + headers.put( + HttpHeaders.COOKIE, + Collections.singletonList( + cookies.stream() + .map(HttpCookie::toString) + .collect(Collectors.joining(";")))); + } + }) + .build(); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/cleanup/DatabaseCleaningIT.java b/commafeed-server/src/test/java/com/commafeed/integration/cleanup/DatabaseCleaningIT.java index 0dbf42f5..f6391791 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/cleanup/DatabaseCleaningIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/cleanup/DatabaseCleaningIT.java @@ -1,16 +1,5 @@ package com.commafeed.integration.cleanup; -import java.time.Duration; -import java.time.Instant; - -import jakarta.inject.Inject; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import com.commafeed.TestConstants; import com.commafeed.backend.service.db.DatabaseCleaningService; import com.commafeed.frontend.model.Entries; @@ -19,194 +8,229 @@ import com.commafeed.frontend.model.request.FeedModificationRequest; import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.integration.BaseIT; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import java.time.Duration; +import java.time.Instant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @QuarkusTest class DatabaseCleaningIT extends BaseIT { - @Inject - DatabaseCleaningService databaseCleaningService; + @Inject DatabaseCleaningService databaseCleaningService; - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - private void starEntry(String entryId, Long subscriptionId) { - StarRequest starRequest = new StarRequest(); - starRequest.setId(entryId); - starRequest.setFeedId(subscriptionId); - starRequest.setStarred(true); - RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star").then().statusCode(200); - } + private void starEntry(String entryId, Long subscriptionId) { + StarRequest starRequest = new StarRequest(); + starRequest.setId(entryId); + starRequest.setFeedId(subscriptionId); + starRequest.setStarred(true); + RestAssured.given() + .body(starRequest) + .contentType(ContentType.JSON) + .post("rest/entry/star") + .then() + .statusCode(200); + } - private void unstarEntry(String entryId, Long subscriptionId) { - StarRequest starRequest = new StarRequest(); - starRequest.setId(entryId); - starRequest.setFeedId(subscriptionId); - starRequest.setStarred(false); - RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star").then().statusCode(200); - } + private void unstarEntry(String entryId, Long subscriptionId) { + StarRequest starRequest = new StarRequest(); + starRequest.setId(entryId); + starRequest.setFeedId(subscriptionId); + starRequest.setStarred(false); + RestAssured.given() + .body(starRequest) + .contentType(ContentType.JSON) + .post("rest/entry/star") + .then() + .statusCode(200); + } - @Nested - class KeepStarredEntries { + @Nested + class KeepStarredEntries { - @Test - void starredEntriesAreKeptWhenCleaningFeedsExceedingCapacity() { - // Subscribe to feed and wait for entries - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void starredEntriesAreKeptWhenCleaningFeedsExceedingCapacity() { + // Subscribe to feed and wait for entries + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - // Verify we have 2 entries - Entries entriesBefore = getFeedEntries(subscriptionId); - Assertions.assertEquals(2, entriesBefore.getEntries().size()); + // Verify we have 2 entries + Entries entriesBefore = getFeedEntries(subscriptionId); + Assertions.assertEquals(2, entriesBefore.getEntries().size()); - // Star the first entry - Entry entryToStar = entriesBefore.getEntries().getFirst(); - starEntry(entryToStar.getId(), subscriptionId); + // Star the first entry + Entry entryToStar = entriesBefore.getEntries().getFirst(); + starEntry(entryToStar.getId(), subscriptionId); - // Verify the entry is starred - Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(1, starredEntries.getEntries().size()); - Assertions.assertEquals(entryToStar.getId(), starredEntries.getEntries().getFirst().getId()); + // Verify the entry is starred + Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(1, starredEntries.getEntries().size()); + Assertions.assertEquals( + entryToStar.getId(), starredEntries.getEntries().getFirst().getId()); - // Run cleanup with capacity of 0 (should delete all non-starred entries) - // With keepStarredEntries=true (default), only non-starred entries are counted for capacity. - // We have 2 entries, 1 starred and 1 non-starred. With capacity=0, the 1 non-starred entry exceeds capacity. - databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0); + // Run cleanup with capacity of 0 (should delete all non-starred entries) + // With keepStarredEntries=true (default), only non-starred entries are counted for + // capacity. + // We have 2 entries, 1 starred and 1 non-starred. With capacity=0, the 1 non-starred + // entry + // exceeds capacity. + databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0); - // Verify starred entry is still present - Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(1, starredEntriesAfter.getEntries().size()); - Assertions.assertEquals(entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId()); + // Verify starred entry is still present + Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(1, starredEntriesAfter.getEntries().size()); + Assertions.assertEquals( + entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId()); - // Verify the non-starred entry was deleted (only starred entry should remain) - Entries entriesAfter = getFeedEntries(subscriptionId); - Assertions.assertEquals(1, entriesAfter.getEntries().size()); - Assertions.assertEquals(entryToStar.getId(), entriesAfter.getEntries().getFirst().getId()); - } + // Verify the non-starred entry was deleted (only starred entry should remain) + Entries entriesAfter = getFeedEntries(subscriptionId); + Assertions.assertEquals(1, entriesAfter.getEntries().size()); + Assertions.assertEquals( + entryToStar.getId(), entriesAfter.getEntries().getFirst().getId()); + } - @Test - void starredEntriesAreKeptWhenCleaningOldEntries() { - // Subscribe to feed and wait for entries - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void starredEntriesAreKeptWhenCleaningOldEntries() { + // Subscribe to feed and wait for entries + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - // Verify we have 2 entries - Entries entriesBefore = getFeedEntries(subscriptionId); - Assertions.assertEquals(2, entriesBefore.getEntries().size()); + // Verify we have 2 entries + Entries entriesBefore = getFeedEntries(subscriptionId); + Assertions.assertEquals(2, entriesBefore.getEntries().size()); - // Star the first entry (oldest one based on published date in rss.xml) - Entry entryToStar = entriesBefore.getEntries().getFirst(); - starEntry(entryToStar.getId(), subscriptionId); + // Star the first entry (oldest one based on published date in rss.xml) + Entry entryToStar = entriesBefore.getEntries().getFirst(); + starEntry(entryToStar.getId(), subscriptionId); - // Verify the entry is starred - Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(1, starredEntries.getEntries().size()); + // Verify the entry is starred + Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(1, starredEntries.getEntries().size()); - // Run cleanup for entries older than now (should try to delete all entries) - // With keepStarredEntries=true (default), the starred entry should be preserved - Instant olderThan = Instant.now().plus(Duration.ofDays(1)); - databaseCleaningService.cleanEntriesOlderThan(olderThan); + // Run cleanup for entries older than now (should try to delete all entries) + // With keepStarredEntries=true (default), the starred entry should be preserved + Instant olderThan = Instant.now().plus(Duration.ofDays(1)); + databaseCleaningService.cleanEntriesOlderThan(olderThan); - // Verify starred entry is still present - Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(1, starredEntriesAfter.getEntries().size()); - Assertions.assertEquals(entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId()); + // Verify starred entry is still present + Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(1, starredEntriesAfter.getEntries().size()); + Assertions.assertEquals( + entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId()); - // Verify the non-starred entry was deleted - Entries entriesAfter = getFeedEntries(subscriptionId); - Assertions.assertEquals(1, entriesAfter.getEntries().size()); - Assertions.assertEquals(entryToStar.getId(), entriesAfter.getEntries().getFirst().getId()); - } + // Verify the non-starred entry was deleted + Entries entriesAfter = getFeedEntries(subscriptionId); + Assertions.assertEquals(1, entriesAfter.getEntries().size()); + Assertions.assertEquals( + entryToStar.getId(), entriesAfter.getEntries().getFirst().getId()); + } - @Test - void multipleStarredEntriesAreAllKept() { - // Subscribe to feed and wait for entries - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void multipleStarredEntriesAreAllKept() { + // Subscribe to feed and wait for entries + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - // Verify we have 2 entries - Entries entriesBefore = getFeedEntries(subscriptionId); - Assertions.assertEquals(2, entriesBefore.getEntries().size()); + // Verify we have 2 entries + Entries entriesBefore = getFeedEntries(subscriptionId); + Assertions.assertEquals(2, entriesBefore.getEntries().size()); - // Star both entries - entriesBefore.getEntries().forEach(entry -> starEntry(entry.getId(), subscriptionId)); + // Star both entries + entriesBefore.getEntries().forEach(entry -> starEntry(entry.getId(), subscriptionId)); - // Verify both entries are starred - Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(2, starredEntries.getEntries().size()); + // Verify both entries are starred + Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(2, starredEntries.getEntries().size()); - // Run cleanup with capacity of 0 (should delete all non-starred entries) - databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0); + // Run cleanup with capacity of 0 (should delete all non-starred entries) + databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0); - // Verify both starred entries are still present - Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(2, starredEntriesAfter.getEntries().size()); + // Verify both starred entries are still present + Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(2, starredEntriesAfter.getEntries().size()); - // Verify all entries are preserved (since all are starred) - Entries entriesAfter = getFeedEntries(subscriptionId); - Assertions.assertEquals(2, entriesAfter.getEntries().size()); - } + // Verify all entries are preserved (since all are starred) + Entries entriesAfter = getFeedEntries(subscriptionId); + Assertions.assertEquals(2, entriesAfter.getEntries().size()); + } - @Test - void unstarringEntryMakesItEligibleForCleanup() { - // Subscribe to feed and wait for entries - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void unstarringEntryMakesItEligibleForCleanup() { + // Subscribe to feed and wait for entries + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - // Star the first entry - Entries entriesBefore = getFeedEntries(subscriptionId); - Entry entry = entriesBefore.getEntries().getFirst(); - starEntry(entry.getId(), subscriptionId); + // Star the first entry + Entries entriesBefore = getFeedEntries(subscriptionId); + Entry entry = entriesBefore.getEntries().getFirst(); + starEntry(entry.getId(), subscriptionId); - // Verify entry is starred - Assertions.assertEquals(1, getCategoryEntries(CategoryREST.STARRED).getEntries().size()); + // Verify entry is starred + Assertions.assertEquals( + 1, getCategoryEntries(CategoryREST.STARRED).getEntries().size()); - // Unstar the entry - unstarEntry(entry.getId(), subscriptionId); + // Unstar the entry + unstarEntry(entry.getId(), subscriptionId); - // Verify entry is no longer starred - Assertions.assertEquals(0, getCategoryEntries(CategoryREST.STARRED).getEntries().size()); + // Verify entry is no longer starred + Assertions.assertEquals( + 0, getCategoryEntries(CategoryREST.STARRED).getEntries().size()); - // Run cleanup for entries older than now - Instant olderThan = Instant.now().plus(Duration.ofDays(1)); - databaseCleaningService.cleanEntriesOlderThan(olderThan); + // Run cleanup for entries older than now + Instant olderThan = Instant.now().plus(Duration.ofDays(1)); + databaseCleaningService.cleanEntriesOlderThan(olderThan); - // Verify both entries were deleted (neither is starred) - Entries entriesAfter = getFeedEntries(subscriptionId); - Assertions.assertEquals(0, entriesAfter.getEntries().size()); - } - } + // Verify both entries were deleted (neither is starred) + Entries entriesAfter = getFeedEntries(subscriptionId); + Assertions.assertEquals(0, entriesAfter.getEntries().size()); + } + } - @Nested - class AutoMarkAsRead { - @Test - void entriesAreMarkedAsReadAfterSpecifiedDays() { - // Subscribe to feed - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Nested + class AutoMarkAsRead { + @Test + void entriesAreMarkedAsReadAfterSpecifiedDays() { + // Subscribe to feed + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - // verify we have 2 unread entries - Entries entries = getFeedEntries(subscriptionId); - Assertions.assertEquals(2, entries.getEntries().stream().filter(e -> !e.isRead()).count()); + // verify we have 2 unread entries + Entries entries = getFeedEntries(subscriptionId); + Assertions.assertEquals( + 2, entries.getEntries().stream().filter(e -> !e.isRead()).count()); - // set auto-mark as read - FeedModificationRequest req = new FeedModificationRequest(); - req.setId(subscriptionId); - req.setAutoMarkAsReadAfterDays(1); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(200); + // set auto-mark as read + FeedModificationRequest req = new FeedModificationRequest(); + req.setId(subscriptionId); + req.setAutoMarkAsReadAfterDays(1); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/feed/modify") + .then() + .statusCode(200); - // run auto-mark as read - databaseCleaningService.autoMarkAsRead(); + // run auto-mark as read + databaseCleaningService.autoMarkAsRead(); - // verify all entries are now read - entries = getFeedEntries(subscriptionId); - Assertions.assertEquals(0, entries.getEntries().stream().filter(e -> !e.isRead()).count()); - } - } + // verify all entries are now read + entries = getFeedEntries(subscriptionId); + Assertions.assertEquals( + 0, entries.getEntries().stream().filter(e -> !e.isRead()).count()); + } + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java index a3d47890..510baf1a 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java @@ -1,7 +1,14 @@ package com.commafeed.integration.rest; +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.UserModel; +import com.commafeed.frontend.model.request.AdminSaveUserRequest; +import com.commafeed.frontend.model.request.IDRequest; +import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; import java.util.List; - import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -9,96 +16,104 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import com.commafeed.TestConstants; -import com.commafeed.frontend.model.UserModel; -import com.commafeed.frontend.model.request.AdminSaveUserRequest; -import com.commafeed.frontend.model.request.IDRequest; -import com.commafeed.integration.BaseIT; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; - @QuarkusTest class AdminIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Nested - class Users { - @Test - void saveModifyAndDeleteNewUser() { - List existingUsers = getAllUsers(); + @Nested + class Users { + @Test + void saveModifyAndDeleteNewUser() { + List existingUsers = getAllUsers(); - long userId = createUser(); - Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size()); + long userId = createUser(); + Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size()); - UserModel user = getUser(userId); - Assertions.assertEquals("test", user.getName()); + UserModel user = getUser(userId); + Assertions.assertEquals("test", user.getName()); - modifyUser(user); - Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size()); + modifyUser(user); + Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size()); - deleteUser(); - Assertions.assertEquals(existingUsers.size(), getAllUsers().size()); - } + deleteUser(); + Assertions.assertEquals(existingUsers.size(), getAllUsers().size()); + } - private long createUser() { - AdminSaveUserRequest user = new AdminSaveUserRequest(); - user.setName("test"); - user.setPassword("Test1234!"); - user.setEmail("test@test.com"); - user.setEnabled(true); - String response = RestAssured.given() - .body(user) - .contentType(ContentType.JSON) - .post("rest/admin/user/save") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .asString(); - return Long.parseLong(response); - } + private long createUser() { + AdminSaveUserRequest user = new AdminSaveUserRequest(); + user.setName("test"); + user.setPassword("Test1234!"); + user.setEmail("test@test.com"); + user.setEnabled(true); + String response = + RestAssured.given() + .body(user) + .contentType(ContentType.JSON) + .post("rest/admin/user/save") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .asString(); + return Long.parseLong(response); + } - private UserModel getUser(long userId) { - return RestAssured.given() - .get("rest/admin/user/get/{id}", userId) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(UserModel.class); - } + private UserModel getUser(long userId) { + return RestAssured.given() + .get("rest/admin/user/get/{id}", userId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(UserModel.class); + } - private void modifyUser(UserModel user) { - user.setEmail("new-email@provider.com"); - RestAssured.given().body(user).contentType(ContentType.JSON).post("rest/admin/user/save").then().statusCode(HttpStatus.SC_OK); - } + private void modifyUser(UserModel user) { + user.setEmail("new-email@provider.com"); + RestAssured.given() + .body(user) + .contentType(ContentType.JSON) + .post("rest/admin/user/save") + .then() + .statusCode(HttpStatus.SC_OK); + } - private void deleteUser() { - List existingUsers = getAllUsers(); - UserModel user = existingUsers.stream() - .filter(u -> u.getName().equals("test")) - .findFirst() - .orElseThrow(() -> new NullPointerException("User not found")); + private void deleteUser() { + List existingUsers = getAllUsers(); + UserModel user = + existingUsers.stream() + .filter(u -> u.getName().equals("test")) + .findFirst() + .orElseThrow(() -> new NullPointerException("User not found")); - IDRequest req = new IDRequest(); - req.setId(user.getId()); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/admin/user/delete").then().statusCode(HttpStatus.SC_OK); - } - - private List getAllUsers() { - return List.of( - RestAssured.given().get("rest/admin/user/getAll").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel[].class)); - } - } + IDRequest req = new IDRequest(); + req.setId(user.getId()); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/admin/user/delete") + .then() + .statusCode(HttpStatus.SC_OK); + } + private List getAllUsers() { + return List.of( + RestAssured.given() + .get("rest/admin/user/getAll") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(UserModel[].class)); + } + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/CategoryIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/CategoryIT.java index a99ccac8..c428f668 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/CategoryIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/CategoryIT.java @@ -1,16 +1,5 @@ package com.commafeed.integration.rest; -import java.io.StringReader; -import java.util.List; - -import org.apache.hc.core5.http.HttpStatus; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.xml.sax.InputSource; - import com.commafeed.TestConstants; import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Entries; @@ -27,214 +16,273 @@ import com.commafeed.integration.BaseIT; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedInput; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.io.StringReader; +import java.util.List; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; @QuarkusTest class CategoryIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Test - void modifyCategory() { - String category1Id = createCategory("test-category-1"); - String category2Id = createCategory("test-category-2"); - String category3Id = createCategory("test-category-3"); + @Test + void modifyCategory() { + String category1Id = createCategory("test-category-1"); + String category2Id = createCategory("test-category-2"); + String category3Id = createCategory("test-category-3"); - CategoryModificationRequest request = new CategoryModificationRequest(); - request.setId(Long.valueOf(category2Id)); - request.setName("modified-category-2"); - request.setParentId(category1Id); - request.setPosition(2); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/category/modify").then().statusCode(200); + CategoryModificationRequest request = new CategoryModificationRequest(); + request.setId(Long.valueOf(category2Id)); + request.setName("modified-category-2"); + request.setParentId(category1Id); + request.setPosition(2); + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/category/modify") + .then() + .statusCode(200); - Category root = getRootCategory(); - Assertions.assertEquals(2, root.getChildren().size()); - Assertions.assertEquals("test-category-1", root.getChildren().getFirst().getName()); - Assertions.assertEquals(1, root.getChildren().getFirst().getChildren().size()); - Assertions.assertEquals("modified-category-2", root.getChildren().getFirst().getChildren().getFirst().getName()); + Category root = getRootCategory(); + Assertions.assertEquals(2, root.getChildren().size()); + Assertions.assertEquals("test-category-1", root.getChildren().getFirst().getName()); + Assertions.assertEquals(1, root.getChildren().getFirst().getChildren().size()); + Assertions.assertEquals( + "modified-category-2", + root.getChildren().getFirst().getChildren().getFirst().getName()); - request = new CategoryModificationRequest(); - request.setId(Long.valueOf(category3Id)); - request.setPosition(0); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/category/modify").then().statusCode(200); + request = new CategoryModificationRequest(); + request.setId(Long.valueOf(category3Id)); + request.setPosition(0); + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/category/modify") + .then() + .statusCode(200); - root = getRootCategory(); - Assertions.assertEquals(2, root.getChildren().size()); - Assertions.assertEquals("test-category-3", root.getChildren().get(0).getName()); - Assertions.assertEquals("test-category-1", root.getChildren().get(1).getName()); - } + root = getRootCategory(); + Assertions.assertEquals(2, root.getChildren().size()); + Assertions.assertEquals("test-category-3", root.getChildren().get(0).getName()); + Assertions.assertEquals("test-category-1", root.getChildren().get(1).getName()); + } - @Test - void collapseCategory() { - String categoryId = createCategory("test-category"); + @Test + void collapseCategory() { + String categoryId = createCategory("test-category"); - Category root = getRootCategory(); - Assertions.assertEquals(1, root.getChildren().size()); - Assertions.assertTrue(root.getChildren().getFirst().isExpanded()); + Category root = getRootCategory(); + Assertions.assertEquals(1, root.getChildren().size()); + Assertions.assertTrue(root.getChildren().getFirst().isExpanded()); - CollapseRequest request = new CollapseRequest(); - request.setId(Long.valueOf(categoryId)); - request.setCollapse(true); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/category/collapse").then().statusCode(200); + CollapseRequest request = new CollapseRequest(); + request.setId(Long.valueOf(categoryId)); + request.setCollapse(true); + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/category/collapse") + .then() + .statusCode(200); - root = getRootCategory(); - Assertions.assertEquals(1, root.getChildren().size()); - Assertions.assertFalse(root.getChildren().getFirst().isExpanded()); - } + root = getRootCategory(); + Assertions.assertEquals(1, root.getChildren().size()); + Assertions.assertFalse(root.getChildren().getFirst().isExpanded()); + } - @Test - void deleteCategory() { - String categoryId = createCategory("test-category"); - Assertions.assertEquals(1, getRootCategory().getChildren().size()); + @Test + void deleteCategory() { + String categoryId = createCategory("test-category"); + Assertions.assertEquals(1, getRootCategory().getChildren().size()); - IDRequest request = new IDRequest(); - request.setId(Long.valueOf(categoryId)); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/category/delete").then().statusCode(200); - Assertions.assertEquals(0, getRootCategory().getChildren().size()); - } + IDRequest request = new IDRequest(); + request.setId(Long.valueOf(categoryId)); + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/category/delete") + .then() + .statusCode(200); + Assertions.assertEquals(0, getRootCategory().getChildren().size()); + } - @Test - void unreadCount() { - String categoryId = createCategory("test-category"); - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl(), categoryId); - Assertions.assertEquals(2, getCategoryEntries(categoryId).getEntries().size()); + @Test + void unreadCount() { + String categoryId = createCategory("test-category"); + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl(), categoryId); + Assertions.assertEquals(2, getCategoryEntries(categoryId).getEntries().size()); - UnreadCount[] counts = RestAssured.given() - .get("rest/category/unreadCount") - .then() - .statusCode(200) - .extract() - .as(UnreadCount[].class); + UnreadCount[] counts = + RestAssured.given() + .get("rest/category/unreadCount") + .then() + .statusCode(200) + .extract() + .as(UnreadCount[].class); - Assertions.assertEquals(1, counts.length); - Assertions.assertEquals(subscriptionId, counts[0].getFeedId()); - Assertions.assertEquals(2, counts[0].getUnreadCount()); - } + Assertions.assertEquals(1, counts.length); + Assertions.assertEquals(subscriptionId, counts[0].getFeedId()); + Assertions.assertEquals(2, counts[0].getUnreadCount()); + } - @Nested - class MarkEntriesAsRead { - @Test - void all() { - subscribeAndWaitForEntries(getFeedUrl()); - Assertions.assertTrue(getCategoryEntries(CategoryREST.ALL).getEntries().stream().noneMatch(Entry::isRead)); + @Nested + class MarkEntriesAsRead { + @Test + void all() { + subscribeAndWaitForEntries(getFeedUrl()); + Assertions.assertTrue( + getCategoryEntries(CategoryREST.ALL).getEntries().stream() + .noneMatch(Entry::isRead)); - MarkRequest request = new MarkRequest(); - request.setId(CategoryREST.ALL); - request.setRead(true); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/category/mark").then().statusCode(200); - Assertions.assertTrue(getCategoryEntries(CategoryREST.ALL).getEntries().stream().allMatch(Entry::isRead)); - } + MarkRequest request = new MarkRequest(); + request.setId(CategoryREST.ALL); + request.setRead(true); + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/category/mark") + .then() + .statusCode(200); + Assertions.assertTrue( + getCategoryEntries(CategoryREST.ALL).getEntries().stream() + .allMatch(Entry::isRead)); + } - @Test - void specificCategory() { - String categoryId = createCategory("test-category"); - subscribeAndWaitForEntries(getFeedUrl(), categoryId); - Assertions.assertTrue(getCategoryEntries(categoryId).getEntries().stream().noneMatch(Entry::isRead)); + @Test + void specificCategory() { + String categoryId = createCategory("test-category"); + subscribeAndWaitForEntries(getFeedUrl(), categoryId); + Assertions.assertTrue( + getCategoryEntries(categoryId).getEntries().stream().noneMatch(Entry::isRead)); - MarkRequest request = new MarkRequest(); - request.setId(categoryId); - request.setRead(true); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/category/mark").then().statusCode(200); - Assertions.assertTrue(getCategoryEntries(categoryId).getEntries().stream().allMatch(Entry::isRead)); - } - } + MarkRequest request = new MarkRequest(); + request.setId(categoryId); + request.setRead(true); + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/category/mark") + .then() + .statusCode(200); + Assertions.assertTrue( + getCategoryEntries(categoryId).getEntries().stream().allMatch(Entry::isRead)); + } + } - @Nested - class GetEntries { - @Test - void all() { - subscribeAndWaitForEntries(getFeedUrl()); - Entries entries = getCategoryEntries(CategoryREST.ALL); - Assertions.assertEquals(2, entries.getEntries().size()); - } + @Nested + class GetEntries { + @Test + void all() { + subscribeAndWaitForEntries(getFeedUrl()); + Entries entries = getCategoryEntries(CategoryREST.ALL); + Assertions.assertEquals(2, entries.getEntries().size()); + } - @Test - void pagination() { - subscribeAndWaitForEntries(getFeedUrl()); + @Test + void pagination() { + subscribeAndWaitForEntries(getFeedUrl()); - Entries firstPage = getCategoryEntries(CategoryREST.ALL, 0, 1); - Assertions.assertEquals(1, firstPage.getEntries().size()); - Assertions.assertTrue(firstPage.isHasMore()); + Entries firstPage = getCategoryEntries(CategoryREST.ALL, 0, 1); + Assertions.assertEquals(1, firstPage.getEntries().size()); + Assertions.assertTrue(firstPage.isHasMore()); - Entries lastPage = getCategoryEntries(CategoryREST.ALL, 1, 10); - Assertions.assertEquals(1, lastPage.getEntries().size()); - Assertions.assertFalse(lastPage.isHasMore()); - } + Entries lastPage = getCategoryEntries(CategoryREST.ALL, 1, 10); + Assertions.assertEquals(1, lastPage.getEntries().size()); + Assertions.assertFalse(lastPage.isHasMore()); + } - @Test - void allAsFeed() throws FeedException { - subscribeAndWaitForEntries(getFeedUrl()); - String xml = RestAssured.given() - .get("rest/category/entriesAsFeed?id=all") - .then() - .statusCode(HttpStatus.SC_OK) - .contentType(ContentType.XML) - .extract() - .asString(); + @Test + void allAsFeed() throws FeedException { + subscribeAndWaitForEntries(getFeedUrl()); + String xml = + RestAssured.given() + .get("rest/category/entriesAsFeed?id=all") + .then() + .statusCode(HttpStatus.SC_OK) + .contentType(ContentType.XML) + .extract() + .asString(); - InputSource source = new InputSource(new StringReader(xml)); - SyndFeed feed = new SyndFeedInput().build(source); - Assertions.assertEquals(2, feed.getEntries().size()); - } + InputSource source = new InputSource(new StringReader(xml)); + SyndFeed feed = new SyndFeedInput().build(source); + Assertions.assertEquals(2, feed.getEntries().size()); + } - @Test - void starred() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Assertions.assertEquals(0, getCategoryEntries(CategoryREST.STARRED).getEntries().size()); + @Test + void starred() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Assertions.assertEquals( + 0, getCategoryEntries(CategoryREST.STARRED).getEntries().size()); - Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); - StarRequest starRequest = new StarRequest(); - starRequest.setId(entry.getId()); - starRequest.setFeedId(subscriptionId); - starRequest.setStarred(true); - RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star"); + StarRequest starRequest = new StarRequest(); + starRequest.setId(entry.getId()); + starRequest.setFeedId(subscriptionId); + starRequest.setStarred(true); + RestAssured.given() + .body(starRequest) + .contentType(ContentType.JSON) + .post("rest/entry/star"); - Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); - Assertions.assertEquals(1, starredEntries.getEntries().size()); - Assertions.assertEquals(entry.getId(), starredEntries.getEntries().getFirst().getId()); - } + Entries starredEntries = getCategoryEntries(CategoryREST.STARRED); + Assertions.assertEquals(1, starredEntries.getEntries().size()); + Assertions.assertEquals(entry.getId(), starredEntries.getEntries().getFirst().getId()); + } - @Test - void tagged() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Assertions.assertEquals(0, getTaggedEntries("my-tag").getEntries().size()); + @Test + void tagged() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Assertions.assertEquals(0, getTaggedEntries("my-tag").getEntries().size()); - Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); - TagRequest tagRequest = new TagRequest(); - tagRequest.setEntryId(Long.valueOf(entry.getId())); - tagRequest.setTags(List.of("my-tag")); - RestAssured.given().body(tagRequest).contentType(ContentType.JSON).post("rest/entry/tag"); + TagRequest tagRequest = new TagRequest(); + tagRequest.setEntryId(Long.valueOf(entry.getId())); + tagRequest.setTags(List.of("my-tag")); + RestAssured.given() + .body(tagRequest) + .contentType(ContentType.JSON) + .post("rest/entry/tag"); - Entries taggedEntries = getTaggedEntries("my-tag"); - Assertions.assertEquals(1, taggedEntries.getEntries().size()); - Assertions.assertEquals(entry.getId(), taggedEntries.getEntries().getFirst().getId()); - } + Entries taggedEntries = getTaggedEntries("my-tag"); + Assertions.assertEquals(1, taggedEntries.getEntries().size()); + Assertions.assertEquals(entry.getId(), taggedEntries.getEntries().getFirst().getId()); + } - @Test - void keywords() { - subscribeAndWaitForEntries(getFeedUrl()); - Assertions.assertEquals(1, getCategoryEntries(CategoryREST.ALL, "Item 2 description").getEntries().size()); - } + @Test + void keywords() { + subscribeAndWaitForEntries(getFeedUrl()); + Assertions.assertEquals( + 1, + getCategoryEntries(CategoryREST.ALL, "Item 2 description").getEntries().size()); + } - @Test - void specificCategory() { - String categoryId = createCategory("test-category"); - subscribeAndWaitForEntries(getFeedUrl(), categoryId); - Entries entries = getCategoryEntries(categoryId); - Assertions.assertEquals(2, entries.getEntries().size()); - } - } + @Test + void specificCategory() { + String categoryId = createCategory("test-category"); + subscribeAndWaitForEntries(getFeedUrl(), categoryId); + Entries entries = getCategoryEntries(categoryId); + Assertions.assertEquals(2, entries.getEntries().size()); + } + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java index 27163b23..c5bee9c5 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java @@ -1,27 +1,5 @@ package com.commafeed.integration.rest; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.util.Objects; - -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; - -import org.apache.commons.io.IOUtils; -import org.apache.hc.core5.http.HttpStatus; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.xml.sax.InputSource; - import com.commafeed.TestConstants; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; @@ -36,278 +14,346 @@ import com.commafeed.integration.BaseIT; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedInput; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Objects; +import org.apache.commons.io.IOUtils; +import org.apache.hc.core5.http.HttpStatus; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; @QuarkusTest class FeedIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Nested - class Fetch { - @Test - void fetchFeed() { - FeedInfoRequest req = new FeedInfoRequest(); - req.setUrl(getFeedUrl()); + @Nested + class Fetch { + @Test + void fetchFeed() { + FeedInfoRequest req = new FeedInfoRequest(); + req.setUrl(getFeedUrl()); - FeedInfo feedInfo = RestAssured.given() - .body(req) - .contentType(ContentType.JSON) - .post("rest/feed/fetch") - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(FeedInfo.class); - Assertions.assertEquals("CommaFeed test feed", feedInfo.getTitle()); - Assertions.assertEquals(getFeedUrl(), feedInfo.getUrl()); - } - } + FeedInfo feedInfo = + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/feed/fetch") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(FeedInfo.class); + Assertions.assertEquals("CommaFeed test feed", feedInfo.getTitle()); + Assertions.assertEquals(getFeedUrl(), feedInfo.getUrl()); + } + } - @Nested - class Subscribe { - @Test - void subscribeAndReadEntries() { - long subscriptionId = subscribe(getFeedUrl()); - Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2); - } + @Nested + class Subscribe { + @Test + void subscribeAndReadEntries() { + long subscriptionId = subscribe(getFeedUrl()); + Awaitility.await() + .atMost(Duration.ofSeconds(15)) + .until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2); + } - @Test - void subscribeFromUrl() { - RestAssured.given() - .queryParam("url", getFeedUrl()) - .redirects() - .follow(false) - .get("rest/feed/subscribe") - .then() - .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT); - } + @Test + void subscribeFromUrl() { + RestAssured.given() + .queryParam("url", getFeedUrl()) + .redirects() + .follow(false) + .get("rest/feed/subscribe") + .then() + .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT); + } - @Test - void unsubscribeFromUnknownFeed() { - Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L)); - } + @Test + void unsubscribeFromUnknownFeed() { + Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L)); + } - @Test - void unsubscribeFromKnownFeed() { - long subscriptionId = subscribe(getFeedUrl()); - Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId)); - } + @Test + void unsubscribeFromKnownFeed() { + long subscriptionId = subscribe(getFeedUrl()); + Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId)); + } - private int unsubsribe(long subscriptionId) { - IDRequest request = new IDRequest(); - request.setId(subscriptionId); + private int unsubsribe(long subscriptionId) { + IDRequest request = new IDRequest(); + request.setId(subscriptionId); - return RestAssured.given() - .body(request) - .contentType(ContentType.JSON) - .post("rest/feed/unsubscribe") - .then() - .extract() - .statusCode(); - } - } + return RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/feed/unsubscribe") + .then() + .extract() + .statusCode(); + } + } - @Nested - class Mark { - @Test - void markWithoutDates() { - long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - markFeedEntries(subscriptionId, null, null); - Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); - } + @Nested + class Mark { + @Test + void markWithoutDates() { + long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + markFeedEntries(subscriptionId, null, null); + Assertions.assertTrue( + getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); + } - @Test - void markOlderThan() { - long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - markFeedEntries(subscriptionId, LocalDate.of(2023, 12, 28).atStartOfDay().toInstant(ZoneOffset.UTC), null); - Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); - } + @Test + void markOlderThan() { + long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + markFeedEntries( + subscriptionId, + LocalDate.of(2023, 12, 28).atStartOfDay().toInstant(ZoneOffset.UTC), + null); + Assertions.assertEquals( + 1, + getFeedEntries(subscriptionId).getEntries().stream() + .filter(Entry::isRead) + .count()); + } - @Test - void markInsertedBeforeBeforeSubscription() { - // mariadb/mysql timestamp precision is 1 second - Instant threshold = Instant.now().minus(Duration.ofSeconds(1)); + @Test + void markInsertedBeforeBeforeSubscription() { + // mariadb/mysql timestamp precision is 1 second + Instant threshold = Instant.now().minus(Duration.ofSeconds(1)); - long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - markFeedEntries(subscriptionId, null, threshold); - Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().noneMatch(Entry::isRead)); - } + long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + markFeedEntries(subscriptionId, null, threshold); + Assertions.assertTrue( + getFeedEntries(subscriptionId).getEntries().stream().noneMatch(Entry::isRead)); + } - @Test - void markInsertedBeforeAfterSubscription() { - long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void markInsertedBeforeAfterSubscription() { + long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - // mariadb/mysql timestamp precision is 1 second - Instant threshold = Instant.now().plus(Duration.ofSeconds(1)); + // mariadb/mysql timestamp precision is 1 second + Instant threshold = Instant.now().plus(Duration.ofSeconds(1)); - markFeedEntries(subscriptionId, null, threshold); - Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); - } + markFeedEntries(subscriptionId, null, threshold); + Assertions.assertTrue( + getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); + } - private void markFeedEntries(long subscriptionId, Instant olderThan, Instant insertedBefore) { - MarkRequest request = new MarkRequest(); - request.setId(String.valueOf(subscriptionId)); - request.setOlderThan(olderThan == null ? null : olderThan.toEpochMilli()); - request.setInsertedBefore(insertedBefore == null ? null : insertedBefore.toEpochMilli()); + private void markFeedEntries( + long subscriptionId, Instant olderThan, Instant insertedBefore) { + MarkRequest request = new MarkRequest(); + request.setId(String.valueOf(subscriptionId)); + request.setOlderThan(olderThan == null ? null : olderThan.toEpochMilli()); + request.setInsertedBefore( + insertedBefore == null ? null : insertedBefore.toEpochMilli()); - RestAssured.given().body(request).contentType(ContentType.JSON).post("rest/feed/mark").then().statusCode(HttpStatus.SC_OK); - } - } + RestAssured.given() + .body(request) + .contentType(ContentType.JSON) + .post("rest/feed/mark") + .then() + .statusCode(HttpStatus.SC_OK); + } + } - @Nested - class Refresh { + @Nested + class Refresh { - @Test - void refreshAll() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Assertions.assertEquals(2, getCategoryEntries(CategoryREST.ALL).getEntries().size()); + @Test + void refreshAll() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Assertions.assertEquals(2, getCategoryEntries(CategoryREST.ALL).getEntries().size()); - // mariadb/mysql timestamp precision is 1 second - Instant threshold = Instant.now().minus(Duration.ofSeconds(1)); - Assertions.assertEquals(HttpStatus.SC_OK, forceRefreshAllFeeds()); + // mariadb/mysql timestamp precision is 1 second + Instant threshold = Instant.now().minus(Duration.ofSeconds(1)); + Assertions.assertEquals(HttpStatus.SC_OK, forceRefreshAllFeeds()); - Awaitility.await() - .atMost(Duration.ofSeconds(15)) - .until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold)); - Assertions.assertEquals(2, getCategoryEntries(CategoryREST.ALL).getEntries().size()); + Awaitility.await() + .atMost(Duration.ofSeconds(15)) + .until( + () -> getSubscription(subscriptionId), + f -> f.getLastRefresh().isAfter(threshold)); + Assertions.assertEquals(2, getCategoryEntries(CategoryREST.ALL).getEntries().size()); - Assertions.assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, forceRefreshAllFeeds()); - } - } + Assertions.assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, forceRefreshAllFeeds()); + } + } - @Nested - class RSS { - @Test - void allAsFeed() throws FeedException { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - String xml = RestAssured.given() - .get("rest/feed/entriesAsFeed?id={id}", subscriptionId) - .then() - .statusCode(HttpStatus.SC_OK) - .contentType(ContentType.XML) - .extract() - .asString(); + @Nested + class RSS { + @Test + void allAsFeed() throws FeedException { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + String xml = + RestAssured.given() + .get("rest/feed/entriesAsFeed?id={id}", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .contentType(ContentType.XML) + .extract() + .asString(); - InputSource source = new InputSource(new StringReader(xml)); - SyndFeed feed = new SyndFeedInput().build(source); - Assertions.assertEquals(2, feed.getEntries().size()); - } - } + InputSource source = new InputSource(new StringReader(xml)); + SyndFeed feed = new SyndFeedInput().build(source); + Assertions.assertEquals(2, feed.getEntries().size()); + } + } - @Nested - class Modify { - @Test - void modify() { - Long subscriptionId = subscribe(getFeedUrl()); + @Nested + class Modify { + @Test + void modify() { + Long subscriptionId = subscribe(getFeedUrl()); - Subscription subscription = getSubscription(subscriptionId); + Subscription subscription = getSubscription(subscriptionId); - FeedModificationRequest req = new FeedModificationRequest(); - req.setId(subscriptionId); - req.setName("new name"); - req.setCategoryId(subscription.getCategoryId()); - req.setPosition(1); - req.setFilter("url.endsWith('commafeed')"); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK); + FeedModificationRequest req = new FeedModificationRequest(); + req.setId(subscriptionId); + req.setName("new name"); + req.setCategoryId(subscription.getCategoryId()); + req.setPosition(1); + req.setFilter("url.endsWith('commafeed')"); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/feed/modify") + .then() + .statusCode(HttpStatus.SC_OK); - subscription = getSubscription(subscriptionId); - Assertions.assertEquals("new name", subscription.getName()); - Assertions.assertEquals("url.endsWith('commafeed')", subscription.getFilter()); - } - } + subscription = getSubscription(subscriptionId); + Assertions.assertEquals("new name", subscription.getName()); + Assertions.assertEquals("url.endsWith('commafeed')", subscription.getFilter()); + } + } - @Nested - class Favicon { - @Test - void favicon() throws IOException { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Nested + class Favicon { + @Test + void favicon() throws IOException { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - byte[] icon = RestAssured.given() - .get("rest/feed/favicon/{id}", subscriptionId) - .then() - .statusCode(HttpStatus.SC_OK) - .header(HttpHeaders.CACHE_CONTROL, "max-age=2592000") - .extract() - .response() - .asByteArray(); - byte[] defaultFavicon = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif"))); - Assertions.assertArrayEquals(defaultFavicon, icon); - } - } + byte[] icon = + RestAssured.given() + .get("rest/feed/favicon/{id}", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .header(HttpHeaders.CACHE_CONTROL, "max-age=2592000") + .extract() + .response() + .asByteArray(); + byte[] defaultFavicon = + IOUtils.toByteArray( + Objects.requireNonNull( + getClass().getResource("/images/default_favicon.gif"))); + Assertions.assertArrayEquals(defaultFavicon, icon); + } + } - @Nested - class Opml { - @Test - void importExportOpml() { - importOpml(); - String opml = RestAssured.given().get("rest/feed/export").then().statusCode(HttpStatus.SC_OK).extract().asString(); - Assertions.assertTrue(opml.contains("admin subscriptions in CommaFeed")); - } + @Nested + class Opml { + @Test + void importExportOpml() { + importOpml(); + String opml = + RestAssured.given() + .get("rest/feed/export") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .asString(); + Assertions.assertTrue(opml.contains("admin subscriptions in CommaFeed")); + } - void importOpml() { - InputStream stream = Objects.requireNonNull(getClass().getResourceAsStream("/opml/opml_v2.0.xml")); + void importOpml() { + InputStream stream = + Objects.requireNonNull(getClass().getResourceAsStream("/opml/opml_v2.0.xml")); - RestAssured.given() - .multiPart("file", "opml_v2.0.xml", stream, MediaType.MULTIPART_FORM_DATA) - .post("rest/feed/import") - .then() - .statusCode(HttpStatus.SC_OK); - } - } + RestAssured.given() + .multiPart("file", "opml_v2.0.xml", stream, MediaType.MULTIPART_FORM_DATA) + .post("rest/feed/import") + .then() + .statusCode(HttpStatus.SC_OK); + } + } - @Nested - class Filter { - @Test - void filterEntriesOnNewFeedItems() throws IOException { - // subscribe and wait for initial 2 entries - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Entries initialEntries = getFeedEntries(subscriptionId); - Assertions.assertEquals(2, initialEntries.getEntries().size()); + @Nested + class Filter { + @Test + void filterEntriesOnNewFeedItems() throws IOException { + // subscribe and wait for initial 2 entries + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entries initialEntries = getFeedEntries(subscriptionId); + Assertions.assertEquals(2, initialEntries.getEntries().size()); - // set up a filter that excludes entries with "item 4" in the title - Subscription subscription = getSubscription(subscriptionId); - FeedModificationRequest req = new FeedModificationRequest(); - req.setId(subscriptionId); - req.setName(subscription.getName()); - req.setCategoryId(subscription.getCategoryId()); - req.setPosition(subscription.getPosition()); - req.setFilter("!titleLower.contains('item 4')"); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK); + // set up a filter that excludes entries with "item 4" in the title + Subscription subscription = getSubscription(subscriptionId); + FeedModificationRequest req = new FeedModificationRequest(); + req.setId(subscriptionId); + req.setName(subscription.getName()); + req.setCategoryId(subscription.getCategoryId()); + req.setPosition(subscription.getPosition()); + req.setFilter("!titleLower.contains('item 4')"); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/feed/modify") + .then() + .statusCode(HttpStatus.SC_OK); - // verify filter is set - subscription = getSubscription(subscriptionId); - Assertions.assertEquals("!titleLower.contains('item 4')", subscription.getFilter()); + // verify filter is set + subscription = getSubscription(subscriptionId); + Assertions.assertEquals("!titleLower.contains('item 4')", subscription.getFilter()); - // feed now returns 2 more entries (Item 3 and Item 4) - feedNowReturnsMoreEntries(); - forceRefreshAllFeeds(); + // feed now returns 2 more entries (Item 3 and Item 4) + feedNowReturnsMoreEntries(); + forceRefreshAllFeeds(); - // wait for new entries to be fetched - Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getCategoryEntries("all"), e -> e.getEntries().size() == 4); - - // verify that Item 4 was marked as read because it matches the filter - Entries unreadEntries = RestAssured.given() - .get("rest/feed/entries?id={id}&readType=unread", subscriptionId) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .as(Entries.class); - Assertions.assertEquals(3, unreadEntries.getEntries().size()); - Assertions.assertTrue(unreadEntries.getEntries().stream().noneMatch(e -> e.getTitle().toLowerCase().contains("item 4")), - "Item 4 should be filtered out (marked as read)"); - } - } + // wait for new entries to be fetched + Awaitility.await() + .atMost(Duration.ofSeconds(15)) + .until(() -> getCategoryEntries("all"), e -> e.getEntries().size() == 4); + // verify that Item 4 was marked as read because it matches the filter + Entries unreadEntries = + RestAssured.given() + .get("rest/feed/entries?id={id}&readType=unread", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); + Assertions.assertEquals(3, unreadEntries.getEntries().size()); + Assertions.assertTrue( + unreadEntries.getEntries().stream() + .noneMatch(e -> e.getTitle().toLowerCase().contains("item 4")), + "Item 4 should be filtered out (marked as read)"); + } + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java index 5b6c1417..c22df93c 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java @@ -1,11 +1,5 @@ package com.commafeed.integration.rest; -import org.apache.hc.core5.http.HttpStatus; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.commafeed.TestConstants; import com.commafeed.backend.Digests; import com.commafeed.frontend.model.Entry; @@ -15,189 +9,247 @@ import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.resource.fever.FeverResponse; import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem; import com.commafeed.integration.BaseIT; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import lombok.Setter; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; @QuarkusTest class FeverIT extends BaseIT { - private FeverClient client; + private FeverClient client; - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - // create api key - ProfileModificationRequest req = new ProfileModificationRequest(); - req.setCurrentPassword(TestConstants.ADMIN_PASSWORD); - req.setNewApiKey(true); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/user/profile").then().statusCode(HttpStatus.SC_OK); + // create api key + ProfileModificationRequest req = new ProfileModificationRequest(); + req.setCurrentPassword(TestConstants.ADMIN_PASSWORD); + req.setNewApiKey(true); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); - // retrieve api key - UserModel user = RestAssured.given().get("rest/user/profile").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel.class); - this.client = new FeverClient(user.getId(), user.getApiKey()); - } + // retrieve api key + UserModel user = + RestAssured.given() + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(UserModel.class); + this.client = new FeverClient(user.getId(), user.getApiKey()); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Test - void invalidApiKey() { - client.apiKey = "invalid-key"; + @Test + void invalidApiKey() { + client.apiKey = "invalid-key"; - FeverResponse response = client.execute("feeds"); - Assertions.assertFalse(response.isAuth()); - } + FeverResponse response = client.execute("feeds"); + Assertions.assertFalse(response.isAuth()); + } - @Test - void validApiKey() { - FeverResponse response = client.execute("feeds"); - Assertions.assertTrue(response.isAuth()); - } + @Test + void validApiKey() { + FeverResponse response = client.execute("feeds"); + Assertions.assertTrue(response.isAuth()); + } - @Test - void feeds() { - subscribe(getFeedUrl()); - FeverResponse feverResponse = client.execute("feeds"); - Assertions.assertEquals(1, feverResponse.getFeeds().size()); - } + @Test + void feeds() { + subscribe(getFeedUrl()); + FeverResponse feverResponse = client.execute("feeds"); + Assertions.assertEquals(1, feverResponse.getFeeds().size()); + } - @Test - void unreadEntries() { - subscribeAndWaitForEntries(getFeedUrl()); - FeverResponse feverResponse = client.execute("unread_item_ids"); - Assertions.assertEquals(2, feverResponse.getUnreadItemIds().size()); - } + @Test + void unreadEntries() { + subscribeAndWaitForEntries(getFeedUrl()); + FeverResponse feverResponse = client.execute("unread_item_ids"); + Assertions.assertEquals(2, feverResponse.getUnreadItemIds().size()); + } - @Test - void entries() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - FeverResponse feverResponse = client.execute("items"); - Assertions.assertEquals(2, feverResponse.getItems().size()); + @Test + void entries() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + FeverResponse feverResponse = client.execute("items"); + Assertions.assertEquals(2, feverResponse.getItems().size()); - FeverItem item = feverResponse.getItems().getFirst(); - Assertions.assertEquals(subscriptionId, item.getFeedId()); - Assertions.assertEquals("Item 2", item.getTitle()); - Assertions.assertEquals("Item 2 description", item.getHtml()); - Assertions.assertEquals("https://hostname.local/commafeed/2", item.getUrl()); - Assertions.assertFalse(item.isSaved()); - Assertions.assertFalse(item.isRead()); - } + FeverItem item = feverResponse.getItems().getFirst(); + Assertions.assertEquals(subscriptionId, item.getFeedId()); + Assertions.assertEquals("Item 2", item.getTitle()); + Assertions.assertEquals("Item 2 description", item.getHtml()); + Assertions.assertEquals("https://hostname.local/commafeed/2", item.getUrl()); + Assertions.assertFalse(item.isSaved()); + Assertions.assertFalse(item.isRead()); + } - @Test - void entriesByIds() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + @Test + void entriesByIds() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); - FeverResponse feverResponse = client.execute("items", new Param("with_ids", entry.getId())); - Assertions.assertEquals(1, feverResponse.getItems().size()); - } + FeverResponse feverResponse = client.execute("items", new Param("with_ids", entry.getId())); + Assertions.assertEquals(1, feverResponse.getItems().size()); + } - @Test - void savedEntries() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + @Test + void savedEntries() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); - StarRequest starRequest = new StarRequest(); - starRequest.setId(entry.getId()); - starRequest.setFeedId(subscriptionId); - starRequest.setStarred(true); - RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star"); + StarRequest starRequest = new StarRequest(); + starRequest.setId(entry.getId()); + starRequest.setFeedId(subscriptionId); + starRequest.setStarred(true); + RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star"); - FeverResponse feverResponse = client.execute("saved_item_ids"); - Assertions.assertEquals(1, feverResponse.getSavedItemIds().size()); - } + FeverResponse feverResponse = client.execute("saved_item_ids"); + Assertions.assertEquals(1, feverResponse.getSavedItemIds().size()); + } - @Test - void markEntry() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + @Test + void markEntry() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); - client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "read")); - Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); + client.execute( + "_", + new Param("mark", "item"), + new Param("id", entry.getId()), + new Param("as", "read")); + Assertions.assertEquals( + 1, + getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); - client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "unread")); - Assertions.assertEquals(0, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); - } + client.execute( + "_", + new Param("mark", "item"), + new Param("id", entry.getId()), + new Param("as", "unread")); + Assertions.assertEquals( + 0, + getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count()); + } - @Test - void markFeed() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + @Test + void markFeed() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - client.execute("_", new Param("mark", "feed"), new Param("id", String.valueOf(subscriptionId)), new Param("as", "read")); + client.execute( + "_", + new Param("mark", "feed"), + new Param("id", String.valueOf(subscriptionId)), + new Param("as", "read")); - Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); - } + Assertions.assertTrue( + getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); + } - @Test - void markCategory() { - String categoryId = createCategory("test-category"); - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl(), categoryId); + @Test + void markCategory() { + String categoryId = createCategory("test-category"); + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl(), categoryId); - client.execute("_", new Param("mark", "group"), new Param("id", String.valueOf(categoryId)), new Param("as", "read")); + client.execute( + "_", + new Param("mark", "group"), + new Param("id", String.valueOf(categoryId)), + new Param("as", "read")); - Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); - } + Assertions.assertTrue( + getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); + } - @Test - void tagEntry() { - Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); - Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); + @Test + void tagEntry() { + Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); + Entry entry = getFeedEntries(subscriptionId).getEntries().getFirst(); - client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "saved")); - Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isStarred).count()); + client.execute( + "_", + new Param("mark", "item"), + new Param("id", entry.getId()), + new Param("as", "saved")); + Assertions.assertEquals( + 1, + getFeedEntries(subscriptionId).getEntries().stream() + .filter(Entry::isStarred) + .count()); - client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "unsaved")); - Assertions.assertEquals(0, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isStarred).count()); - } + client.execute( + "_", + new Param("mark", "item"), + new Param("id", entry.getId()), + new Param("as", "unsaved")); + Assertions.assertEquals( + 0, + getFeedEntries(subscriptionId).getEntries().stream() + .filter(Entry::isStarred) + .count()); + } - @Test - void groups() { - createCategory("category-1"); - FeverResponse feverResponse = client.execute("groups"); - Assertions.assertEquals(1, feverResponse.getGroups().size()); - Assertions.assertEquals("category-1", feverResponse.getGroups().getFirst().getTitle()); - } + @Test + void groups() { + createCategory("category-1"); + FeverResponse feverResponse = client.execute("groups"); + Assertions.assertEquals(1, feverResponse.getGroups().size()); + Assertions.assertEquals("category-1", feverResponse.getGroups().getFirst().getTitle()); + } - @Test - void links() { - FeverResponse feverResponse = client.execute("links"); - Assertions.assertTrue(feverResponse.getLinks().isEmpty()); - } + @Test + void links() { + FeverResponse feverResponse = client.execute("links"); + Assertions.assertTrue(feverResponse.getLinks().isEmpty()); + } - private static class FeverClient { - private final Long userId; + private static class FeverClient { + private final Long userId; - @Setter - private String apiKey; + @Setter private String apiKey; - public FeverClient(Long userId, String apiKey) { - this.userId = userId; - this.apiKey = apiKey; - } + public FeverClient(Long userId, String apiKey) { + this.userId = userId; + this.apiKey = apiKey; + } - private FeverResponse execute(String action, Param... params) { - RequestSpecification spec = RestAssured.given() - .auth() - .none() - .formParam("api_key", Digests.md5Hex("admin:" + apiKey)) - .formParam(action, 1); + private FeverResponse execute(String action, Param... params) { + RequestSpecification spec = + RestAssured.given() + .auth() + .none() + .formParam("api_key", Digests.md5Hex("admin:" + apiKey)) + .formParam(action, 1); - for (Param param : params) { - spec.formParam(param.name(), param.value()); - } + for (Param param : params) { + spec.formParam(param.name(), param.value()); + } - return spec.post("rest/fever/user/{userId}", userId).then().statusCode(HttpStatus.SC_OK).extract().as(FeverResponse.class); + return spec.post("rest/fever/user/{userId}", userId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(FeverResponse.class); + } + } - } - } - - private record Param(String name, String value) {} + private record Param(String name, String value) {} } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java index 9ea51fa1..75d7e775 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java @@ -1,29 +1,32 @@ package com.commafeed.integration.rest; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - import com.commafeed.frontend.model.ServerInfo; import com.commafeed.integration.BaseIT; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; @QuarkusTest class ServerIT extends BaseIT { - @Test - void getServerInfos() { - ServerInfo serverInfos = RestAssured.given().get("/rest/server/get").then().statusCode(200).extract().as(ServerInfo.class); - Assertions.assertTrue(serverInfos.isAllowRegistrations()); - Assertions.assertTrue(serverInfos.isSmtpEnabled()); - Assertions.assertTrue(serverInfos.isDemoAccountEnabled()); - Assertions.assertTrue(serverInfos.isWebsocketEnabled()); - Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval()); - Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval()); - Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration()); - Assertions.assertEquals(4, serverInfos.getMinimumPasswordLength()); - Assertions.assertTrue(serverInfos.isPushNotificationsEnabled()); - - } + @Test + void getServerInfos() { + ServerInfo serverInfos = + RestAssured.given() + .get("/rest/server/get") + .then() + .statusCode(200) + .extract() + .as(ServerInfo.class); + Assertions.assertTrue(serverInfos.isAllowRegistrations()); + Assertions.assertTrue(serverInfos.isSmtpEnabled()); + Assertions.assertTrue(serverInfos.isDemoAccountEnabled()); + Assertions.assertTrue(serverInfos.isWebsocketEnabled()); + Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval()); + Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval()); + Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration()); + Assertions.assertEquals(4, serverInfos.getMinimumPasswordLength()); + Assertions.assertTrue(serverInfos.isPushNotificationsEnabled()); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java index 320fd883..8e481292 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java @@ -1,11 +1,19 @@ package com.commafeed.integration.rest; +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.Settings; +import com.commafeed.frontend.model.request.PasswordResetConfirmationRequest; +import com.commafeed.frontend.model.request.PasswordResetRequest; +import com.commafeed.integration.BaseIT; +import io.quarkus.mailer.MockMailbox; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.vertx.ext.mail.MailMessage; +import jakarta.inject.Inject; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.List; - -import jakarta.inject.Inject; - import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.junit.jupiter.api.AfterEach; @@ -13,88 +21,97 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.commafeed.TestConstants; -import com.commafeed.frontend.model.Settings; -import com.commafeed.frontend.model.request.PasswordResetConfirmationRequest; -import com.commafeed.frontend.model.request.PasswordResetRequest; -import com.commafeed.integration.BaseIT; - -import io.quarkus.mailer.MockMailbox; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.vertx.ext.mail.MailMessage; - @QuarkusTest class UserIT extends BaseIT { - @Inject - MockMailbox mailbox; + @Inject MockMailbox mailbox; - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - mailbox.clear(); - } + mailbox.clear(); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Test - void resetPassword() { - PasswordResetRequest req = new PasswordResetRequest(); - req.setEmail("admin@commafeed.com"); - RestAssured.given().body(req).contentType(ContentType.JSON).post("rest/user/passwordReset").then().statusCode(200); + @Test + void resetPassword() { + PasswordResetRequest req = new PasswordResetRequest(); + req.setEmail("admin@commafeed.com"); + RestAssured.given() + .body(req) + .contentType(ContentType.JSON) + .post("rest/user/passwordReset") + .then() + .statusCode(200); - List mails = mailbox.getMailMessagesSentTo("admin@commafeed.com"); - Assertions.assertEquals(1, mails.size()); + List mails = mailbox.getMailMessagesSentTo("admin@commafeed.com"); + Assertions.assertEquals(1, mails.size()); - MailMessage message = mails.getFirst(); - Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject()); - Assertions.assertTrue(message.getHtml().startsWith("You asked for password recovery for account 'admin'")); - Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst()); + MailMessage message = mails.getFirst(); + Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject()); + Assertions.assertTrue( + message.getHtml() + .startsWith("You asked for password recovery for account 'admin'")); + Assertions.assertEquals("admin@commafeed.com", message.getTo().getFirst()); - Element a = Jsoup.parse(message.getHtml()).select("a").getFirst(); - String link = a.attr("href"); + Element a = Jsoup.parse(message.getHtml()).select("a").getFirst(); + String link = a.attr("href"); - String email = null; - String token = null; - String queryString = link.substring(link.indexOf('?') + 1); - for (String param : queryString.split("&")) { - String[] keyValue = param.split("="); - if ("email".equals(keyValue[0])) { - email = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); - } else if ("token".equals(keyValue[0])) { - token = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); - } - } + String email = null; + String token = null; + String queryString = link.substring(link.indexOf('?') + 1); + for (String param : queryString.split("&")) { + String[] keyValue = param.split("="); + if ("email".equals(keyValue[0])) { + email = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); + } else if ("token".equals(keyValue[0])) { + token = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); + } + } - Assertions.assertNotNull(email); - Assertions.assertNotNull(token); - Assertions.assertTrue(link.contains("#/passwordReset?")); + Assertions.assertNotNull(email); + Assertions.assertNotNull(token); + Assertions.assertTrue(link.contains("#/passwordReset?")); - String newPassword = "MyNewPassword123!"; - PasswordResetConfirmationRequest confirmReq = new PasswordResetConfirmationRequest(); - confirmReq.setEmail(email); - confirmReq.setToken(token); - confirmReq.setPassword(newPassword); - RestAssured.given().body(confirmReq).contentType(ContentType.JSON).post("rest/user/passwordResetCallback").then().statusCode(200); + String newPassword = "MyNewPassword123!"; + PasswordResetConfirmationRequest confirmReq = new PasswordResetConfirmationRequest(); + confirmReq.setEmail(email); + confirmReq.setToken(token); + confirmReq.setPassword(newPassword); + RestAssured.given() + .body(confirmReq) + .contentType(ContentType.JSON) + .post("rest/user/passwordResetCallback") + .then() + .statusCode(200); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, newPassword); - RestAssured.given().get("rest/user/settings").then().statusCode(200); - } + RestAssured.authentication = + RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, newPassword); + RestAssured.given().get("rest/user/settings").then().statusCode(200); + } - @Test - void saveSettings() { - Settings settings = RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class); - settings.setLanguage("test"); - RestAssured.given().body(settings).contentType(ContentType.JSON).post("rest/user/settings").then().statusCode(200); + @Test + void saveSettings() { + Settings settings = + RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class); + settings.setLanguage("test"); + RestAssured.given() + .body(settings) + .contentType(ContentType.JSON) + .post("rest/user/settings") + .then() + .statusCode(200); - Settings updatedSettings = RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class); - Assertions.assertEquals("test", updatedSettings.getLanguage()); - } + Settings updatedSettings = + RestAssured.given().get("rest/user/settings").then().extract().as(Settings.class); + Assertions.assertEquals("test", updatedSettings.getLanguage()); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java index 4b168bc2..d313b0ae 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java @@ -1,45 +1,64 @@ package com.commafeed.integration.servlet; +import com.commafeed.TestConstants; +import com.commafeed.frontend.model.Settings; +import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; import org.apache.hc.core5.http.HttpStatus; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.commafeed.TestConstants; -import com.commafeed.frontend.model.Settings; -import com.commafeed.integration.BaseIT; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; - @QuarkusTest class CustomCodeIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Test - void test() { - // get settings - Settings settings = RestAssured.given().get("rest/user/settings").then().statusCode(200).extract().as(Settings.class); + @Test + void test() { + // get settings + Settings settings = + RestAssured.given() + .get("rest/user/settings") + .then() + .statusCode(200) + .extract() + .as(Settings.class); - // update settings - settings.setCustomJs("custom-js"); - settings.setCustomCss("custom-css"); - RestAssured.given().body(settings).contentType(ContentType.JSON).post("rest/user/settings").then().statusCode(HttpStatus.SC_OK); + // update settings + settings.setCustomJs("custom-js"); + settings.setCustomCss("custom-css"); + RestAssured.given() + .body(settings) + .contentType(ContentType.JSON) + .post("rest/user/settings") + .then() + .statusCode(HttpStatus.SC_OK); - // check custom code servlets - RestAssured.given().get("custom_js.js").then().statusCode(HttpStatus.SC_OK).body(CoreMatchers.is("custom-js")); - RestAssured.given().get("custom_css.css").then().statusCode(HttpStatus.SC_OK).body(CoreMatchers.is("custom-css")); - } + // check custom code servlets + RestAssured.given() + .get("custom_js.js") + .then() + .statusCode(HttpStatus.SC_OK) + .body(CoreMatchers.is("custom-js")); + RestAssured.given() + .get("custom_css.css") + .then() + .statusCode(HttpStatus.SC_OK) + .body(CoreMatchers.is("custom-css")); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java index 72b283dc..5b83e049 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java @@ -1,45 +1,49 @@ package com.commafeed.integration.servlet; +import com.commafeed.TestConstants; +import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.Headers; +import jakarta.ws.rs.core.HttpHeaders; import java.net.HttpCookie; import java.util.List; import java.util.stream.Collectors; - -import jakarta.ws.rs.core.HttpHeaders; - import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.commafeed.TestConstants; -import com.commafeed.integration.BaseIT; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import io.restassured.http.Headers; - @QuarkusTest class LogoutIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @Test - void test() { - List cookies = login(); - Headers responseHeaders = RestAssured.given() - .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .redirects() - .follow(false) - .get("logout") - .then() - .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT) - .extract() - .headers(); + @Test + void test() { + List cookies = login(); + Headers responseHeaders = + RestAssured.given() + .header( + HttpHeaders.COOKIE, + cookies.stream() + .map(HttpCookie::toString) + .collect(Collectors.joining(";"))) + .redirects() + .follow(false) + .get("logout") + .then() + .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT) + .extract() + .headers(); - List setCookieHeaders = responseHeaders.getValues(HttpHeaders.SET_COOKIE); - Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0)); - } + List setCookieHeaders = responseHeaders.getValues(HttpHeaders.SET_COOKIE); + Assertions.assertTrue( + setCookieHeaders.stream() + .flatMap(c -> HttpCookie.parse(c).stream()) + .allMatch(c -> c.getMaxAge() == 0)); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java index 89e5d2f8..42a841a5 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java @@ -1,43 +1,41 @@ package com.commafeed.integration.servlet; +import com.commafeed.TestConstants; +import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import jakarta.ws.rs.core.HttpHeaders; - import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.commafeed.TestConstants; -import com.commafeed.integration.BaseIT; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - @QuarkusTest class NextUnreadIT extends BaseIT { - @BeforeEach - void setup() { - initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); - } + @BeforeEach + void setup() { + initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + RestAssured.authentication = + RestAssured.preemptive() + .basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD); + } - @AfterEach - void cleanup() { - RestAssured.reset(); - } + @AfterEach + void cleanup() { + RestAssured.reset(); + } - @Test - void test() { - subscribeAndWaitForEntries(getFeedUrl()); - - RestAssured.given() - .redirects() - .follow(false) - .get("next") - .then() - .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT) - .header(HttpHeaders.LOCATION, "https://hostname.local/commafeed/2"); - } + @Test + void test() { + subscribeAndWaitForEntries(getFeedUrl()); + RestAssured.given() + .redirects() + .follow(false) + .get("next") + .then() + .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT) + .header(HttpHeaders.LOCATION, "https://hostname.local/commafeed/2"); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java index 1a99563d..f0bfb7f2 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java @@ -1,17 +1,19 @@ package com.commafeed.integration.servlet; +import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; -import com.commafeed.integration.BaseIT; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - @QuarkusTest class RobotsTxtIT extends BaseIT { - @Test - void test() { - RestAssured.given().get("robots.txt").then().statusCode(200).body(CoreMatchers.is("User-agent: *\nDisallow: /")); - } + @Test + void test() { + RestAssured.given() + .get("robots.txt") + .then() + .statusCode(200) + .body(CoreMatchers.is("User-agent: *\nDisallow: /")); + } } diff --git a/commafeed-server/src/test/java/com/commafeed/tools/CommaFeedPropertiesGeneratorTest.java b/commafeed-server/src/test/java/com/commafeed/tools/CommaFeedPropertiesGeneratorTest.java index e2500224..26adcc4d 100644 --- a/commafeed-server/src/test/java/com/commafeed/tools/CommaFeedPropertiesGeneratorTest.java +++ b/commafeed-server/src/test/java/com/commafeed/tools/CommaFeedPropertiesGeneratorTest.java @@ -1,27 +1,26 @@ package com.commafeed.tools; +import com.google.common.io.Resources; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.google.common.io.Resources; - class CommaFeedPropertiesGeneratorTest { - @Test - void testGenerate() throws Exception { - InputStream model = getClass().getResourceAsStream("/properties/quarkus-config-model.yaml"); - InputStream javadoc = getClass().getResourceAsStream("/properties/quarkus-config-javadoc.yaml"); - URL output = getClass().getResource("/properties/output.properties"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - new CommaFeedPropertiesGenerator().generate(model, javadoc, baos); + @Test + void testGenerate() throws Exception { + InputStream model = getClass().getResourceAsStream("/properties/quarkus-config-model.yaml"); + InputStream javadoc = + getClass().getResourceAsStream("/properties/quarkus-config-javadoc.yaml"); + URL output = getClass().getResource("/properties/output.properties"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new CommaFeedPropertiesGenerator().generate(model, javadoc, baos); - Assertions.assertLinesMatch(Resources.readLines(output, StandardCharsets.UTF_8).stream(), - baos.toString(StandardCharsets.UTF_8).lines()); - } - -} \ No newline at end of file + Assertions.assertLinesMatch( + Resources.readLines(output, StandardCharsets.UTF_8).stream(), + baos.toString(StandardCharsets.UTF_8).lines()); + } +}