From cc32f8ad16c18d32ed051c217b92f626788b96e5 Mon Sep 17 00:00:00 2001 From: Athou Date: Wed, 7 Aug 2024 08:10:14 +0200 Subject: [PATCH 01/50] WIP --- .github/workflows/build.yml | 110 +---- commafeed-client/pom.xml | 2 +- commafeed-client/src/app/client.ts | 12 +- .../src/pages/auth/RegistrationPage.tsx | 16 +- commafeed-client/vite.config.ts | 1 + commafeed-server/TODO.md | 30 ++ commafeed-server/config.dev.yml | 157 -------- commafeed-server/config.yml.example | 158 -------- commafeed-server/docker-compose.dev.yml | 6 +- commafeed-server/pom.xml | 370 ++++++++--------- .../src/main/assembly/zip-quarkus-app.xml | 21 + .../com/commafeed/CommaFeedApplication.java | 281 ++----------- .../com/commafeed/CommaFeedConfiguration.java | 379 +++++++++++------- .../java/com/commafeed/CommaFeedModule.java | 97 ----- .../com/commafeed/CommaFeedProducers.java | 31 ++ .../java/com/commafeed/CommaFeedVersion.java | 31 ++ .../java/com/commafeed/ExceptionMappers.java | 33 ++ .../java/com/commafeed/JacksonCustomizer.java | 27 ++ .../com/commafeed/NativeImageClasses.java | 165 ++++++++ .../com/commafeed/backend/HttpGetter.java | 24 +- .../backend/cache/RedisPoolFactory.java | 51 --- .../backend/dao/FeedCategoryDAO.java | 9 +- .../com/commafeed/backend/dao/FeedDAO.java | 8 +- .../backend/dao/FeedEntryContentDAO.java | 9 +- .../commafeed/backend/dao/FeedEntryDAO.java | 9 +- .../backend/dao/FeedEntryStatusDAO.java | 23 +- .../backend/dao/FeedEntryTagDAO.java | 9 +- .../backend/dao/FeedSubscriptionDAO.java | 26 +- .../com/commafeed/backend/dao/GenericDAO.java | 32 +- .../com/commafeed/backend/dao/UnitOfWork.java | 58 +-- .../com/commafeed/backend/dao/UserDAO.java | 9 +- .../commafeed/backend/dao/UserRoleDAO.java | 9 +- .../backend/dao/UserSettingsDAO.java | 9 +- .../favicon/DefaultFaviconFetcher.java | 5 +- .../favicon/FacebookFaviconFetcher.java | 3 +- .../favicon/YoutubeFaviconFetcher.java | 13 +- .../commafeed/backend/feed/FeedFetcher.java | 16 +- .../backend/feed/FeedRefreshEngine.java | 21 +- .../feed/FeedRefreshIntervalCalculator.java | 16 +- .../backend/feed/FeedRefreshUpdater.java | 2 - .../backend/feed/FeedRefreshWorker.java | 10 +- .../backend/feed/parser/FeedParser.java | 3 +- .../commafeed/backend/opml/OPMLExporter.java | 5 +- .../commafeed/backend/opml/OPMLImporter.java | 3 +- .../backend/rome/OPML11Generator.java | 3 + .../commafeed/backend/rome/OPML11Parser.java | 3 + .../rome/RSS090DescriptionConverter.java | 3 + .../backend/rome/RSS090DescriptionParser.java | 3 + .../backend/rome/RSSRDF10Parser.java | 3 + .../FeedEntryContentCleaningService.java | 3 +- .../service/FeedEntryContentService.java | 3 +- .../service/FeedEntryFilteringService.java | 3 +- .../backend/service/FeedEntryService.java | 3 +- .../backend/service/FeedEntryTagService.java | 3 +- .../backend/service/FeedService.java | 9 +- .../service/FeedSubscriptionService.java | 6 +- .../backend/service/MailService.java | 23 +- .../service/PasswordEncryptionService.java | 3 +- .../backend/service/UserService.java | 6 +- .../service/db/DatabaseCleaningService.java | 4 +- .../service/db/DatabaseStartupService.java | 94 +---- .../service/db/H2MigrationService.java | 87 ---- .../service/internal/PostLoginActivities.java | 3 +- .../backend/task/DemoAccountCleanupTask.java | 5 +- ...triesExceedingFeedCapacityCleanupTask.java | 5 +- .../backend/task/OldEntriesCleanupTask.java | 9 +- .../backend/task/OldStatusesCleanupTask.java | 5 +- .../task/OrphanedContentsCleanupTask.java | 3 +- .../task/OrphanedFeedsCleanupTask.java | 3 +- .../commafeed/backend/task/TaskScheduler.java | 28 ++ .../InPageReferenceFeedURLProvider.java | 3 + .../urlprovider/YoutubeFeedURLProvider.java | 3 + .../frontend/auth/SecurityCheck.java | 22 - .../frontend/auth/SecurityCheckFactory.java | 103 ----- .../auth/SecurityCheckFactoryProvider.java | 72 ---- .../commafeed/frontend/model/Category.java | 2 + .../com/commafeed/frontend/model/Entries.java | 2 + .../com/commafeed/frontend/model/Entry.java | 2 + .../commafeed/frontend/model/FeedInfo.java | 2 + .../commafeed/frontend/model/ServerInfo.java | 2 + .../commafeed/frontend/model/Settings.java | 2 + .../frontend/model/Subscription.java | 2 + .../commafeed/frontend/model/UnreadCount.java | 2 + .../commafeed/frontend/model/UserModel.java | 2 + .../frontend/model/request/LoginRequest.java | 24 -- .../request/ProfileModificationRequest.java | 2 +- .../model/request/RegistrationRequest.java | 2 +- .../frontend/resource/AdminREST.java | 60 +-- .../frontend/resource/CategoryREST.java | 82 ++-- .../frontend/resource/EntryREST.java | 48 ++- .../commafeed/frontend/resource/FeedREST.java | 130 +++--- .../frontend/resource/ServerREST.java | 51 ++- .../commafeed/frontend/resource/UserREST.java | 93 ++--- .../frontend/resource/fever/FeverREST.java | 29 +- .../servlet/AbstractCustomCodeServlet.java | 54 --- .../frontend/servlet/CustomCssServlet.java | 39 +- .../frontend/servlet/CustomJsServlet.java | 37 +- .../frontend/servlet/LogoutServlet.java | 33 +- .../frontend/servlet/NextUnreadServlet.java | 73 ++-- .../servlet/RobotsTxtDisallowAllServlet.java | 37 +- .../session/SessionHandlerFactory.java | 53 --- .../frontend/session/SessionHelper.java | 35 -- .../session/SessionHelperFactoryProvider.java | 51 --- .../frontend/ws/WebSocketConfigurator.java | 38 -- .../frontend/ws/WebSocketEndpoint.java | 52 ++- .../frontend/ws/WebSocketSessions.java | 2 - .../security/AuthenticationContext.java | 29 ++ .../java/com/commafeed/security/Roles.java | 6 + .../DatabaseApiKeyIdentityProvider.java | 50 +++ ...abaseUsernamePasswordIdentityProvider.java | 51 +++ .../identity/TrustedIdentityProvider.java | 57 +++ .../ApiKeyAuthenticationMecanism.java | 46 +++ .../PasswordConstraintValidator.java | 2 +- .../password}/ValidPassword.java | 2 +- .../commafeed/resource-config.json | 10 + .../src/main/resources/application.properties | 43 ++ .../resources/changelogs/db.changelog-1.0.xml | 4 +- .../resources/changelogs/db.changelog-1.1.xml | 4 +- .../resources/changelogs/db.changelog-1.2.xml | 7 +- .../resources/changelogs/db.changelog-1.3.xml | 5 +- .../resources/changelogs/db.changelog-1.4.xml | 9 +- .../resources/changelogs/db.changelog-1.5.xml | 5 +- .../resources/changelogs/db.changelog-2.1.xml | 5 +- .../resources/changelogs/db.changelog-2.2.xml | 5 +- .../resources/changelogs/db.changelog-2.6.xml | 6 +- .../resources/changelogs/db.changelog-3.2.xml | 2 +- .../resources/changelogs/db.changelog-3.5.xml | 2 +- .../resources/changelogs/db.changelog-3.6.xml | 2 +- .../resources/changelogs/db.changelog-3.8.xml | 2 +- .../resources/changelogs/db.changelog-3.9.xml | 4 +- .../resources/changelogs/db.changelog-4.0.xml | 2 +- .../resources/changelogs/db.changelog-4.1.xml | 2 +- .../resources/changelogs/db.changelog-4.2.xml | 2 +- .../resources/changelogs/db.changelog-4.3.xml | 2 +- .../resources/changelogs/db.changelog-4.4.xml | 2 +- .../{banner.txt => default_banner.txt} | 0 .../src/main/resources/migrations.xml | 2 +- .../CommaFeedDropwizardAppExtension.java | 125 ------ .../java/com/commafeed/DatabaseReset.java | 34 ++ .../com/commafeed/backend/HttpGetterTest.java | 38 +- .../backend/feed/FeedFetcherTest.java | 4 +- .../service/db/H2MigrationServiceTest.java | 29 -- .../com/commafeed/e2e/AuthentificationIT.java | 33 +- .../com/commafeed/e2e/PlaywrightTestBase.java | 140 ------- .../java/com/commafeed/e2e/ReadingIT.java | 30 +- .../auth/SecurityCheckFactoryTest.java | 35 -- .../frontend/resource/UserRestTest.java | 107 ----- .../frontend/session/SessionHelperTest.java | 63 --- .../com/commafeed/integration/BaseIT.java | 94 ++--- .../com/commafeed/integration/SecurityIT.java | 24 +- .../commafeed/integration/WebSocketIT.java | 25 +- .../commafeed/integration/rest/AdminIT.java | 9 +- .../commafeed/integration/rest/FeedIT.java | 26 +- .../commafeed/integration/rest/FeverIT.java | 5 +- .../commafeed/integration/rest/ServerIT.java | 5 +- .../commafeed/integration/rest/UserIT.java | 22 +- .../integration/servlet/CustomCodeIT.java | 12 +- .../integration/servlet/LogoutIT.java | 22 +- .../integration/servlet/NextUnreadIT.java | 14 +- .../integration/servlet/RobotsTxtIT.java | 2 + .../src/test/resources/config.test.yml | 138 ------- .../test/resources/docker-images.properties | 4 - .../h2-migration/database-v2.1.214.mv.db | Bin 28672 -> 0 bytes .../src/test/resources/logback-test.xml | 11 - 164 files changed, 2011 insertions(+), 3288 deletions(-) create mode 100644 commafeed-server/TODO.md delete mode 100644 commafeed-server/config.dev.yml delete mode 100644 commafeed-server/config.yml.example create mode 100644 commafeed-server/src/main/assembly/zip-quarkus-app.xml delete mode 100644 commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java create mode 100644 commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java create mode 100644 commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java create mode 100644 commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java create mode 100644 commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java create mode 100644 commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java create mode 100644 commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java create mode 100644 commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java create mode 100644 commafeed-server/src/main/java/com/commafeed/security/Roles.java create mode 100644 commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java create mode 100644 commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java create mode 100644 commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java create mode 100644 commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java rename commafeed-server/src/main/java/com/commafeed/{frontend/auth => security/password}/PasswordConstraintValidator.java (94%) rename commafeed-server/src/main/java/com/commafeed/{frontend/auth => security/password}/ValidPassword.java (90%) create mode 100644 commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json create mode 100644 commafeed-server/src/main/resources/application.properties rename commafeed-server/src/main/resources/{banner.txt => default_banner.txt} (100%) delete mode 100644 commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java create mode 100644 commafeed-server/src/test/java/com/commafeed/DatabaseReset.java delete mode 100644 commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java delete mode 100644 commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java delete mode 100644 commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java delete mode 100644 commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java delete mode 100644 commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java delete mode 100644 commafeed-server/src/test/resources/config.test.yml delete mode 100644 commafeed-server/src/test/resources/docker-images.properties delete mode 100644 commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db delete mode 100644 commafeed-server/src/test/resources/logback-test.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06d3ded3..d2ba3b56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,115 +7,39 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ "17", "21" ] + database: [ "h2", "postgresql", "mysql", "mariadb" ] steps: + # Checkout + - name: Configure git to checkout as-is + run: git config --global core.autocrlf false + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 # Setup - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Set up Java - uses: actions/setup-java@v4 + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 with: - java-version: ${{ matrix.java }} - distribution: "temurin" + java-version: "21" + distribution: "graalvm" cache: "maven" # Build & Test - name: Build with Maven - run: mvn --batch-mode --no-transfer-progress install - env: - TEST_DATABASE: h2 - - - name: Run integration tests on PostgreSQL - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: postgresql - - - name: Run integration tests on MySQL - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: mysql - - - name: Run integration tests on MariaDB - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: mariadb - - - name: Run integration tests with Redis cache enabled - run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify - env: - TEST_DATABASE: h2 - REDIS: true + run: mvn --batch-mode --no-transfer-progress install -Pnative -D"quarkus.datasource.db-kind"=${{ matrix.database }} # Upload artifacts - - name: Upload JAR - uses: actions/upload-artifact@v4 - if: ${{ matrix.java == '17' }} - with: - name: commafeed.jar - path: commafeed-server/target/commafeed.jar - - - name: Upload Playwright artifacts - if: failure() + - name: Upload cross-platform app uses: actions/upload-artifact@v4 with: - name: playwright-artifacts - path: | - **/target/playwright-artifacts/ + name: commafeed-${{ matrix.database }}-jvm + path: commafeed-server/target/commafeed-*.zip - # Docker - - name: Login to Container Registry - uses: docker/login-action@v3 - if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }} + - name: Upload native executable + uses: actions/upload-artifact@v4 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Docker build and push tag - uses: docker/build-push-action@v6 - if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} - with: - context: . - push: true - platforms: linux/amd64,linux/arm64/v8 - tags: | - athou/commafeed:latest - athou/commafeed:${{ github.ref_name }} - - - name: Docker build and push master - uses: docker/build-push-action@v6 - if: ${{ matrix.java == '17' && github.ref_name == 'master' }} - with: - context: . - push: true - platforms: linux/amd64,linux/arm64/v8 - tags: athou/commafeed:master - - # Create GitHub release after Docker image has been published - - name: Extract Changelog Entry - uses: mindsers/changelog-reader-action@v2 - if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} - id: changelog_reader - with: - version: ${{ github.ref_name }} - - - name: Create GitHub release - uses: softprops/action-gh-release@v2 - if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} - with: - name: CommaFeed ${{ github.ref_name }} - body: ${{ steps.changelog_reader.outputs.changes }} - draft: false - prerelease: false - files: | - commafeed-server/target/commafeed.jar - commafeed-server/config.yml.example + name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }} + path: commafeed-server/target/commafeed-*-runner diff --git a/commafeed-client/pom.xml b/commafeed-client/pom.xml index 7049bbff..c55058fb 100644 --- a/commafeed-client/pom.xml +++ b/commafeed-client/pom.xml @@ -80,7 +80,7 @@ copy-resources - ${project.build.directory}/classes/assets + ${project.build.directory}/classes/META-INF/resources dist diff --git a/commafeed-client/src/app/client.ts b/commafeed-client/src/app/client.ts index 3e9a7cd1..e34759ed 100644 --- a/commafeed-client/src/app/client.ts +++ b/commafeed-client/src/app/client.ts @@ -81,7 +81,17 @@ export const client = { }, }, user: { - login: async (req: LoginRequest) => await axiosInstance.post("user/login", req), + login: async (req: LoginRequest) => { + const formData = new URLSearchParams() + formData.append("j_username", req.name) + formData.append("j_password", req.password) + return await axiosInstance.post("j_security_check", formData, { + baseURL: ".", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + }, register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req), passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req), getSettings: async () => await axiosInstance.get("user/settings"), diff --git a/commafeed-client/src/pages/auth/RegistrationPage.tsx b/commafeed-client/src/pages/auth/RegistrationPage.tsx index a5570687..23dd8fc0 100644 --- a/commafeed-client/src/pages/auth/RegistrationPage.tsx +++ b/commafeed-client/src/pages/auth/RegistrationPage.tsx @@ -24,12 +24,18 @@ export function RegistrationPage() { }, }) - const register = useAsyncCallback(client.user.register, { + const login = useAsyncCallback(client.user.login, { onSuccess: () => { dispatch(redirectToRootCategory()) }, }) + const register = useAsyncCallback(client.user.register, { + onSuccess: () => { + login.execute(form.values) + }, + }) + return ( @@ -50,6 +56,12 @@ export function RegistrationPage() { )} + {login.error && ( + + + + )} +
@@ -68,7 +80,7 @@ export function RegistrationPage() { size="md" required /> -
diff --git a/commafeed-client/vite.config.ts b/commafeed-client/vite.config.ts index eaba0d76..4abeea41 100644 --- a/commafeed-client/vite.config.ts +++ b/commafeed-client/vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig(env => ({ "/openapi.json": "http://localhost:8083", "/custom_css.css": "http://localhost:8083", "/custom_js.js": "http://localhost:8083", + "/j_security_check": "http://localhost:8083", "/logout": "http://localhost:8083", }, }, diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md new file mode 100644 index 00000000..3a14ea57 --- /dev/null +++ b/commafeed-server/TODO.md @@ -0,0 +1,30 @@ +TODO +---- + +MVP: + +- quarkus mailer for smtp + - https://quarkus.io/guides/mailer +- cookie duration too short + - https://github.com/quarkusio/quarkus/issues/42463 + +- update dockerfile +- update github actions (build and copy outside target each database artifact) +- update readme +- update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) + +Nice to have: + +- find a better way to scan rome classes +- remove suppresswarnings "deprecation" +- remove rest assured or use only rest assured +- rename "servlets" since they are now rest endpoints +- warnings hibernate on startup +- OPML encoding is not handled correctly +- remove Timers metrics page + +native-image +------------- + +- https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Resources/ +- https://github.com/rometools/rome/pull/636/files \ No newline at end of file diff --git a/commafeed-server/config.dev.yml b/commafeed-server/config.dev.yml deleted file mode 100644 index 600490ea..00000000 --- a/commafeed-server/config.dev.yml +++ /dev/null @@ -1,157 +0,0 @@ -# CommaFeed settings -# ------------------ -app: - # url used to access commafeed - publicUrl: http://localhost:8082/ - - # whether to expose a robots.txt file that disallows web crawlers and search engine indexers - hideFromWebCrawlers: true - - # whether to allow user registrations - allowRegistrations: true - - # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) - strictPasswordPolicy: true - - # create a demo account the first time the app starts - createDemoAccount: true - - # put your google analytics tracking code here - googleAnalyticsTrackingCode: - - # put your google server key (used for youtube favicon fetching) - googleAuthKey: - - # number of http threads - backgroundThreads: 3 - - # number of database updating threads - databaseUpdateThreads: 1 - - # rows to delete per query while cleaning up old entries - databaseCleanupBatchSize: 100 - - # settings for sending emails (password recovery) - smtpHost: localhost - smtpPort: 25 - smtpTls: false - smtpUserName: user - smtpPassword: pass - smtpFromAddress: - - # Graphite Metric settings - # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds) - graphiteEnabled: false - graphitePrefix: "test.commafeed" - graphiteHost: "localhost" - graphitePort: 2003 - graphiteInterval: 60 - - # whether this commafeed instance has a lot of feeds to refresh - # leave this to false in almost all cases - heavyLoad: false - - # minimum amount of time commafeed will wait before refreshing the same feed - refreshIntervalMinutes: 5 - - # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser - # useful if commafeed is usually accessed through a restricting proxy - imageProxyEnabled: true - - # database query timeout (in milliseconds), 0 to disable - queryTimeout: 0 - - # time to keep unread statuses (in days), 0 to disable - keepStatusDays: 0 - - # entries to keep per feed, old entries will be deleted, 0 to disable - maxFeedCapacity: 500 - - # entries older than this will be deleted, 0 to disable - maxEntriesAgeDays: 365 - - # limit the number of feeds a user can subscribe to, 0 to disable - maxFeedsPerUser: 0 - - # don't parse feeds that are too large to prevent memory issues - maxFeedResponseSize: 5M - - # cache service to use, possible values are 'noop' and 'redis' - cache: noop - - # announcement string displayed on the main page - announcement: - - # user-agent string that will be used by the http client, leave empty for the default one - userAgent: - - # enable websocket connection so the server can notify the web client that there are new entries for your feeds - websocketEnabled: true - - # interval at which the client will send a ping message on the websocket to keep the connection alive - websocketPingInterval: 15m - - # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval - treeReloadInterval: 30s - -# Database connection -# ------------------- -# for MariaDB -# driverClass is org.mariadb.jdbc.Driver -# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for MySQL -# driverClass is com.mysql.cj.jdbc.Driver -# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for PostgreSQL -# driverClass is org.postgresql.Driver -# url is jdbc:postgresql://localhost:5432/commafeed - -database: - driverClass: org.h2.Driver - url: jdbc:h2:./target/commafeed - user: sa - password: sa - properties: - charSet: UTF-8 - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - -server: - applicationConnectors: - - type: http - port: 8083 - adminConnectors: - - type: http - port: 8084 - -logging: - level: INFO - loggers: - com.commafeed: DEBUG - liquibase: INFO - org.hibernate.SQL: INFO # or ALL for sql debugging - org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN - appenders: - - type: console - - type: file - currentLogFilename: log/commafeed.log - threshold: ALL - archive: true - archivedLogFilenamePattern: log/commafeed-%d.log - archivedFileCount: 5 - timeZone: UTC - -# Redis pool configuration -# (only used if app.cache is 'redis') -# ----------------------------------- -redis: - host: localhost - port: 6379 - # username is only required when using ACLs - username: - password: - timeout: 2000 - database: 0 - maxTotal: 500 - \ No newline at end of file diff --git a/commafeed-server/config.yml.example b/commafeed-server/config.yml.example deleted file mode 100644 index 8568b50d..00000000 --- a/commafeed-server/config.yml.example +++ /dev/null @@ -1,158 +0,0 @@ -# CommaFeed settings -# ------------------ -app: - # url used to access commafeed - publicUrl: http://localhost:8082/ - - # whether to expose a robots.txt file that disallows web crawlers and search engine indexers - hideFromWebCrawlers: true - - # whether to allow user registrations - allowRegistrations: false - - # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) - strictPasswordPolicy: true - - # create a demo account the first time the app starts - createDemoAccount: false - - # put your google analytics tracking code here - googleAnalyticsTrackingCode: - - # put your google server key (used for youtube favicon fetching) - googleAuthKey: - - # number of http threads - backgroundThreads: 3 - - # number of database updating threads - databaseUpdateThreads: 1 - - # rows to delete per query while cleaning up old entries - databaseCleanupBatchSize: 100 - - # settings for sending emails (password recovery) - smtpHost: - smtpPort: - smtpTls: false - smtpUserName: - smtpPassword: - smtpFromAddress: - - # Graphite Metric settings - # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds) - graphiteEnabled: false - graphitePrefix: "test.commafeed" - graphiteHost: "localhost" - graphitePort: 2003 - graphiteInterval: 60 - - # whether this commafeed instance has a lot of feeds to refresh - # leave this to false in almost all cases - heavyLoad: false - - # minimum amount of time commafeed will wait before refreshing the same feed - refreshIntervalMinutes: 5 - - # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser - # useful if commafeed is usually accessed through a restricting proxy - imageProxyEnabled: false - - # database query timeout (in milliseconds), 0 to disable - queryTimeout: 0 - - # time to keep unread statuses (in days), 0 to disable - keepStatusDays: 0 - - # entries to keep per feed, old entries will be deleted, 0 to disable - maxFeedCapacity: 500 - - # entries older than this will be deleted, 0 to disable - maxEntriesAgeDays: 365 - - # limit the number of feeds a user can subscribe to, 0 to disable - maxFeedsPerUser: 0 - - # don't parse feeds that are too large to prevent memory issues - maxFeedResponseSize: 5M - - # cache service to use, possible values are 'noop' and 'redis' - cache: noop - - # announcement string displayed on the main page - announcement: - - # user-agent string that will be used by the http client, leave empty for the default one - userAgent: - - # enable websocket connection so the server can notify the web client that there are new entries for your feeds - websocketEnabled: true - - # interval at which the client will send a ping message on the websocket to keep the connection alive - websocketPingInterval: 15m - - # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval - treeReloadInterval: 30s - -# Database connection -# ------------------- -# for MariaDB -# driverClass is org.mariadb.jdbc.Driver -# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for MySQL -# driverClass is com.mysql.cj.jdbc.Driver -# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for PostgreSQL -# driverClass is org.postgresql.Driver -# url is jdbc:postgresql://localhost:5432/commafeed - -database: - driverClass: org.h2.Driver - url: jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE - user: sa - password: sa - properties: - charSet: UTF-8 - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - minSize: 1 - maxSize: 50 - maxConnectionAge: 30m - -server: - applicationConnectors: - - type: http - port: 8082 - adminConnectors: [ ] - requestLog: - appenders: [ ] - -logging: - level: ERROR - loggers: - com.commafeed: INFO - liquibase: INFO - io.dropwizard.server.ServerFactory: INFO - appenders: - - type: console - - type: file - currentLogFilename: log/commafeed.log - threshold: ALL - archive: true - archivedLogFilenamePattern: log/commafeed-%d.log - archivedFileCount: 5 - timeZone: UTC - -# Redis pool configuration -# (only used if app.cache is 'redis') -# ----------------------------------- -redis: - host: localhost - port: 6379 - # username is only required when using ACLs - username: - password: - timeout: 2000 - database: 0 - maxTotal: 500 diff --git a/commafeed-server/docker-compose.dev.yml b/commafeed-server/docker-compose.dev.yml index 9fdfb1a6..29b9c6f8 100644 --- a/commafeed-server/docker-compose.dev.yml +++ b/commafeed-server/docker-compose.dev.yml @@ -4,7 +4,7 @@ services: mysql: image: mariadb environment: - - MYSQL_ROOT_PASSWORD=root + - MYSQL_ROOT_PASSWORD=commafeed - MYSQL_DATABASE=commafeed ports: - "3306:3306" @@ -12,8 +12,8 @@ services: postgresql: image: postgres environment: - - POSTGRES_USER=root - - POSTGRES_PASSWORD=root + - POSTGRES_USER=commafeed + - POSTGRES_PASSWORD=commafeed - POSTGRES_DB=commafeed ports: - "5432:5432" diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 8522cfad..4ab11efd 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -12,28 +12,20 @@ CommaFeed Server - 7.0.0 + 3.13.1 6.6 2.1.0 - 1.78.1 + 3.1.8 - 1.20.1 - - 16.4 - - 9.0.1 - - 11.4.3 - - 7.4.0 + h2 - io.dropwizard - dropwizard-dependencies - 4.0.7 + io.quarkus.platform + quarkus-bom + ${quarkus.version} pom import @@ -41,26 +33,67 @@ - commafeed - - - src/test/resources - false - - - src/test/resources - - docker-images.properties - - true - - - + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.version} + true + + + + build + generate-code + generate-code-tests + native-image-agent + + + + commafeed-${project.version} + + -${quarkus.datasource.db-kind}-${os.detected.name}-${os.detected.arch}-runner + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + package + + single + + + commafeed-${project.version}-${quarkus.datasource.db-kind} + false + + src/main/assembly/zip-quarkus-app.xml + + + + + org.apache.maven.plugins maven-surefire-plugin 3.3.1 + + + org.jboss.logmanager.LogManager + + org.apache.maven.plugins @@ -74,6 +107,13 @@ + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + + io.github.git-commit-id @@ -94,63 +134,13 @@ false - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - org.kordamp.shade - maven-shade-ext-transformers - 1.4.0 - - - - false - - - *:* - - module-info.class - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - package - - shade - - - - - - com.commafeed.CommaFeedApplication - - - - rome.properties - - append - - - - - - io.swagger.core.v3 swagger-maven-plugin-jakarta 2.2.22 - ${project.build.directory}/classes/assets + ${project.build.directory}/classes/META-INF/resources JSONANDYAML com.commafeed.frontend.resource @@ -168,18 +158,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - true - - - - org.apache.maven.plugins maven-checkstyle-plugin @@ -247,60 +225,65 @@ provided - org.slf4j - slf4j-api - - - org.slf4j - jcl-over-slf4j + org.kohsuke.metainf-services + metainf-services + 1.11 + provided - com.google.inject - guice - ${guice.version} + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-websockets + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-liquibase + + + io.quarkus + quarkus-jdbc-h2 + + + io.quarkus + quarkus-jdbc-mysql + + + io.quarkus + quarkus-jdbc-mariadb + + + io.quarkus + quarkus-jdbc-postgresql - - io.dropwizard - dropwizard-core - - - io.dropwizard - dropwizard-unix-socket - - - io.dropwizard - dropwizard-hibernate - - - io.dropwizard - dropwizard-migrations - - - io.dropwizard - dropwizard-assets - - - io.dropwizard - dropwizard-forms - - - io.dropwizard.metrics - metrics-graphite - io.dropwizard.metrics metrics-json - - - io.whitfin - dropwizard-environment-substitutor - 1.1.1 - - - org.eclipse.jetty.websocket - websocket-jakarta-server + 4.2.26 @@ -357,7 +340,6 @@ com.sun.mail jakarta.mail - 2.0.1 @@ -409,7 +391,15 @@ org.apache.httpcomponents.client5 httpclient5 + 5.3.1 + + + org.brotli + dec + 0.1.2 + + io.github.hakky54 sslcontext-kickstart-for-apache5 @@ -423,35 +413,8 @@ - com.h2database - h2 - 2.3.232 - - - com.manticore-projects.tools - h2migrationtool - 1.7 - - - - com.mysql - mysql-connector-j - 9.0.0 - - - org.mariadb.jdbc - mariadb-java-client - 3.4.1 - - - org.postgresql - postgresql - 42.7.3 - - - - org.junit.jupiter - junit-jupiter-engine + io.quarkus + quarkus-junit5 test @@ -464,39 +427,41 @@ mockito-junit-jupiter test - org.mock-server mockserver-junit-jupiter 5.15.0 test - - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} + org.glassfish.jersey.core + jersey-client + ${jersey.version} test - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.version} + test + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + test + + + io.rest-assured + rest-assured test - com.icegreen greenmail-junit5 2.0.1 test - - io.dropwizard - dropwizard-testing - test - org.awaitility awaitility @@ -508,30 +473,19 @@ 1.46.0 test - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - org.testcontainers - mysql - ${testcontainers.version} - test - - - org.testcontainers - mariadb - ${testcontainers.version} - test - + + + + native + + + native + + + + true + + + diff --git a/commafeed-server/src/main/assembly/zip-quarkus-app.xml b/commafeed-server/src/main/assembly/zip-quarkus-app.xml new file mode 100644 index 00000000..e35ab082 --- /dev/null +++ b/commafeed-server/src/main/assembly/zip-quarkus-app.xml @@ -0,0 +1,21 @@ + + + zip-quarkus-app + + true + commafeed-${project.version}-${quarkus.datasource.db-kind} + + + zip + + + + ${project.build.directory}/quarkus-app + / + + **/* + + + + \ No newline at end of file diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java index 4bd7183f..222b6548 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java @@ -1,277 +1,44 @@ package com.commafeed; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; -import java.util.EnumSet; -import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; -import org.hibernate.cfg.AvailableSettings; - -import com.codahale.metrics.json.MetricsModule; -import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.feed.FeedRefreshEngine; -import com.commafeed.backend.model.AbstractModel; -import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedCategory; -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 com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole; -import com.commafeed.backend.model.UserSettings; -import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.db.DatabaseStartupService; -import com.commafeed.backend.service.db.H2MigrationService; -import com.commafeed.backend.task.ScheduledTask; -import com.commafeed.frontend.auth.PasswordConstraintValidator; -import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; -import com.commafeed.frontend.resource.AdminREST; -import com.commafeed.frontend.resource.CategoryREST; -import com.commafeed.frontend.resource.EntryREST; -import com.commafeed.frontend.resource.FeedREST; -import com.commafeed.frontend.resource.ServerREST; -import com.commafeed.frontend.resource.UserREST; -import com.commafeed.frontend.resource.fever.FeverREST; -import com.commafeed.frontend.servlet.CustomCssServlet; -import com.commafeed.frontend.servlet.CustomJsServlet; -import com.commafeed.frontend.servlet.LogoutServlet; -import com.commafeed.frontend.servlet.NextUnreadServlet; -import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet; -import com.commafeed.frontend.session.SessionHelperFactoryProvider; -import com.commafeed.frontend.ws.WebSocketConfigurator; -import com.commafeed.frontend.ws.WebSocketEndpoint; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Key; -import com.google.inject.TypeLiteral; +import com.commafeed.backend.task.TaskScheduler; +import com.commafeed.security.password.PasswordConstraintValidator; -import io.dropwizard.assets.AssetsBundle; -import io.dropwizard.configuration.DefaultConfigurationFactoryFactory; -import io.dropwizard.configuration.EnvironmentVariableSubstitutor; -import io.dropwizard.configuration.SubstitutingSourceProvider; -import io.dropwizard.core.Application; -import io.dropwizard.core.ConfiguredBundle; -import io.dropwizard.core.setup.Bootstrap; -import io.dropwizard.core.setup.Environment; -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.forms.MultiPartBundle; -import io.dropwizard.hibernate.HibernateBundle; -import io.dropwizard.migrations.MigrationsBundle; -import io.dropwizard.servlets.CacheBustingFilter; -import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor; -import jakarta.servlet.DispatcherType; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.websocket.server.ServerEndpointConfig; +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; -public class CommaFeedApplication extends Application { +@Singleton +@RequiredArgsConstructor +public class CommaFeedApplication { public static final String USERNAME_ADMIN = "admin"; public static final String USERNAME_DEMO = "demo"; public static final Instant STARTUP_TIME = Instant.now(); - private HibernateBundle hibernateBundle; + private final DatabaseStartupService databaseStartupService; + private final FeedRefreshEngine feedRefreshEngine; + private final TaskScheduler taskScheduler; + private final CommaFeedConfiguration config; - @Override - public String getName() { - return "CommaFeed"; + public void start(@Observes StartupEvent ev) { + PasswordConstraintValidator.setStrict(config.users().strictPasswordPolicy()); + + databaseStartupService.populateInitialData(); + + feedRefreshEngine.start(); + taskScheduler.start(); } - @Override - public void initialize(Bootstrap bootstrap) { - configureEnvironmentSubstitutor(bootstrap); - configureObjectMapper(bootstrap.getObjectMapper()); - - // run h2 migration as the first bundle because we need to migrate before hibernate is initialized - bootstrap.addBundle(new ConfiguredBundle<>() { - @Override - public void run(CommaFeedConfiguration config, Environment environment) { - DataSourceFactory dataSourceFactory = config.getDataSourceFactory(); - String url = dataSourceFactory.getUrl(); - if (isFileBasedH2(url)) { - Path path = getFilePath(url); - String user = dataSourceFactory.getUser(); - String password = dataSourceFactory.getPassword(); - new H2MigrationService().migrateIfNeeded(path, user, password); - } - } - - private boolean isFileBasedH2(String url) { - return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:"); - } - - private Path getFilePath(String url) { - String name = url.substring("jdbc:h2:".length()).split(";")[0]; - return Paths.get(name + ".mv.db"); - } - }); - - bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class, - FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class, - UserSettings.class) { - @Override - public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { - DataSourceFactory factory = configuration.getDataSourceFactory(); - - factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo"); - - factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50"); - factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); - factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true"); - factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true"); - return factory; - } - }); - - bootstrap.addBundle(new MigrationsBundle<>() { - @Override - public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { - return configuration.getDataSourceFactory(); - } - }); - - bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html")); - bootstrap.addBundle(new MultiPartBundle()); + public void stop(@Observes ShutdownEvent ev) { + feedRefreshEngine.stop(); + taskScheduler.stop(); } - private static void configureEnvironmentSubstitutor(Bootstrap bootstrap) { - bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<>() { - @Override - protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) { - // disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty - return objectMapper - .setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)); - } - }); - - bootstrap.setConfigurationSourceProvider(buildEnvironmentSubstitutor(bootstrap)); - } - - private static void configureObjectMapper(ObjectMapper objectMapper) { - // read and write instants as milliseconds instead of nanoseconds - objectMapper.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)); - } - - private static EnvironmentSubstitutor buildEnvironmentSubstitutor(Bootstrap bootstrap) { - // enable config.yml string substitution - // e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR - SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), - new EnvironmentVariableSubstitutor(false)); - - // enable config.yml properties override with env variables prefixed with CF_ - // e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true - return new EnvironmentSubstitutor("CF", substitutingSourceProvider); - } - - @Override - public void run(CommaFeedConfiguration config, Environment environment) { - PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy()); - - // guice init - Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); - - // session management - environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory())); - - // support for "@SecurityCheck User user" injection - environment.jersey() - .register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserDAO.class), - injector.getInstance(UserService.class), config)); - // support for "@Context SessionHelper sessionHelper" injection - environment.jersey().register(new SessionHelperFactoryProvider.Binder()); - - // REST resources - environment.jersey().setUrlPattern("/rest/*"); - environment.jersey().register(injector.getInstance(AdminREST.class)); - environment.jersey().register(injector.getInstance(CategoryREST.class)); - environment.jersey().register(injector.getInstance(EntryREST.class)); - environment.jersey().register(injector.getInstance(FeedREST.class)); - environment.jersey().register(injector.getInstance(ServerREST.class)); - environment.jersey().register(injector.getInstance(UserREST.class)); - environment.jersey().register(injector.getInstance(FeverREST.class)); - - // Servlets - environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next"); - environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout"); - environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css"); - environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js"); - if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) { - environment.servlets() - .addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class)) - .addMapping("/robots.txt"); - } - - // WebSocket endpoint - JakartaWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> { - container.setDefaultMaxSessionIdleTimeout(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000); - - container.addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws") - .configurator(injector.getInstance(WebSocketConfigurator.class)) - .build()); - }); - - // Scheduled tasks - Set tasks = injector.getInstance(Key.get(new TypeLiteral<>() { - })); - ScheduledExecutorService executor = environment.lifecycle() - .scheduledExecutorService("task-scheduler", true) - .threads(tasks.size()) - .build(); - for (ScheduledTask task : tasks) { - task.register(executor); - } - - // database init/changelogs - environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class)); - - // start feed fetching engine - environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class)); - - // prevent caching index.html, so that the webapp is always up to date - environment.servlets() - .addFilter("index-cache-busting-filter", new CacheBustingFilter()) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/"); - - // prevent caching openapi files, so that the documentation is always up to date - environment.servlets() - .addFilter("openapi-cache-busting-filter", new CacheBustingFilter()) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/openapi.json", "/openapi.yaml"); - - // prevent caching REST resources, except for favicons - environment.servlets().addFilter("rest-cache-busting-filter", new CacheBustingFilter() { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - String path = ((HttpServletRequest) request).getRequestURI(); - if (path.contains("/feed/favicon")) { - chain.doFilter(request, response); - } else { - super.doFilter(request, response, chain); - } - } - }).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*"); - - } - - public static void main(String[] args) throws Exception { - new CommaFeedApplication().run(args); - } } diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 845c01cd..eb64073f 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -1,188 +1,287 @@ package com.commafeed; -import java.io.IOException; -import java.io.InputStream; +import java.time.Duration; import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Properties; +import java.util.Optional; -import com.commafeed.backend.cache.RedisPoolFactory; -import com.commafeed.frontend.session.SessionHandlerFactory; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.commafeed.backend.feed.FeedRefreshIntervalCalculator; -import io.dropwizard.core.Configuration; -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.util.DataSize; -import io.dropwizard.util.Duration; -import jakarta.validation.Valid; +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.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; -import lombok.Getter; -import lombok.Setter; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Protocol; -@Getter -@Setter -public class CommaFeedConfiguration extends Configuration { +/** + * CommaFeed configuration + * + * Default values are for production, they can be overridden in application.properties + */ +@ConfigMapping(prefix = "commafeed") +public interface CommaFeedConfiguration { - public enum CacheType { - NOOP, REDIS + /** + * URL used to access commafeed, used for various redirects. + * + */ + @NotBlank + @WithDefault("http://localhost:8082") + String publicUrl(); + + /** + * 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(); + + /** + * Message displayed in a notification at the bottom of the page. + */ + Optional announcement(); + + /** + * Google Analytics tracking code. + */ + Optional googleAnalyticsTrackingCode(); + + /** + * Google Auth key for fetching Youtube favicons. + */ + Optional googleAuthKey(); + + /** + * Feed refresh engine settings. + */ + FeedRefresh feedRefresh(); + + /** + * Database settings. + */ + Database database(); + + /** + * Users settings. + */ + Users users(); + + /** + * Websocket settings. + */ + Websocket websocket(); + + /** + * SMTP settings for password recovery. + */ + Optional smtp(); + + /** + * Redis settings to enable caching. This is only really useful on instances with a lot of users. + */ + Redis redis(); + + interface FeedRefresh { + /** + * Amount of time CommaFeed will wait before refreshing the same feed. + */ + @WithDefault("5m") + Duration interval(); + + /** + * If true, CommaFeed will calculate the next refresh time based on the feed's average entry interval and the time since the last + * entry was published. See {@link FeedRefreshIntervalCalculator} for details. + */ + @WithDefault("false") + boolean intervalEmpirical(); + + /** + * 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(); + + /** + * If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed. + */ + @WithDefault("5M") + MemorySize maxResponseSize(); + + /** + * Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. + */ + @WithDefault("0") + Duration userInactivityPeriod(); + + /** + * User-Agent string that will be used by the http client, leave empty for the default one. + */ + Optional userAgent(); } - @Valid - @NotNull - @JsonProperty("database") - private final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + interface Database { + /** + * Database query timeout. + */ + @WithDefault("0") + int queryTimeout(); - @Valid - @NotNull - @JsonProperty("redis") - private final RedisPoolFactory redisPoolFactory = new RedisPoolFactory(); + Cleanup cleanup(); - @Valid - @NotNull - @JsonProperty("session") - private final SessionHandlerFactory sessionHandlerFactory = new SessionHandlerFactory(); + interface Cleanup { + /** + * Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable. + */ + @WithDefault("365d") + Duration entriesMaxAge(); - @Valid - @NotNull - @JsonProperty("app") - private ApplicationSettings applicationSettings; + /** + * Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. 0 to disable. + */ + @WithDefault("0") + Duration statusesMaxAge(); - private final String version; - private final String gitCommit; + /** + * Maximum number of entries per feed to keep in the database. 0 to disable. + */ + @WithDefault("500") + int maxFeedCapacity(); - public CommaFeedConfiguration() { - Properties properties = new Properties(); - try (InputStream stream = getClass().getResourceAsStream("/git.properties")) { - if (stream != null) { - properties.load(stream); + /** + * 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(); + + default Instant statusesInstantThreshold() { + return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null; } - } catch (IOException e) { - throw new RuntimeException(e); } - - this.version = properties.getProperty("git.build.version", "unknown"); - this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown"); } - @Getter - @Setter - public static class ApplicationSettings { - @NotNull - @NotBlank - @Valid - private String publicUrl; + interface Users { + /** + * Whether to let users create accounts for themselves. + */ + @WithDefault("false") + boolean allowRegistrations(); - @NotNull - @Valid - private Boolean hideFromWebCrawlers = true; + /** + * Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char). + */ + @WithDefault("true") + boolean strictPasswordPolicy(); - @NotNull - @Valid - private Boolean allowRegistrations; + /** + * Whether to create a demo account the first time the app starts. + */ + @WithDefault("false") + boolean createDemoAccount(); + } - @NotNull - @Valid - private Boolean strictPasswordPolicy = true; + interface Smtp { + String host(); - @NotNull - @Valid - private Boolean createDemoAccount; + int port(); - private String googleAnalyticsTrackingCode; + boolean tls(); - private String googleAuthKey; + String userName(); - @NotNull - @Min(1) - @Valid - private Integer backgroundThreads; + String password(); - @NotNull - @Min(1) - @Valid - private Integer databaseUpdateThreads; + String fromAddress(); + } - @NotNull - @Positive - @Valid - private Integer databaseCleanupBatchSize = 100; + interface Websocket { + /** + * Enable websocket connection so the server can notify the web client that there are new entries for your feeds. + */ + @WithDefault("true") + boolean enabled(); - private String smtpHost; - private int smtpPort; - private boolean smtpTls; - private String smtpUserName; - private String smtpPassword; - private String smtpFromAddress; + /** + * Interval at which the client will send a ping message on the websocket to keep the connection alive. + */ + @WithDefault("15m") + Duration pingInterval(); - private boolean graphiteEnabled; - private String graphitePrefix; - private String graphiteHost; - private int graphitePort; - private int graphiteInterval; + /** + * 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(); + } - @NotNull - @Valid - private Boolean heavyLoad; + interface Redis { - @NotNull - @Valid - private Boolean imageProxyEnabled; + Optional host(); - @NotNull - @Min(0) - @Valid - private Integer queryTimeout; + @WithDefault("" + Protocol.DEFAULT_PORT) + int port(); - @NotNull - @Min(0) - @Valid - private Integer keepStatusDays; + /** + * Username is only required when using Redis ACLs + */ + Optional username(); - @NotNull - @Min(0) - @Valid - private Integer maxFeedCapacity; + Optional password(); - @NotNull - @Min(0) - @Valid - private Integer maxEntriesAgeDays = 0; + @WithDefault("" + Protocol.DEFAULT_TIMEOUT) + int timeout(); - @NotNull - @Valid - private Integer maxFeedsPerUser = 0; + @WithDefault("" + Protocol.DEFAULT_DATABASE) + int database(); - @NotNull - @Valid - private DataSize maxFeedResponseSize = DataSize.megabytes(5); + @WithDefault("500") + int maxTotal(); - @NotNull - @Min(0) - @Valid - private Integer refreshIntervalMinutes; + default JedisPool build() { + Optional host = host(); + if (host.isEmpty()) { + throw new IllegalStateException("Redis host is required"); + } - @NotNull - @Valid - private CacheType cache; + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(maxTotal()); - @Valid - private String announcement; + JedisClientConfig clientConfig = DefaultJedisClientConfig.builder() + .user(username().orElse(null)) + .password(password().orElse(null)) + .timeoutMillis(timeout()) + .database(database()) + .build(); - private String userAgent; - - private Boolean websocketEnabled = true; - - private Duration websocketPingInterval = Duration.minutes(15); - - private Duration treeReloadInterval = Duration.seconds(30); - - public Instant getUnreadThreshold() { - return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null; + return new JedisPool(poolConfig, new HostAndPort(host.get(), port()), clientConfig); } - } } diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java deleted file mode 100644 index 945aa5b7..00000000 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.commafeed; - -import java.net.InetSocketAddress; -import java.util.concurrent.TimeUnit; - -import org.hibernate.SessionFactory; - -import com.codahale.metrics.MetricFilter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.graphite.Graphite; -import com.codahale.metrics.graphite.GraphiteReporter; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; -import com.commafeed.CommaFeedConfiguration.CacheType; -import com.commafeed.backend.cache.CacheService; -import com.commafeed.backend.cache.NoopCacheService; -import com.commafeed.backend.cache.RedisCacheService; -import com.commafeed.backend.favicon.AbstractFaviconFetcher; -import com.commafeed.backend.favicon.DefaultFaviconFetcher; -import com.commafeed.backend.favicon.FacebookFaviconFetcher; -import com.commafeed.backend.favicon.YoutubeFaviconFetcher; -import com.commafeed.backend.task.DemoAccountCleanupTask; -import com.commafeed.backend.task.EntriesExceedingFeedCapacityCleanupTask; -import com.commafeed.backend.task.OldEntriesCleanupTask; -import com.commafeed.backend.task.OldStatusesCleanupTask; -import com.commafeed.backend.task.OrphanedContentsCleanupTask; -import com.commafeed.backend.task.OrphanedFeedsCleanupTask; -import com.commafeed.backend.task.ScheduledTask; -import com.commafeed.backend.urlprovider.FeedURLProvider; -import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider; -import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider; -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import com.google.inject.multibindings.Multibinder; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RequiredArgsConstructor -@Slf4j -public class CommaFeedModule extends AbstractModule { - - @Getter(onMethod = @__({ @Provides })) - private final SessionFactory sessionFactory; - - @Getter(onMethod = @__({ @Provides })) - private final CommaFeedConfiguration config; - - @Getter(onMethod = @__({ @Provides })) - private final MetricRegistry metrics; - - @Override - protected void configure() { - CacheService cacheService = config.getApplicationSettings().getCache() == CacheType.NOOP ? new NoopCacheService() - : new RedisCacheService(config.getRedisPoolFactory().build()); - log.info("using cache {}", cacheService.getClass()); - bind(CacheService.class).toInstance(cacheService); - - Multibinder faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class); - faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class); - faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class); - faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class); - - Multibinder urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class); - urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class); - urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class); - - Multibinder taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class); - taskMultibinder.addBinding().to(OldStatusesCleanupTask.class); - taskMultibinder.addBinding().to(EntriesExceedingFeedCapacityCleanupTask.class); - taskMultibinder.addBinding().to(OldEntriesCleanupTask.class); - taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class); - taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class); - taskMultibinder.addBinding().to(DemoAccountCleanupTask.class); - - ApplicationSettings settings = config.getApplicationSettings(); - - if (settings.isGraphiteEnabled()) { - final String graphitePrefix = settings.getGraphitePrefix(); - final String graphiteHost = settings.getGraphiteHost(); - final int graphitePort = settings.getGraphitePort(); - final int graphiteInterval = settings.getGraphiteInterval(); - - log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort, - graphitePrefix, graphiteInterval); - - final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort)); - final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics) - .prefixedWith(graphitePrefix) - .convertRatesTo(TimeUnit.SECONDS) - .convertDurationsTo(TimeUnit.MILLISECONDS) - .filter(MetricFilter.ALL) - .build(graphite); - reporter.start(graphiteInterval, TimeUnit.SECONDS); - } - } -} diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java new file mode 100644 index 00000000..63df69dd --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java @@ -0,0 +1,31 @@ +package com.commafeed; + +import com.codahale.metrics.MetricRegistry; +import com.commafeed.CommaFeedConfiguration.Redis; +import com.commafeed.backend.cache.CacheService; +import com.commafeed.backend.cache.NoopCacheService; +import com.commafeed.backend.cache.RedisCacheService; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; + +@Singleton +public class CommaFeedProducers { + + @Produces + @Singleton + public CacheService cacheService(CommaFeedConfiguration config) { + Redis redis = config.redis(); + if (redis.host().isEmpty()) { + return new NoopCacheService(); + } + + return new RedisCacheService(redis.build()); + } + + @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 new file mode 100644 index 00000000..63188ed5 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java @@ -0,0 +1,31 @@ +package com.commafeed; + +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; + + public CommaFeedVersion() { + Properties properties = new Properties(); + try (InputStream stream = getClass().getResourceAsStream("/git.properties")) { + if (stream != null) { + properties.load(stream); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + 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 new file mode 100644 index 00000000..68618094 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java @@ -0,0 +1,33 @@ +package com.commafeed; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.RestResponse.Status; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +import io.quarkus.security.AuthenticationFailedException; +import jakarta.annotation.Priority; +import jakarta.validation.ValidationException; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Priority(1) +public class ExceptionMappers { + + // display a message when the user fails to authenticate + @ServerExceptionMapper(AuthenticationFailedException.class) + public RestResponse authenticationFailed(AuthenticationFailedException e) { + return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationExceptionInfo(e.getMessage())); + } + + // display a message for validation errors + @ServerExceptionMapper(ValidationException.class) + public RestResponse validationException(ValidationException e) { + return RestResponse.status(Status.BAD_REQUEST, new ValidationExceptionInfo(e.getMessage())); + } + + public record AuthenticationExceptionInfo(String message) { + } + + public record ValidationExceptionInfo(String message) { + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java new file mode 100644 index 00000000..0bc56613 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java @@ -0,0 +1,27 @@ +package com.commafeed; + +import java.util.concurrent.TimeUnit; + +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; + +@Singleton +public class JacksonCustomizer implements ObjectMapperCustomizer { + @Override + public void customize(ObjectMapper objectMapper) { + objectMapper.registerModule(new JavaTimeModule()); + + // read and write instants as milliseconds instead of nanoseconds + objectMapper.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)); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java new file mode 100644 index 00000000..5f6b145b --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java @@ -0,0 +1,165 @@ +package com.commafeed; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +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, + + // rome + com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class, + com.rometools.modules.content.ContentModuleImpl.class, com.rometools.modules.mediarss.MediaModuleImpl.class, + com.rometools.modules.mediarss.MediaEntryModuleImpl.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 { +} 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 c7bc6155..2d5a59f0 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java @@ -21,21 +21,21 @@ import org.apache.hc.client5.http.protocol.RedirectLocations; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; -import org.eclipse.jetty.http.HttpStatus; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedVersion; import com.google.common.collect.Iterables; import com.google.common.io.ByteStreams; import com.google.common.net.HttpHeaders; -import io.dropwizard.util.DataSize; -import jakarta.inject.Inject; +import io.quarkus.runtime.configuration.MemorySize; import jakarta.inject.Singleton; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -51,15 +51,15 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils; public class HttpGetter { private final CloseableHttpClient client; - private final DataSize maxResponseSize; + private final MemorySize maxResponseSize; - @Inject - public HttpGetter(CommaFeedConfiguration config, MetricRegistry metrics) { - PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.getApplicationSettings().getBackgroundThreads()); - String userAgent = Optional.ofNullable(config.getApplicationSettings().getUserAgent()) - .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion())); + public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) { + PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.feedRefresh().httpThreads()); + String userAgent = config.feedRefresh() + .userAgent() + .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion())); this.client = newClient(connectionManager, userAgent); - this.maxResponseSize = config.getApplicationSettings().getMaxFeedResponseSize(); + this.maxResponseSize = config.feedRefresh().maxResponseSize(); metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax()); metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"), @@ -98,7 +98,7 @@ public class HttpGetter { context.setRequestConfig(RequestConfig.custom().setResponseTimeout(timeout, TimeUnit.MILLISECONDS).build()); HttpResponse response = client.execute(request, context, resp -> { - byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.toBytes()); + byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.asLongValue()); int code = resp.getCode(); String lastModifiedHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED)) .map(NameValuePair::getValue) @@ -120,7 +120,7 @@ public class HttpGetter { }); int code = response.getCode(); - if (code == HttpStatus.NOT_MODIFIED_304) { + if (code == HttpStatus.SC_NOT_MODIFIED) { throw new NotModifiedException("'304 - not modified' http code received"); } else if (code >= 300) { throw new HttpResponseException(code, "Server returned HTTP error code " + code); diff --git a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java deleted file mode 100644 index b74fbf19..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.commafeed.backend.cache; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Getter; -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisClientConfig; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.Protocol; - -@Getter -public class RedisPoolFactory { - - @JsonProperty - private String host = "localhost"; - - @JsonProperty - private int port = Protocol.DEFAULT_PORT; - - @JsonProperty - private String username; - - @JsonProperty - private String password; - - @JsonProperty - private int timeout = Protocol.DEFAULT_TIMEOUT; - - @JsonProperty - private int database = Protocol.DEFAULT_DATABASE; - - @JsonProperty - private int maxTotal = 500; - - public JedisPool build() { - JedisPoolConfig poolConfig = new JedisPoolConfig(); - poolConfig.setMaxTotal(maxTotal); - - JedisClientConfig clientConfig = DefaultJedisClientConfig.builder() - .user(username) - .password(password) - .timeoutMillis(timeout) - .database(database) - .build(); - - return new JedisPool(poolConfig, new HostAndPort(host, port), clientConfig); - } - -} 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 16bb496b..a84449a0 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 @@ -3,25 +3,22 @@ package com.commafeed.backend.dao; import java.util.List; import java.util.Objects; -import org.hibernate.SessionFactory; - 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.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedCategoryDAO extends GenericDAO { private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory; - @Inject - public FeedCategoryDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedCategoryDAO(EntityManager entityManager) { + super(entityManager, FeedCategory.class); } public List findAll(User user) { 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 f34a923e..cdaa690e 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 @@ -4,7 +4,6 @@ import java.time.Instant; import java.util.List; import org.apache.commons.lang3.StringUtils; -import org.hibernate.SessionFactory; import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.QFeed; @@ -12,8 +11,8 @@ import com.commafeed.backend.model.QFeedSubscription; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedDAO extends GenericDAO { @@ -21,9 +20,8 @@ public class FeedDAO extends GenericDAO { private static final QFeed FEED = QFeed.feed; private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription; - @Inject - public FeedDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedDAO(EntityManager entityManager) { + super(entityManager, Feed.class); } public List findNextUpdatable(int count, Instant lastLoginThreshold) { 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 8d65b449..0b742e64 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 @@ -2,16 +2,14 @@ package com.commafeed.backend.dao; import java.util.List; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntryContent; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLSubQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedEntryContentDAO extends GenericDAO { @@ -19,9 +17,8 @@ public class FeedEntryContentDAO extends GenericDAO { private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent; private static final QFeedEntry ENTRY = QFeedEntry.feedEntry; - @Inject - public FeedEntryContentDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedEntryContentDAO(EntityManager entityManager) { + super(entityManager, FeedEntryContent.class); } public List findExisting(String contentHash, String titleHash) { 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 2cf226ad..27d5de3f 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 @@ -3,16 +3,14 @@ package com.commafeed.backend.dao; import java.time.Instant; import java.util.List; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.QFeedEntry; import com.querydsl.core.Tuple; import com.querydsl.core.types.dsl.NumberExpression; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,9 +19,8 @@ public class FeedEntryDAO extends GenericDAO { private static final QFeedEntry ENTRY = QFeedEntry.feedEntry; - @Inject - public FeedEntryDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedEntryDAO(EntityManager entityManager) { + super(entityManager, FeedEntry.class); } public FeedEntry findExisting(String guidHash, Feed feed) { 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 358fbaa2..dee56442 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 @@ -7,7 +7,6 @@ import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; -import org.hibernate.SessionFactory; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.feed.FeedEntryKeyword; @@ -28,8 +27,8 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedEntryStatusDAO extends GenericDAO { @@ -42,9 +41,8 @@ public class FeedEntryStatusDAO extends GenericDAO { private final FeedEntryTagDAO feedEntryTagDAO; private final CommaFeedConfiguration config; - @Inject - public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) { - super(sessionFactory); + public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) { + super(entityManager, FeedEntryStatus.class); this.feedEntryTagDAO = feedEntryTagDAO; this.config = config; } @@ -60,8 +58,8 @@ public class FeedEntryStatusDAO extends GenericDAO { */ private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) { if (status == null) { - Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); - boolean read = unreadThreshold != null && entry.getPublished().isBefore(unreadThreshold); + 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); @@ -84,6 +82,7 @@ public class FeedEntryStatusDAO extends GenericDAO { boolean includeContent) { JPAQuery query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue()); if (includeContent) { + query.join(STATUS.entry).fetchJoin(); query.join(STATUS.entry.content).fetchJoin(); } @@ -105,7 +104,7 @@ public class FeedEntryStatusDAO extends GenericDAO { query.limit(limit); } - setTimeout(query, config.getApplicationSettings().getQueryTimeout()); + setTimeout(query, config.database().queryTimeout()); List statuses = query.fetch(); statuses.forEach(s -> s.setMarkable(true)); @@ -179,7 +178,7 @@ public class FeedEntryStatusDAO extends GenericDAO { query.limit(limit); } - setTimeout(query, config.getApplicationSettings().getQueryTimeout()); + setTimeout(query, config.database().queryTimeout()); List statuses = new ArrayList<>(); List tuples = query.fetch(); @@ -217,9 +216,9 @@ public class FeedEntryStatusDAO extends GenericDAO { or.or(STATUS.read.isNull()); or.or(STATUS.read.isFalse()); - Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); - if (unreadThreshold != null) { - return or.and(ENTRY.published.goe(unreadThreshold)); + Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold(); + if (statusesInstantThreshold != null) { + return or.and(ENTRY.published.goe(statusesInstantThreshold)); } else { return or; } 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 5d87d729..bb00ad12 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 @@ -4,24 +4,21 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.hibernate.SessionFactory; - 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.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedEntryTagDAO extends GenericDAO { private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag; - @Inject - public FeedEntryTagDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public FeedEntryTagDAO(EntityManager entityManager) { + super(entityManager, FeedEntryTag.class); } public List findByUser(User user) { 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 05b1cde7..990ac21c 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 @@ -5,12 +5,11 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import org.hibernate.SessionFactory; -import org.hibernate.engine.spi.SessionFactoryImplementor; +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.event.spi.PostInsertEventListener; import org.hibernate.persister.entity.EntityPersister; import com.commafeed.backend.model.AbstractModel; @@ -23,28 +22,28 @@ import com.commafeed.backend.model.User; import com.google.common.collect.Iterables; import com.querydsl.jpa.JPQLQuery; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class FeedSubscriptionDAO extends GenericDAO { private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription; - private final SessionFactory sessionFactory; + private final EntityManager entityManager; - @Inject - public FeedSubscriptionDAO(SessionFactory sessionFactory) { - super(sessionFactory); - this.sessionFactory = sessionFactory; + public FeedSubscriptionDAO(EntityManager entityManager) { + super(entityManager, FeedSubscription.class); + this.entityManager = entityManager; } public void onPostCommitInsert(Consumer consumer) { - sessionFactory.unwrap(SessionFactoryImplementor.class) + entityManager.unwrap(SharedSessionContractImplementor.class) + .getFactory() .getServiceRegistry() .getService(EventListenerRegistry.class) .getEventListenerGroup(EventType.POST_COMMIT_INSERT) - .appendListener(new PostInsertEventListener() { + .appendListener(new PostCommitInsertEventListener() { @Override public void onPostInsert(PostInsertEvent event) { if (event.getEntity() instanceof FeedSubscription s) { @@ -56,6 +55,11 @@ public class FeedSubscriptionDAO extends GenericDAO { public boolean requiresPostCommitHandling(EntityPersister persister) { return true; } + + @Override + public void onPostInsertCommitFailed(PostInsertEvent event) { + // do nothing + } }); } 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 b2a75bfb..8041295b 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 @@ -2,7 +2,7 @@ package com.commafeed.backend.dao; import java.util.Collection; -import org.hibernate.SessionFactory; +import org.hibernate.Session; import org.hibernate.jpa.SpecHints; import com.commafeed.backend.model.AbstractModel; @@ -12,45 +12,43 @@ import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAUpdateClause; -import io.dropwizard.hibernate.AbstractDAO; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; -public abstract class GenericDAO extends AbstractDAO { +@RequiredArgsConstructor +public abstract class GenericDAO { - protected GenericDAO(SessionFactory sessionFactory) { - super(sessionFactory); - } + private final EntityManager entityManager; + private final Class entityClass; protected JPAQueryFactory query() { - return new JPAQueryFactory(currentSession()); + return new JPAQueryFactory(entityManager); } protected JPAUpdateClause updateQuery(EntityPath entityPath) { - return new JPAUpdateClause(currentSession(), entityPath); + return new JPAUpdateClause(entityManager, entityPath); } protected JPADeleteClause deleteQuery(EntityPath entityPath) { - return new JPADeleteClause(currentSession(), entityPath); + return new JPADeleteClause(entityManager, entityPath); } + @SuppressWarnings("deprecation") public void saveOrUpdate(T model) { - persist(model); + entityManager.unwrap(Session.class).saveOrUpdate(model); } public void saveOrUpdate(Collection models) { - models.forEach(this::persist); - } - - public void update(T model) { - currentSession().merge(model); + models.forEach(this::saveOrUpdate); } public T findById(Long id) { - return get(id); + return entityManager.find(entityClass, id); } public void delete(T object) { if (object != null) { - currentSession().remove(object); + entityManager.remove(object); } } 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 87be4676..5caa1ed2 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,68 +1,20 @@ package com.commafeed.backend.dao; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.Transaction; -import org.hibernate.context.internal.ManagedSessionContext; - -import jakarta.inject.Inject; +import io.quarkus.narayana.jta.QuarkusTransaction; import jakarta.inject.Singleton; -import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton public class UnitOfWork { - private final SessionFactory sessionFactory; - - public void run(SessionRunner sessionRunner) { + public void run(SessionRunner runner) { call(() -> { - sessionRunner.runInSession(); + runner.runInSession(); return null; }); } - public T call(SessionRunnerReturningValue sessionRunner) { - T t = null; - - boolean sessionAlreadyBound = ManagedSessionContext.hasBind(sessionFactory); - try (Session session = sessionFactory.openSession()) { - if (!sessionAlreadyBound) { - ManagedSessionContext.bind(session); - } - - Transaction tx = session.beginTransaction(); - try { - t = sessionRunner.runInSession(); - commitTransaction(tx); - } catch (Exception e) { - rollbackTransaction(tx); - UnitOfWork.rethrow(e); - } - } finally { - if (!sessionAlreadyBound) { - ManagedSessionContext.unbind(sessionFactory); - } - } - - return t; - } - - private static void rollbackTransaction(Transaction tx) { - if (tx != null && tx.isActive()) { - tx.rollback(); - } - } - - private static void commitTransaction(Transaction tx) { - if (tx != null && tx.isActive()) { - tx.commit(); - } - } - - @SuppressWarnings("unchecked") - private static void rethrow(Exception e) throws E { - throw (E) e; + public T call(SessionRunnerReturningValue runner) { + return QuarkusTransaction.joiningExisting().call(runner::runInSession); } @FunctionalInterface 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 ae7d87f4..ea7e475c 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,21 +1,18 @@ package com.commafeed.backend.dao; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class UserDAO extends GenericDAO { private static final QUser USER = QUser.user; - @Inject - public UserDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public UserDAO(EntityManager entityManager) { + super(entityManager, User.class); } public User findByName(String name) { 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 37c23e92..81d06301 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 @@ -4,24 +4,21 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.hibernate.SessionFactory; - 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.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class UserRoleDAO extends GenericDAO { private static final QUserRole ROLE = QUserRole.userRole; - @Inject - public UserRoleDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public UserRoleDAO(EntityManager entityManager) { + super(entityManager, UserRole.class); } public List findAll() { 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 a95450ad..89048624 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,19 @@ package com.commafeed.backend.dao; -import org.hibernate.SessionFactory; - import com.commafeed.backend.model.QUserSettings; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings; -import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; @Singleton public class UserSettingsDAO extends GenericDAO { private static final QUserSettings SETTINGS = QUserSettings.userSettings; - @Inject - public UserSettingsDAO(SessionFactory sessionFactory) { - super(sessionFactory); + public UserSettingsDAO(EntityManager entityManager) { + super(entityManager, UserSettings.class); } public UserSettings findByUser(User user) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java index 7b814fef..32a0ee11 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java @@ -10,7 +10,7 @@ import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; +import jakarta.annotation.Priority; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,8 +20,9 @@ import lombok.extern.slf4j.Slf4j; * */ @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton +@Priority(Integer.MIN_VALUE) public class DefaultFaviconFetcher extends AbstractFaviconFetcher { private final HttpGetter getter; 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 fd9722f0..fb0f0fb9 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 @@ -11,13 +11,12 @@ import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FacebookFaviconFetcher extends AbstractFaviconFetcher { 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 def1dda5..ea219e3e 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 @@ -22,13 +22,12 @@ import com.google.api.services.youtube.model.ChannelListResponse; import com.google.api.services.youtube.model.PlaylistListResponse; import com.google.api.services.youtube.model.Thumbnail; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { @@ -43,8 +42,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { return null; } - String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey(); - if (googleAuthKey == null) { + Optional googleAuthKey = config.googleAuthKey(); + if (googleAuthKey.isEmpty()) { log.debug("no google auth key configured"); return null; } @@ -63,13 +62,13 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { ChannelListResponse response = null; if (userId.isPresent()) { log.debug("contacting youtube api for user {}", userId.get().getValue()); - response = fetchForUser(youtube, googleAuthKey, userId.get().getValue()); + response = fetchForUser(youtube, googleAuthKey.get(), userId.get().getValue()); } else if (channelId.isPresent()) { log.debug("contacting youtube api for channel {}", channelId.get().getValue()); - response = fetchForChannel(youtube, googleAuthKey, channelId.get().getValue()); + response = fetchForChannel(youtube, googleAuthKey.get(), channelId.get().getValue()); } else if (playlistId.isPresent()) { log.debug("contacting youtube api for playlist {}", playlistId.get().getValue()); - response = fetchForPlaylist(youtube, googleAuthKey, playlistId.get().getValue()); + response = fetchForPlaylist(youtube, googleAuthKey.get(), playlistId.get().getValue()); } if (response == null || response.isEmpty() || CollectionUtils.isEmpty(response.getItems())) { 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 18f9f2a0..03be80a9 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 @@ -2,7 +2,7 @@ package com.commafeed.backend.feed; import java.io.IOException; import java.time.Instant; -import java.util.Set; +import java.util.List; import org.apache.commons.codec.binary.StringUtils; @@ -15,22 +15,26 @@ import com.commafeed.backend.feed.parser.FeedParserResult; import com.commafeed.backend.urlprovider.FeedURLProvider; import com.rometools.rome.io.FeedException; -import jakarta.inject.Inject; +import io.quarkus.arc.All; import jakarta.inject.Singleton; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * Fetches a feed then parses it */ @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Singleton public class FeedFetcher { private final FeedParser parser; private final HttpGetter getter; - private final Set urlProviders; + private final List 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 FeedException, IOException, NotModifiedException { @@ -87,7 +91,7 @@ public class FeedFetcher { result.getDuration()); } - private static String extractFeedUrl(Set urlProviders, String url, String urlContent) { + private static String extractFeedUrl(List urlProviders, String url, String urlContent) { for (FeedURLProvider urlProvider : urlProviders) { String feedUrl = urlProvider.get(url, urlContent); if (feedUrl != null) { 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 9f39cbf0..bd1ece18 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,6 +1,5 @@ package com.commafeed.backend.feed; -import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.concurrent.BlockingDeque; @@ -21,14 +20,12 @@ import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.Feed; -import io.dropwizard.lifecycle.Managed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton -public class FeedRefreshEngine implements Managed { +public class FeedRefreshEngine { private final UnitOfWork unitOfWork; private final FeedDAO feedDAO; @@ -45,7 +42,6 @@ public class FeedRefreshEngine implements Managed { private final ThreadPoolExecutor workerExecutor; private final ThreadPoolExecutor databaseUpdaterExecutor; - @Inject public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, CommaFeedConfiguration config, MetricRegistry metrics) { this.unitOfWork = unitOfWork; @@ -60,15 +56,14 @@ public class FeedRefreshEngine implements Managed { this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor(); this.refillLoopExecutor = Executors.newSingleThreadExecutor(); this.refillExecutor = newDiscardingSingleThreadExecutorService(); - this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads()); - this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads()); + this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads()); + this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads()); 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); } - @Override public void start() { startFeedProcessingLoop(); startRefillLoop(); @@ -165,22 +160,20 @@ public class FeedRefreshEngine implements Managed { private List getNextUpdatableFeeds(int max) { return unitOfWork.call(() -> { - Instant lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) - ? Instant.now().minus(Duration.ofDays(30)) - : null; + Instant lastLoginThreshold = config.feedRefresh().userInactivityPeriod().isZero() ? null + : Instant.now().minus(config.feedRefresh().userInactivityPeriod()); List feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold); // update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable() - Instant nextUpdateDate = Instant.now().plus(Duration.ofMinutes(config.getApplicationSettings().getRefreshIntervalMinutes())); + 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.getApplicationSettings().getBackgroundThreads()); + return Math.min(100, 3 * config.feedRefresh().httpThreads()); } - @Override public void stop() { this.feedProcessingLoopExecutor.shutdownNow(); this.refillLoopExecutor.shutdownNow(); 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 d4097332..8aea399e 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 @@ -6,24 +6,22 @@ import java.time.temporal.ChronoUnit; import com.commafeed.CommaFeedConfiguration; -import jakarta.inject.Inject; import jakarta.inject.Singleton; @Singleton public class FeedRefreshIntervalCalculator { - private final boolean heavyLoad; - private final int refreshIntervalMinutes; + private final Duration refreshInterval; + private final boolean empiricalInterval; - @Inject public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) { - this.heavyLoad = config.getApplicationSettings().getHeavyLoad(); - this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes(); + this.refreshInterval = config.feedRefresh().interval(); + this.empiricalInterval = config.feedRefresh().intervalEmpirical(); } public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) { Instant defaultRefreshInterval = getDefaultRefreshInterval(); - return heavyLoad ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval) + return empiricalInterval ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval) : defaultRefreshInterval; } @@ -33,7 +31,7 @@ public class FeedRefreshIntervalCalculator { public Instant onFetchError(int errorCount) { int retriesBeforeDisable = 3; - if (errorCount < retriesBeforeDisable || !heavyLoad) { + if (errorCount < retriesBeforeDisable || !empiricalInterval) { return getDefaultRefreshInterval(); } @@ -42,7 +40,7 @@ public class FeedRefreshIntervalCalculator { } private Instant getDefaultRefreshInterval() { - return Instant.now().plus(Duration.ofMinutes(refreshIntervalMinutes)); + return Instant.now().plus(refreshInterval); } private Instant computeRefreshIntervalForHeavyLoad(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) { 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 ea891b03..b2237929 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 @@ -32,7 +32,6 @@ import com.commafeed.frontend.ws.WebSocketMessageBuilder; import com.commafeed.frontend.ws.WebSocketSessions; import com.google.common.util.concurrent.Striped; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -58,7 +57,6 @@ public class FeedRefreshUpdater { private final Meter feedUpdated; private final Meter entryInserted; - @Inject public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO, CacheService cache, WebSocketSessions webSocketSessions) { this.unitOfWork = unitOfWork; 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 fa855f2a..11421313 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 @@ -16,7 +16,6 @@ import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult; import com.commafeed.backend.feed.parser.FeedParserResult.Entry; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -32,7 +31,6 @@ public class FeedRefreshWorker { private final CommaFeedConfiguration config; private final Meter feedFetched; - @Inject public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config, MetricRegistry metrics) { this.refreshIntervalCalculator = refreshIntervalCalculator; @@ -51,14 +49,14 @@ public class FeedRefreshWorker { List entries = result.feed().entries(); - Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity(); + int maxFeedCapacity = config.database().cleanup().maxFeedCapacity(); if (maxFeedCapacity > 0) { entries = entries.stream().limit(maxFeedCapacity).toList(); } - Integer maxEntriesAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays(); - if (maxEntriesAgeDays > 0) { - Instant threshold = Instant.now().minus(Duration.ofDays(maxEntriesAgeDays)); + Duration maxEntriesAgeDays = config.database().cleanup().entriesMaxAge(); + if (!maxEntriesAgeDays.isZero()) { + Instant threshold = Instant.now().minus(maxEntriesAgeDays); entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList(); } 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 2f62c530..d25f7c13 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 @@ -38,14 +38,13 @@ import com.rometools.rome.feed.synd.SyndLinkImpl; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedInput; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; /** * Parses raw xml into a FeedParserResult object */ -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedParser { 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 5dc3329d..f451ac93 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 @@ -15,11 +15,10 @@ import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Outline; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OPMLExporter { @@ -28,7 +27,7 @@ public class OPMLExporter { public Opml export(User user) { Opml opml = new Opml(); - opml.setFeedType("opml_1.1"); + opml.setFeedType("opml_1.0"); opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName())); opml.setCreated(new Date()); 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 a4a00480..f28c5247 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 @@ -17,13 +17,12 @@ import com.rometools.opml.feed.opml.Outline; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.WireFeedInput; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OPMLImporter { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java index 4bfc44af..480a1ba7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java @@ -4,10 +4,13 @@ import org.jdom2.Element; import com.rometools.opml.feed.opml.Opml; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Add missing title to the generated OPML * */ +@RegisterForReflection public class OPML11Generator extends com.rometools.opml.io.impl.OPML10Generator { public OPML11Generator() { 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 3342a5c9..8770667a 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 @@ -9,10 +9,13 @@ 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; + /** * Support for OPML 1.1 parsing * */ +@RegisterForReflection public class OPML11Parser extends OPML10Parser { public OPML11Parser() { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java index c2154565..404364b2 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java @@ -6,10 +6,13 @@ import com.rometools.rome.feed.synd.SyndContentImpl; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.impl.ConverterForRSS090; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Support description tag for RSS09 * */ +@RegisterForReflection public class RSS090DescriptionConverter extends ConverterForRSS090 { @Override diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java index d30f82dc..abde98ed 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java @@ -8,10 +8,13 @@ import com.rometools.rome.feed.rss.Description; import com.rometools.rome.feed.rss.Item; import com.rometools.rome.io.impl.RSS090Parser; +import io.quarkus.runtime.annotations.RegisterForReflection; + /** * Support description tag for RSS09 * */ +@RegisterForReflection public class RSS090DescriptionParser extends RSS090Parser { @Override diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java index 4c101230..6c7e11c7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java @@ -10,6 +10,9 @@ import org.jdom2.Namespace; import com.google.common.collect.Lists; import com.rometools.rome.io.impl.RSS10Parser; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public class RSSRDF10Parser extends RSS10Parser { private static final String RSS_URI = "http://purl.org/rss/1.0/"; 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 5886f20a..f3e90352 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 @@ -21,12 +21,11 @@ import org.w3c.dom.css.CSSStyleDeclaration; import com.steadystate.css.parser.CSSOMParser; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Slf4j @Singleton public class FeedEntryContentCleaningService { 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 1ad16ab5..879f42f3 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 @@ -12,11 +12,10 @@ 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.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryContentService { 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 2ca965a2..6832c58a 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 @@ -25,11 +25,10 @@ import org.jsoup.Jsoup; import com.commafeed.backend.model.FeedEntry; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryFilteringService { 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 d75e1915..7b789042 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 @@ -18,13 +18,12 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryService { 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 70f451c9..540956ab 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 @@ -10,11 +10,10 @@ import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class FeedEntryTagService { 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 69a5ecf2..49cfd653 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 @@ -2,8 +2,8 @@ package com.commafeed.backend.service; import java.io.IOException; import java.time.Instant; +import java.util.List; import java.util.Objects; -import java.util.Set; import com.commafeed.backend.Digests; import com.commafeed.backend.dao.FeedDAO; @@ -14,19 +14,18 @@ import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Models; import com.google.common.io.Resources; -import jakarta.inject.Inject; +import io.quarkus.arc.All; import jakarta.inject.Singleton; @Singleton public class FeedService { private final FeedDAO feedDAO; - private final Set faviconFetchers; + private final List faviconFetchers; private final Favicon defaultFavicon; - @Inject - public FeedService(FeedDAO feedDAO, Set faviconFetchers) { + public FeedService(FeedDAO feedDAO, @All List faviconFetchers) { this.feedDAO = feedDAO; this.faviconFetchers = faviconFetchers; 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 444b9f34..5f48a7c1 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 @@ -21,7 +21,6 @@ import com.commafeed.backend.model.Models; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UnreadCount; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -37,7 +36,6 @@ public class FeedSubscriptionService { private final CacheService cache; private final CommaFeedConfiguration config; - @Inject public FeedSubscriptionService(FeedDAO feedDAO, FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO, FeedService feedService, FeedRefreshEngine feedRefreshEngine, CacheService cache, CommaFeedConfiguration config) { this.feedDAO = feedDAO; @@ -63,7 +61,7 @@ public class FeedSubscriptionService { public long subscribe(User user, String url, String title, FeedCategory category, int position) { - final String pubUrl = config.getApplicationSettings().getPublicUrl(); + final String pubUrl = config.publicUrl(); if (StringUtils.isBlank(pubUrl)) { throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set"); } @@ -71,7 +69,7 @@ public class FeedSubscriptionService { throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance"); } - Integer maxFeedsPerUser = config.getApplicationSettings().getMaxFeedsPerUser(); + 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); 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 e50556f0..758945aa 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 @@ -4,10 +4,9 @@ import java.util.Optional; import java.util.Properties; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; +import com.commafeed.CommaFeedConfiguration.Smtp; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.mail.Authenticator; import jakarta.mail.Message; @@ -22,27 +21,29 @@ import lombok.RequiredArgsConstructor; * Mailing service * */ -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class MailService { private final CommaFeedConfiguration config; public void sendMail(User user, String subject, String content) throws Exception { + Optional settings = config.smtp(); + if (settings.isEmpty()) { + throw new IllegalArgumentException("SMTP settings not configured"); + } - ApplicationSettings settings = config.getApplicationSettings(); - - final String username = settings.getSmtpUserName(); - final String password = settings.getSmtpPassword(); - final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName()); + final String username = settings.get().userName(); + final String password = settings.get().password(); + final String fromAddress = Optional.ofNullable(settings.get().fromAddress()).orElse(settings.get().userName()); String dest = user.getEmail(); Properties props = new Properties(); props.put("mail.smtp.auth", "true"); - props.put("mail.smtp.starttls.enable", String.valueOf(settings.isSmtpTls())); - props.put("mail.smtp.host", settings.getSmtpHost()); - props.put("mail.smtp.port", String.valueOf(settings.getSmtpPort())); + props.put("mail.smtp.starttls.enable", String.valueOf(settings.get().tls())); + props.put("mail.smtp.host", settings.get().host()); + props.put("mail.smtp.port", String.valueOf(settings.get().port())); Session session = Session.getInstance(props, new Authenticator() { @Override 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 26aa70b9..a6f7d5c6 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 @@ -12,7 +12,6 @@ import javax.crypto.spec.PBEKeySpec; import org.apache.commons.lang3.StringUtils; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; // taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html @SuppressWarnings("serial") @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class PasswordEncryptionService implements Serializable { 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 76a0df1c..b56d4c47 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 @@ -24,11 +24,10 @@ import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.internal.PostLoginActivities; import com.google.common.base.Preconditions; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class UserService { @@ -117,8 +116,7 @@ public class UserService { public User register(String name, String password, String email, Collection roles, boolean forceRegistration) { if (!forceRegistration) { - Preconditions.checkState(config.getApplicationSettings().getAllowRegistrations(), - "Registrations are closed on this CommaFeed instance"); + Preconditions.checkState(config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance"); } Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken"); 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 a8f54d4e..d64bb25b 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 @@ -14,7 +14,6 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.model.Feed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -35,7 +34,6 @@ public class DatabaseCleaningService { private final FeedEntryStatusDAO feedEntryStatusDAO; private final Meter entriesDeletedMeter; - @Inject public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO, FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) { this.unitOfWork = unitOfWork; @@ -43,7 +41,7 @@ public class DatabaseCleaningService { this.feedEntryDAO = feedEntryDAO; this.feedEntryContentDAO = feedEntryContentDAO; this.feedEntryStatusDAO = feedEntryStatusDAO; - this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize(); + this.batchSize = config.database().cleanup().batchSize(); this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted")); } 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 865118e6..a5bbe584 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,109 +1,41 @@ package com.commafeed.backend.service.db; -import java.util.HashMap; -import java.util.Map; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; +import org.kohsuke.MetaInfServices; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.service.UserService; -import io.dropwizard.lifecycle.Managed; -import jakarta.inject.Inject; import jakarta.inject.Singleton; -import liquibase.Scope; -import liquibase.UpdateSummaryEnum; -import liquibase.changelog.ChangeLogParameters; -import liquibase.command.CommandScope; -import liquibase.command.core.UpdateCommandStep; -import liquibase.command.core.helpers.DatabaseChangelogCommandStep; -import liquibase.command.core.helpers.DbUrlConnectionArgumentsCommandStep; -import liquibase.command.core.helpers.ShowSummaryArgument; import liquibase.database.Database; -import liquibase.database.DatabaseFactory; import liquibase.database.core.PostgresDatabase; -import liquibase.database.jvm.JdbcConnection; -import liquibase.exception.DatabaseException; -import liquibase.resource.ClassLoaderResourceAccessor; import liquibase.structure.DatabaseObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton -public class DatabaseStartupService implements Managed { +public class DatabaseStartupService { private final UnitOfWork unitOfWork; - private final SessionFactory sessionFactory; private final UserDAO userDAO; private final UserService userService; private final CommaFeedConfiguration config; - @Override - public void start() { - updateSchema(); + public void populateInitialData() { long count = unitOfWork.call(userDAO::count); if (count == 0) { unitOfWork.run(this::initialData); } } - private void updateSchema() { - log.info("checking if database schema needs updating"); - - try (Session session = sessionFactory.openSession()) { - session.doWork(connection -> { - try { - JdbcConnection jdbcConnection = new JdbcConnection(connection); - Database database = getDatabase(jdbcConnection); - - Map scopeObjects = new HashMap<>(); - scopeObjects.put(Scope.Attr.database.name(), database); - scopeObjects.put(Scope.Attr.resourceAccessor.name(), - new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader())); - - Scope.child(scopeObjects, () -> { - CommandScope command = new CommandScope(UpdateCommandStep.COMMAND_NAME); - command.addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, database); - command.addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, "migrations.xml"); - command.addArgumentValue(DatabaseChangelogCommandStep.CHANGELOG_PARAMETERS, new ChangeLogParameters(database)); - command.addArgumentValue(ShowSummaryArgument.SHOW_SUMMARY, UpdateSummaryEnum.OFF); - command.execute(); - }); - - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - log.info("database schema is up to date"); - } - - private Database getDatabase(JdbcConnection connection) throws DatabaseException { - Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection); - if (database instanceof PostgresDatabase) { - database = new PostgresDatabase() { - @Override - public String escapeObjectName(String objectName, Class objectType) { - return objectName; - } - }; - database.setConnection(connection); - } - - return database; - } - private void initialData() { log.info("populating database with default values"); try { userService.createAdminUser(); - if (config.getApplicationSettings().getCreateDemoAccount()) { + if (config.users().createDemoAccount()) { userService.createDemoUser(); } } catch (Exception e) { @@ -111,4 +43,20 @@ public class DatabaseStartupService implements Managed { } } + /** + * 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/db/H2MigrationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java deleted file mode 100644 index 75b839c4..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.commafeed.backend.service.db; - -import java.io.BufferedReader; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Stream; - -import org.apache.commons.lang3.StringUtils; - -import com.manticore.h2.H2MigrationTool; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RequiredArgsConstructor -@Slf4j -public class H2MigrationService { - - private static final String H2_FILE_SUFFIX = ".mv.db"; - - public void migrateIfNeeded(Path path, String user, String password) { - if (Files.notExists(path)) { - return; - } - - int format; - try { - format = getH2FileFormat(path); - } catch (IOException e) { - throw new RuntimeException("could not detect H2 format", e); - } - - if (format == 2) { - try { - migrate(path, user, password, "2.1.214", "2.2.224"); - } catch (Exception e) { - throw new RuntimeException("could not migrate H2 to format 3", e); - } - } - } - - public int getH2FileFormat(Path path) throws IOException { - try (BufferedReader reader = Files.newBufferedReader(path)) { - String headers = reader.readLine(); - - return Stream.of(headers.split(",")) - .filter(h -> h.startsWith("format:")) - .map(h -> h.split(":")[1]) - .map(Integer::parseInt) - .findFirst() - .orElseThrow(() -> new RuntimeException("could not find format in H2 file headers")); - } - } - - private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception { - log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion); - - Path scriptPath = path.resolveSibling("script-%d.sql".formatted(System.currentTimeMillis())); - Path newVersionPath = path.resolveSibling("%s.%s%s".formatted(StringUtils.removeEnd(path.getFileName().toString(), H2_FILE_SUFFIX), - getPatchVersion(toVersion), H2_FILE_SUFFIX)); - Path oldVersionBackupPath = path.resolveSibling("%s.%s.backup".formatted(path.getFileName(), getPatchVersion(fromVersion))); - - Files.deleteIfExists(scriptPath); - Files.deleteIfExists(newVersionPath); - Files.deleteIfExists(oldVersionBackupPath); - - H2MigrationTool.readDriverRecords(); - new H2MigrationTool().migrate(fromVersion, toVersion, path.toAbsolutePath().toString(), user, password, - scriptPath.toAbsolutePath().toString(), "", "", false, false, ""); - if (!Files.exists(newVersionPath)) { - throw new RuntimeException("H2 migration failed, new version file not found"); - } - - Files.move(path, oldVersionBackupPath); - Files.move(newVersionPath, path); - Files.delete(oldVersionBackupPath); - Files.delete(scriptPath); - - log.info("migrated H2 database from format {} to format {}", fromVersion, toVersion); - } - - private String getPatchVersion(String version) { - return StringUtils.substringAfterLast(version, "."); - } - -} 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 752ef81b..bd66f02b 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 @@ -7,11 +7,10 @@ import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class PostLoginActivities { 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 ccd2a5d2..faa2757a 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 @@ -9,12 +9,11 @@ import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.UserService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Slf4j public class DemoAccountCleanupTask extends ScheduledTask { @@ -26,7 +25,7 @@ public class DemoAccountCleanupTask extends ScheduledTask { @Override protected void run() { - if (!config.getApplicationSettings().getCreateDemoAccount()) { + if (!config.users().createDemoAccount()) { return; } 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 5508cbd7..09351381 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 @@ -5,11 +5,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask { @@ -18,7 +17,7 @@ public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask { @Override public void run() { - int maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity(); + int maxFeedCapacity = config.database().cleanup().maxFeedCapacity(); if (maxFeedCapacity > 0) { cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity); } 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 fad42a75..4bc0cd32 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 @@ -7,11 +7,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OldEntriesCleanupTask extends ScheduledTask { @@ -20,9 +19,9 @@ public class OldEntriesCleanupTask extends ScheduledTask { @Override public void run() { - int maxAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays(); - if (maxAgeDays > 0) { - Instant threshold = Instant.now().minus(Duration.ofDays(maxAgeDays)); + Duration entriesMaxAge = config.database().cleanup().entriesMaxAge(); + if (!entriesMaxAge.isZero()) { + Instant threshold = Instant.now().minus(entriesMaxAge); cleaner.cleanEntriesOlderThan(threshold); } } 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 0c19afe3..62e87c90 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 @@ -6,11 +6,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OldStatusesCleanupTask extends ScheduledTask { @@ -19,7 +18,7 @@ public class OldStatusesCleanupTask extends ScheduledTask { @Override public void run() { - Instant threshold = config.getApplicationSettings().getUnreadThreshold(); + Instant threshold = config.database().cleanup().statusesInstantThreshold(); if (threshold != null) { cleaner.cleanStatusesOlderThan(threshold); } 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 7b2afcaa..786b7708 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 @@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OrphanedContentsCleanupTask extends ScheduledTask { 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 4606dad3..81eb14ea 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 @@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit; import com.commafeed.backend.service.db.DatabaseCleaningService; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton public class OrphanedFeedsCleanupTask extends ScheduledTask { 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 new file mode 100644 index 00000000..73a37bf0 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java @@ -0,0 +1,28 @@ +package com.commafeed.backend.task; + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import io.quarkus.arc.All; +import jakarta.inject.Singleton; + +@Singleton +public class TaskScheduler { + + private final List tasks; + private final ScheduledExecutorService executor; + + public TaskScheduler(@All List tasks) { + this.tasks = tasks; + this.executor = Executors.newScheduledThreadPool(tasks.size()); + } + + public void start() { + tasks.forEach(task -> task.register(executor)); + } + + public void stop() { + executor.shutdownNow(); + } +} 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 36913423..b2bf773c 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 @@ -4,6 +4,9 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; +import jakarta.inject.Singleton; + +@Singleton public class InPageReferenceFeedURLProvider implements FeedURLProvider { @Override 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 ab94a1c0..882da01f 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 @@ -3,12 +3,15 @@ package com.commafeed.backend.urlprovider; import java.util.regex.Matcher; import java.util.regex.Pattern; +import jakarta.inject.Singleton; + /** * Workaround for Youtube channels * * 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 Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE); diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java deleted file mode 100644 index 0e685686..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import com.commafeed.backend.model.UserRole.Role; - -@Inherited -@Target({ ElementType.PARAMETER }) -@Retention(RetentionPolicy.RUNTIME) -public @interface SecurityCheck { - - /** - * Roles needed. - */ - Role value() default Role.USER; - - boolean apiKeyAllowed() default false; -} \ No newline at end of file diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java deleted file mode 100644 index 2e14bd7a..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; - -import org.glassfish.jersey.server.ContainerRequest; - -import com.commafeed.CommaFeedConfiguration; -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.frontend.session.SessionHelper; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class SecurityCheckFactory implements Function { - - private static final String PREFIX = "Basic"; - - private final UserDAO userDAO; - private final UserService userService; - private final CommaFeedConfiguration config; - private final HttpServletRequest request; - private final Role role; - private final boolean apiKeyAllowed; - - @Override - public User apply(ContainerRequest req) { - Optional user = apiKeyLogin(); - if (user.isEmpty()) { - user = basicAuthenticationLogin(); - } - if (user.isEmpty()) { - user = cookieSessionLogin(new SessionHelper(request)); - } - - if (user.isPresent()) { - Set roles = userService.getRoles(user.get()); - if (roles.contains(role)) { - return user.get(); - } else { - throw buildWebApplicationException(Response.Status.FORBIDDEN, "You don't have the required role to access this resource."); - } - } else { - throw buildWebApplicationException(Response.Status.UNAUTHORIZED, "Credentials are required to access this resource."); - } - } - - Optional cookieSessionLogin(SessionHelper sessionHelper) { - Optional loggedInUser = sessionHelper.getLoggedInUserId().map(userDAO::findById); - loggedInUser.ifPresent(userService::performPostLoginActivities); - return loggedInUser; - } - - private Optional basicAuthenticationLogin() { - String header = request.getHeader(HttpHeaders.AUTHORIZATION); - if (header != null) { - int space = header.indexOf(' '); - if (space > 0) { - String method = header.substring(0, space); - if (PREFIX.equalsIgnoreCase(method)) { - byte[] decodedBytes = Base64.getDecoder().decode(header.substring(space + 1)); - String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1); - int i = decoded.indexOf(':'); - if (i > 0) { - String username = decoded.substring(0, i); - String password = decoded.substring(i + 1); - return userService.login(username, password); - } - } - } - } - return Optional.empty(); - } - - private Optional apiKeyLogin() { - String apiKey = request.getParameter("apiKey"); - if (apiKey != null && apiKeyAllowed) { - return userService.login(apiKey); - } - return Optional.empty(); - } - - private WebApplicationException buildWebApplicationException(Response.Status status, String message) { - Map response = new HashMap<>(); - response.put("message", message); - response.put("allowRegistrations", config.getApplicationSettings().getAllowRegistrations()); - return new WebApplicationException(Response.status(status).entity(response).type(MediaType.APPLICATION_JSON).build()); - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java deleted file mode 100644 index 4e70d479..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.util.function.Function; - -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.server.ContainerRequest; -import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider; -import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.spi.internal.ValueParamProvider; - -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.service.UserService; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; - -@Singleton -public class SecurityCheckFactoryProvider extends AbstractValueParamProvider { - - private final UserService userService; - private final UserDAO userDAO; - private final CommaFeedConfiguration config; - private final HttpServletRequest request; - - @Inject - public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserDAO userDAO, - UserService userService, CommaFeedConfiguration config, HttpServletRequest request) { - super(() -> extractorProvider, Parameter.Source.UNKNOWN); - this.userDAO = userDAO; - this.userService = userService; - this.config = config; - this.request = request; - } - - @Override - protected Function createValueProvider(Parameter parameter) { - final Class classType = parameter.getRawType(); - - SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class); - if (securityCheck == null) { - return null; - } - - if (!classType.isAssignableFrom(User.class)) { - return null; - } - - return new SecurityCheckFactory(userDAO, userService, config, request, securityCheck.value(), securityCheck.apiKeyAllowed()); - } - - @RequiredArgsConstructor - public static class Binder extends AbstractBinder { - - private final UserDAO userDAO; - private final UserService userService; - private final CommaFeedConfiguration config; - - @Override - protected void configure() { - bind(SecurityCheckFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class); - bind(userDAO).to(UserDAO.class); - bind(userService).to(UserService.class); - bind(config).to(CommaFeedConfiguration.class); - } - } - -} 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 c360d7cd..631e56a7 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 @@ -4,6 +4,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -11,6 +12,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Entry details") @Data +@RegisterForReflection public class Category implements Serializable { @Schema(description = "category id", requiredMode = RequiredMode.REQUIRED) 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 9e7d6483..9d51ee62 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 @@ -4,6 +4,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -11,6 +12,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "List of entries with some metadata") @Data +@RegisterForReflection public class Entries implements Serializable { @Schema(description = "name of the feed or the category requested", requiredMode = RequiredMode.REQUIRED) 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 87624d77..b7add91f 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 @@ -19,6 +19,7 @@ import com.rometools.rome.feed.synd.SyndEnclosureImpl; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndEntryImpl; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -26,6 +27,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Entry details") @Data +@RegisterForReflection public class Entry implements Serializable { @Schema(description = "entry id", requiredMode = RequiredMode.REQUIRED) 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 ffc4cca3..643ad6a5 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 @@ -2,6 +2,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -9,6 +10,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Feed details") @Data +@RegisterForReflection public class FeedInfo implements Serializable { @Schema(description = "url", requiredMode = RequiredMode.REQUIRED) 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 0eee703b..f7ffd7cf 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 @@ -2,6 +2,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -9,6 +10,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Server infos") @Data +@RegisterForReflection public class ServerInfo implements Serializable { @Schema 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 28850422..e28412f0 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 @@ -2,6 +2,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -9,6 +10,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "User settings") @Data +@RegisterForReflection public class Settings implements Serializable { @Schema(description = "user's preferred language, english if none", requiredMode = RequiredMode.REQUIRED) 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 1a022af2..902de115 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 @@ -8,6 +8,7 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -15,6 +16,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "User information") @Data +@RegisterForReflection public class Subscription implements Serializable { @Schema(description = "subscription id", requiredMode = RequiredMode.REQUIRED) 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 4f946ea2..6ecd7a1a 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 @@ -3,12 +3,14 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.time.Instant; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @SuppressWarnings("serial") @Schema(description = "Unread count") @Data +@RegisterForReflection public class UnreadCount implements Serializable { @Schema 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 7c7d332a..43d5faea 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 @@ -3,6 +3,7 @@ package com.commafeed.frontend.model; import java.io.Serializable; import java.time.Instant; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import lombok.Data; @@ -10,6 +11,7 @@ import lombok.Data; @SuppressWarnings("serial") @Schema(description = "User information") @Data +@RegisterForReflection public class UserModel implements Serializable { @Schema(description = "user id", requiredMode = RequiredMode.REQUIRED) diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java deleted file mode 100644 index c5dc4c98..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.commafeed.frontend.model.request; - -import java.io.Serializable; - -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Size; -import lombok.Data; - -@SuppressWarnings("serial") -@Data -@Schema -public class LoginRequest implements Serializable { - - @Schema(description = "username", requiredMode = RequiredMode.REQUIRED) - @Size(min = 3, max = 32) - private String name; - - @Schema(description = "password", requiredMode = RequiredMode.REQUIRED) - @NotEmpty - @Size(max = 128) - private String password; -} 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 c066a44d..1c2e1612 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 @@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; -import com.commafeed.frontend.auth.ValidPassword; +import com.commafeed.security.password.ValidPassword; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; 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 bdf84ec9..cc27052e 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 @@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request; import java.io.Serializable; -import com.commafeed.frontend.auth.ValidPassword; +import com.commafeed.security.password.ValidPassword; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; 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 1eccadb7..7f4f2c27 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 @@ -7,10 +7,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.model.User; @@ -18,14 +15,14 @@ import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.service.PasswordEncryptionService; import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.AdminSaveUserRequest; import com.commafeed.frontend.model.request.IDRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -33,8 +30,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -46,30 +44,29 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; -@Path("/admin") +@Path("/rest/admin") +@RolesAllowed(Roles.ADMIN) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @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 CommaFeedConfiguration config; private final MetricRegistry metrics; @Path("/user/save") @POST - @UnitOfWork + @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") - @Timed - public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user, - @Parameter(required = true) AdminSaveUserRequest req) { + public Response adminSaveUser(@Parameter(required = true) AdminSaveUserRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getName()); @@ -87,6 +84,7 @@ public class AdminREST { 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(); } @@ -121,14 +119,12 @@ public class AdminREST { @Path("/user/get/{id}") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get user information", description = "Get user information", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) }) - @Timed - public Response adminGetUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user, - @Parameter(description = "user id", required = true) @PathParam("id") Long id) { + public Response adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) { Preconditions.checkNotNull(id); User u = userDAO.findById(id); UserModel userModel = new UserModel(); @@ -142,13 +138,12 @@ public class AdminREST { @Path("/user/getAll") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get all users", description = "Get all users", responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserModel.class)))) }) - @Timed - public Response adminGetUsers(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) { + public Response adminGetUsers() { Map users = new HashMap<>(); for (UserRole role : userRoleDAO.findAll()) { User u = role.getUser(); @@ -173,11 +168,9 @@ public class AdminREST { @Path("/user/delete") @POST - @UnitOfWork + @Transactional @Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions") - @Timed - public Response adminDeleteUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user, - @Parameter(required = true) IDRequest req) { + public Response adminDeleteUser(@Parameter(required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -185,6 +178,8 @@ public class AdminREST { 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(); } @@ -192,24 +187,11 @@ public class AdminREST { return Response.ok().build(); } - @Path("/settings") - @GET - @UnitOfWork - @Operation( - summary = "Retrieve application settings", - description = "Retrieve application settings", - responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ApplicationSettings.class))) }) - @Timed - public Response getApplicationSettings(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) { - return Response.ok(config.getApplicationSettings()).build(); - } - @Path("/metrics") @GET - @UnitOfWork + @Transactional @Operation(summary = "Retrieve server metrics") - @Timed - public Response getMetrics(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) { + 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 374f03a4..3a6fd362 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 @@ -13,7 +13,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; @@ -29,7 +28,6 @@ import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedSubscriptionService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Category; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; @@ -40,13 +38,14 @@ import com.commafeed.frontend.model.request.CategoryModificationRequest; import com.commafeed.frontend.model.request.CollapseRequest; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; 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 io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -54,8 +53,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +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; @@ -70,11 +70,12 @@ import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Path("/category") +@Path("/rest/category") +@RolesAllowed(Roles.USER) @Slf4j @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Feed categories") public class CategoryREST { @@ -82,6 +83,7 @@ public class CategoryREST { public static final String ALL = "all"; public static final String STARRED = "starred"; + private final AuthenticationContext authenticationContext; private final FeedCategoryDAO feedCategoryDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedSubscriptionDAO feedSubscriptionDAO; @@ -92,13 +94,12 @@ public class CategoryREST { @Path("/entries") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get category entries", description = "Get a list of category entries", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) }) - @Timed - public Response getCategoryEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, + 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", @@ -137,22 +138,24 @@ public class CategoryREST { excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList(); } + 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); for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } } else if (STARRED.equals(id)) { entries.setName("Starred"); List starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, true); for (FeedEntryStatus status : starred) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } } else { FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id)); @@ -164,7 +167,7 @@ public class CategoryREST { offset, limit + 1, order, true, tag, null, null); for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } entries.setName(parent.getName()); } else { @@ -186,11 +189,10 @@ public class CategoryREST { @Path("/entriesAsFeed") @GET - @UnitOfWork + @Transactional @Operation(summary = "Get category entries as feed", description = "Get a feed of category entries") @Produces(MediaType.APPLICATION_XML) - @Timed - public Response getCategoryEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, + 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", @@ -205,7 +207,7 @@ public class CategoryREST { 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(user, id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag); + Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag); if (response.getStatus() != Status.OK.getStatusCode()) { return response; } @@ -215,7 +217,7 @@ public class CategoryREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(config.getApplicationSettings().getPublicUrl()); + feed.setLink(config.publicUrl()); feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList()); SyndFeedOutput output = new SyndFeedOutput(); @@ -231,11 +233,9 @@ public class CategoryREST { @Path("/mark") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read") - @Timed - public Response markCategoryEntries(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) { + public Response markCategoryEntries(@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -244,6 +244,7 @@ public class CategoryREST { 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()); @@ -268,17 +269,17 @@ public class CategoryREST { @Path("/add") @POST - @UnitOfWork + @Transactional @Operation( summary = "Add a category", description = "Add a new feed category", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) }) - @Timed - public Response addCategory(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(required = true) AddCategoryRequest req) { + public Response addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getName()); + User user = authenticationContext.getCurrentUser(); + FeedCategory cat = new FeedCategory(); cat.setName(req.getName()); cat.setUser(user); @@ -296,14 +297,14 @@ public class CategoryREST { @POST @Path("/delete") - @UnitOfWork + @Transactional @Operation(summary = "Delete a category", description = "Delete an existing feed category") - @Timed - public Response deleteCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) { + public Response deleteCategory(@Parameter(required = true) IDRequest req) { 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); @@ -329,14 +330,13 @@ public class CategoryREST { @POST @Path("/modify") - @UnitOfWork + @Transactional @Operation(summary = "Rename a category", description = "Rename an existing feed category") - @Timed - public Response modifyCategory(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(required = true) CategoryModificationRequest req) { + 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()); if (StringUtils.isNotBlank(req.getName())) { @@ -380,13 +380,13 @@ public class CategoryREST { @POST @Path("/collapse") - @UnitOfWork + @Transactional @Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category") - @Timed - public Response collapseCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) CollapseRequest req) { + 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(); @@ -399,25 +399,25 @@ public class CategoryREST { @GET @Path("/unreadCount") - @UnitOfWork + @Transactional @Operation( summary = "Get unread count for feed subscriptions", responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UnreadCount.class)))) }) - @Timed - public Response getUnreadCount(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user) { + public Response getUnreadCount() { + User user = authenticationContext.getCurrentUser(); Map unreadCount = feedSubscriptionService.getUnreadCount(user); return Response.ok(Lists.newArrayList(unreadCount.values())).build(); } @GET @Path("/get") - @UnitOfWork + @Transactional @Operation( summary = "Get root category", description = "Get all categories and subscriptions of the user", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Category.class))) }) - @Timed - public Response getRootCategory(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getRootCategory() { + User user = authenticationContext.getCurrentUser(); Category root = cache.getUserRootCategory(user); if (root == null) { log.debug("tree cache miss for {}", user.getId()); 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 2726f4d2..1ebe6954 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 @@ -2,24 +2,24 @@ package com.commafeed.frontend.resource; import java.util.List; -import com.codahale.metrics.annotation.Timed; import com.commafeed.backend.dao.FeedEntryTagDAO; import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedEntryTagService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.MultipleMarkRequest; import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.model.request.TagRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +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; @@ -30,42 +30,42 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; -@Path("/entry") +@Path("/rest/entry") +@RolesAllowed(Roles.USER) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Feed entries") public class EntryREST { + private final AuthenticationContext authenticationContext; private final FeedEntryTagDAO feedEntryTagDAO; private final FeedEntryService feedEntryService; private final FeedEntryTagService feedEntryTagService; @Path("/mark") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread") - @Timed - public Response markEntry(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) { + 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(); } @Path("/markMultiple") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread") - @Timed - public Response markEntries(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) { + 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()); @@ -76,15 +76,14 @@ public class EntryREST { @Path("/star") @POST - @UnitOfWork + @Transactional @Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread") - @Timed - public Response starEntry(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Star Request", required = true) StarRequest req) { + 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()); return Response.ok().build(); @@ -92,24 +91,23 @@ public class EntryREST { @Path("/tags") @GET - @UnitOfWork + @Transactional @Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user") - @Timed - public Response getTags(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getTags() { + User user = authenticationContext.getCurrentUser(); List tags = feedEntryTagDAO.findByUser(user); return Response.ok(tags).build(); } @Path("/tag") @POST - @UnitOfWork + @Transactional @Operation(summary = "Set feed entry tags") - @Timed - public Response tagEntry(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Tag Request", required = true) TagRequest req) { + 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(); 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 0d25fa42..52879805 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,9 +1,7 @@ package com.commafeed.frontend.resource; -import java.io.InputStream; import java.io.StringWriter; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Calendar; import java.util.Collections; @@ -13,9 +11,8 @@ import java.util.Objects; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.glassfish.jersey.media.multipart.FormDataParam; +import org.jboss.resteasy.reactive.RestForm; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.cache.CacheService; @@ -44,7 +41,6 @@ import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterEx import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedService; import com.commafeed.backend.service.FeedSubscriptionService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entry; import com.commafeed.frontend.model.FeedInfo; @@ -55,6 +51,8 @@ import com.commafeed.frontend.model.request.FeedModificationRequest; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.frontend.model.request.SubscribeRequest; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.rometools.opml.feed.opml.Opml; @@ -63,15 +61,15 @@ import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; import com.rometools.rome.io.WireFeedOutput; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +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; @@ -90,17 +88,19 @@ import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Path("/feed") +@Path("/rest/feed") +@RolesAllowed(Roles.USER) @Slf4j @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Feeds") public class FeedREST { private static final FeedEntry TEST_ENTRY = initTestEntry(); + private final AuthenticationContext authenticationContext; private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedCategoryDAO feedCategoryDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; @@ -129,14 +129,12 @@ public class FeedREST { @Path("/entries") @GET - @UnitOfWork + @Transactional @Operation( summary = "Get feed entries", description = "Get a list of feed entries", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) }) - @Timed - public Response getFeedEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, - @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, + 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, @@ -164,6 +162,7 @@ public class FeedREST { 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()); @@ -175,7 +174,7 @@ public class FeedREST { entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null); for (FeedEntryStatus status : list) { - entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); + entries.getEntries().add(Entry.build(status, config.imageProxyEnabled())); } boolean hasMore = entries.getEntries().size() > limit; @@ -195,12 +194,10 @@ public class FeedREST { @Path("/entriesAsFeed") @GET - @UnitOfWork + @Transactional @Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries") @Produces(MediaType.APPLICATION_XML) - @Timed - public Response getFeedEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user, - @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id, + 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, @@ -210,7 +207,7 @@ public class FeedREST { @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, 3 characters minimum") @QueryParam("keywords") String keywords) { - Response response = getFeedEntries(user, id, readType, newerThan, offset, limit, order, keywords); + Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords); if (response.getStatus() != Status.OK.getStatusCode()) { return response; } @@ -220,7 +217,7 @@ public class FeedREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(config.getApplicationSettings().getPublicUrl()); + feed.setLink(config.publicUrl()); feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList()); SyndFeedOutput output = new SyndFeedOutput(); @@ -253,14 +250,12 @@ public class FeedREST { @POST @Path("/fetch") - @UnitOfWork + @Transactional @Operation( summary = "Fetch a feed", description = "Fetch a feed by its url", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = FeedInfo.class))) }) - @Timed - public Response fetchFeed(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) { + public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getUrl()); @@ -279,25 +274,23 @@ public class FeedREST { @Path("/refreshAll") @GET - @UnitOfWork + @Transactional @Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue") - @Timed - public Response queueAllForRefresh(@Parameter(hidden = true) @SecurityCheck User user) { + public Response queueAllForRefresh() { + User user = authenticationContext.getCurrentUser(); feedSubscriptionService.refreshAll(user); return Response.ok().build(); } @Path("/refresh") @POST - @UnitOfWork + @Transactional @Operation(summary = "Queue a feed for refresh", description = "Manually add a feed to the refresh queue") - @Timed - public Response queueForRefresh(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "Feed id", required = true) IDRequest req) { - + public Response queueForRefresh(@Parameter(description = "Feed id", required = true) IDRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); + User user = authenticationContext.getCurrentUser(); FeedSubscription sub = feedSubscriptionDAO.findById(user, req.getId()); if (sub != null) { Feed feed = sub.getFeed(); @@ -309,11 +302,9 @@ public class FeedREST { @Path("/mark") @POST - @UnitOfWork + @Transactional @Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)") - @Timed - public Response markFeedEntries(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "Mark request", required = true) MarkRequest req) { + public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -322,6 +313,7 @@ public class FeedREST { 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, @@ -332,15 +324,14 @@ public class FeedREST { @GET @Path("/get/{id}") - @UnitOfWork + @Transactional @Operation( summary = "get feed", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Subscription.class))) }) - @Timed - public Response getFeed(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "user id", required = true) @PathParam("id") Long id) { - + 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(); @@ -351,13 +342,12 @@ public class FeedREST { @GET @Path("/favicon/{id}") - @UnitOfWork + @Transactional @Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon") - @Timed - public Response getFeedFavicon(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "subscription id", required = true) @PathParam("id") Long id) { - + 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(); @@ -382,14 +372,12 @@ public class FeedREST { @POST @Path("/subscribe") - @UnitOfWork + @Transactional @Operation( summary = "Subscribe to a feed", description = "Subscribe to a feed", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) }) - @Timed - public Response subscribe(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) { + public Response subscribe(@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getTitle()); Preconditions.checkNotNull(req.getUrl()); @@ -401,6 +389,7 @@ public class FeedREST { } FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl())); + User user = authenticationContext.getCurrentUser(); long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category); return Response.ok(subscriptionId).build(); } catch (Exception e) { @@ -413,19 +402,18 @@ public class FeedREST { @GET @Path("/subscribe") - @UnitOfWork + @Transactional @Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed") - @Timed - public Response subscribeFromUrl(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "feed url", required = true) @QueryParam("url") String url) { + 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()); } catch (Exception e) { log.info("Could not subscribe to url {} : {}", url, e.getMessage()); } - return Response.temporaryRedirect(URI.create(config.getApplicationSettings().getPublicUrl())).build(); + return Response.temporaryRedirect(URI.create(config.publicUrl())).build(); } private String prependHttp(String url) { @@ -437,13 +425,13 @@ public class FeedREST { @POST @Path("/unsubscribe") - @UnitOfWork + @Transactional @Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed") - @Timed - public Response unsubscribe(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) { + 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(); @@ -454,11 +442,9 @@ public class FeedREST { @POST @Path("/modify") - @UnitOfWork + @Transactional @Operation(summary = "Modify a subscription", description = "Modify a feed subscription") - @Timed - public Response modifyFeed(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) { + public Response modifyFeed(@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) { Preconditions.checkNotNull(req); Preconditions.checkNotNull(req.getId()); @@ -468,6 +454,7 @@ public class FeedREST { return Response.status(Status.BAD_REQUEST).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build(); } + User user = authenticationContext.getCurrentUser(); FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId()); subscription.setFilter(StringUtils.lowerCase(req.getFilter())); @@ -509,39 +496,36 @@ public class FeedREST { @POST @Path("/import") - @UnitOfWork + @Transactional @Consumes(MediaType.MULTIPART_FORM_DATA) @Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name") - @Timed - public Response importOpml(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "ompl file", required = true) @FormDataParam("file") InputStream input) { + public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) { + User user = authenticationContext.getCurrentUser(); if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build(); } try { - String opml = new String(input.readAllBytes(), StandardCharsets.UTF_8); opmlImporter.importOpml(user, opml); } catch (Exception e) { - log.error(e.getMessage(), e); - throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build()); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); } return Response.ok().build(); } @GET @Path("/export") - @UnitOfWork + @Transactional @Produces(MediaType.APPLICATION_XML) @Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions") - @Timed - public Response exportOpml(@Parameter(hidden = true) @SecurityCheck User user) { + public Response exportOpml() { + User user = authenticationContext.getCurrentUser(); Opml opml = opmlExporter.export(user); WireFeedOutput output = new WireFeedOutput(); String opmlString; try { opmlString = output.outputString(opml); } catch (Exception e) { - return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e).build(); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); } return Response.ok(opmlString).build(); } 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 eb213f50..881a7a04 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,25 +1,23 @@ package com.commafeed.frontend.resource; -import org.apache.commons.lang3.StringUtils; - -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedConfiguration; +import com.commafeed.CommaFeedVersion; import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.feed.FeedUtils; -import com.commafeed.backend.model.User; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.ServerInfo; +import com.commafeed.security.Roles; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.transaction.Transactional; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -30,49 +28,50 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; -@Path("/server") +@Path("/rest/server") + @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Server") public class ServerREST { private final HttpGetter httpGetter; private final CommaFeedConfiguration config; + private final CommaFeedVersion version; @Path("/get") @GET - @UnitOfWork + @PermitAll + @Transactional @Operation( summary = "Get server infos", description = "Get server infos", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ServerInfo.class))) }) - @Timed public Response getServerInfos() { ServerInfo infos = new ServerInfo(); - infos.setAnnouncement(config.getApplicationSettings().getAnnouncement()); - infos.setVersion(config.getVersion()); - infos.setGitCommit(config.getGitCommit()); - infos.setAllowRegistrations(config.getApplicationSettings().getAllowRegistrations()); - infos.setGoogleAnalyticsCode(config.getApplicationSettings().getGoogleAnalyticsTrackingCode()); - infos.setSmtpEnabled(StringUtils.isNotBlank(config.getApplicationSettings().getSmtpHost())); - infos.setDemoAccountEnabled(config.getApplicationSettings().getCreateDemoAccount()); - infos.setWebsocketEnabled(config.getApplicationSettings().getWebsocketEnabled()); - infos.setWebsocketPingInterval(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds()); - infos.setTreeReloadInterval(config.getApplicationSettings().getTreeReloadInterval().toMilliseconds()); + infos.setAnnouncement(config.announcement().orElse(null)); + infos.setVersion(version.getVersion()); + infos.setGitCommit(version.getGitCommit()); + infos.setAllowRegistrations(config.users().allowRegistrations()); + infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null)); + infos.setSmtpEnabled(config.smtp().isPresent()); + infos.setDemoAccountEnabled(config.users().createDemoAccount()); + infos.setWebsocketEnabled(config.websocket().enabled()); + infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis()); + infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis()); return Response.ok(infos).build(); } @Path("/proxy") @GET - @UnitOfWork + @RolesAllowed(Roles.USER) + @Transactional @Operation(summary = "proxy image") @Produces("image/png") - @Timed - public Response getProxiedImage(@Parameter(hidden = true) @SecurityCheck User user, - @Parameter(description = "image url", required = true) @QueryParam("u") String url) { - if (!config.getApplicationSettings().getImageProxyEnabled()) { + public Response getProxiedImage(@Parameter(description = "image url", required = true) @QueryParam("u") String url) { + if (!config.imageProxyEnabled()) { return Response.status(Status.FORBIDDEN).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 3ab79050..b391631f 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 @@ -10,7 +10,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.net.URIBuilder; -import com.codahale.metrics.annotation.Timed; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.Digests; @@ -29,25 +28,25 @@ import com.commafeed.backend.model.UserSettings.ScrollMode; import com.commafeed.backend.service.MailService; import com.commafeed.backend.service.PasswordEncryptionService; import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.auth.SecurityCheck; import com.commafeed.frontend.model.Settings; import com.commafeed.frontend.model.UserModel; -import com.commafeed.frontend.model.request.LoginRequest; import com.commafeed.frontend.model.request.PasswordResetRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.RegistrationRequest; -import com.commafeed.frontend.session.SessionHelper; +import com.commafeed.security.AuthenticationContext; +import com.commafeed.security.Roles; import com.google.common.base.Preconditions; -import io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.inject.Inject; +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; @@ -56,22 +55,23 @@ 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.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Path("/user") +@Path("/rest/user") +@RolesAllowed(Roles.USER) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Slf4j -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Tag(name = "Users") public class UserREST { + private final AuthenticationContext authenticationContext; private final UserDAO userDAO; private final UserRoleDAO userRoleDAO; private final UserSettingsDAO userSettingsDAO; @@ -82,14 +82,15 @@ public class UserREST { @Path("/settings") @GET - @UnitOfWork + @Transactional @Operation( summary = "Retrieve user settings", description = "Retrieve user settings", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Settings.class))) }) - @Timed - public Response getUserSettings(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getUserSettings() { Settings s = new Settings(); + + User user = authenticationContext.getCurrentUser(); UserSettings settings = userSettingsDAO.findByUser(user); if (settings != null) { s.setReadingMode(settings.getReadingMode().name()); @@ -145,12 +146,12 @@ public class UserREST { @Path("/settings") @POST - @UnitOfWork + @Transactional @Operation(summary = "Save user settings", description = "Save user settings") - @Timed - public Response saveUserSettings(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) Settings 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(); @@ -187,12 +188,13 @@ public class UserREST { @Path("/profile") @GET - @UnitOfWork + @Transactional @Operation( summary = "Retrieve user's profile", responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) }) - @Timed - public Response getUserProfile(@Parameter(hidden = true) @SecurityCheck User user) { + public Response getUserProfile() { + User user = authenticationContext.getCurrentUser(); + UserModel userModel = new UserModel(); userModel.setId(user.getId()); userModel.setName(user.getName()); @@ -209,11 +211,10 @@ public class UserREST { @Path("/profile") @POST - @UnitOfWork + @Transactional @Operation(summary = "Save user's profile") - @Timed - public Response saveUserProfile(@Parameter(hidden = true) @SecurityCheck User user, - @Valid @Parameter(required = true) ProfileModificationRequest request) { + public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) { + User user = authenticationContext.getCurrentUser(); if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).build(); } @@ -242,49 +243,29 @@ public class UserREST { user.setApiKey(userService.generateApiKey(user)); } - userDAO.update(user); + userDAO.saveOrUpdate(user); return Response.ok().build(); } @Path("/register") + @PermitAll @POST - @UnitOfWork + @Transactional @Operation(summary = "Register a new account") - @Timed - public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req, - @Context @Parameter(hidden = true) SessionHelper sessionHelper) { + public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) { try { - User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(), - Collections.singletonList(Role.USER)); - userService.login(req.getName(), req.getPassword()); - sessionHelper.setLoggedInUser(registeredUser); + 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("/login") - @POST - @UnitOfWork - @Operation(summary = "Login and create a session") - @Timed - public Response login(@Valid @Parameter(required = true) LoginRequest req, - @Parameter(hidden = true) @Context SessionHelper sessionHelper) { - Optional user = userService.login(req.getName(), req.getPassword()); - if (user.isPresent()) { - sessionHelper.setLoggedInUser(user.get()); - return Response.ok().build(); - } else { - return Response.status(Response.Status.UNAUTHORIZED).entity("wrong username or password").type(MediaType.TEXT_PLAIN).build(); - } - } - @Path("/passwordReset") + @PermitAll @POST - @UnitOfWork + @Transactional @Operation(summary = "send a password reset email") - @Timed public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) { User user = userDAO.findByEmail(req.getEmail()); if (user == null) { @@ -304,7 +285,7 @@ public class UserREST { } private String buildEmailContent(User user) throws Exception { - String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl()); + String publicUrl = FeedUtils.removeTrailingSlash(config.publicUrl()); publicUrl += "/rest/user/passwordResetCallback"; 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.", @@ -320,10 +301,10 @@ public class UserREST { } @Path("/passwordResetCallback") + @PermitAll @GET - @UnitOfWork + @Transactional @Produces(MediaType.TEXT_HTML) - @Timed public Response passwordRecoveryCallback(@Parameter(required = true) @QueryParam("email") String email, @Parameter(required = true) @QueryParam("token") String token) { Preconditions.checkNotNull(email); @@ -352,16 +333,16 @@ public class UserREST { String message = "Your new password is: " + passwd; message += "
"; - message += String.format("Back to Homepage", config.getApplicationSettings().getPublicUrl()); + message += String.format("Back to Homepage", config.publicUrl()); return Response.ok(message).build(); } @Path("/profile/deleteAccount") @POST - @UnitOfWork + @Transactional @Operation(summary = "Delete the user account") - @Timed - public Response deleteUser(@Parameter(hidden = true) @SecurityCheck User user) { + public Response deleteUser() { + User user = authenticationContext.getCurrentUser(); if (CommaFeedApplication.USERNAME_ADMIN.equals(user.getName()) || CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) { return Response.status(Status.FORBIDDEN).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 551f1f8e..69b7ba1a 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 @@ -13,9 +13,9 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.jboss.resteasy.reactive.server.multipart.FormValue; +import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; -import com.codahale.metrics.annotation.Timed; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -37,10 +37,10 @@ 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 io.dropwizard.hibernate.UnitOfWork; import io.swagger.v3.oas.annotations.Hidden; -import jakarta.inject.Inject; +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; @@ -64,9 +64,10 @@ import lombok.RequiredArgsConstructor; * * See https://feedafever.com/api */ -@Path("/fever") +@Path("/rest/fever") +@PermitAll @Produces(MediaType.APPLICATION_JSON) -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@RequiredArgsConstructor @Singleton @Hidden public class FeverREST { @@ -88,8 +89,7 @@ public class FeverREST { @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Path(PATH) @POST - @UnitOfWork - @Timed + @Transactional 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.get(0))); @@ -101,8 +101,7 @@ public class FeverREST { // e.g. FeedMe @Path(PATH) @POST - @UnitOfWork - @Timed + @Transactional public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) { Map params = new HashMap<>(); uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0))); @@ -113,8 +112,7 @@ public class FeverREST { // e.g. Unread @Path(PATH) @GET - @UnitOfWork - @Timed + @Transactional public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) { Map params = new HashMap<>(); uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0))); @@ -126,12 +124,11 @@ public class FeverREST { @Consumes(MediaType.MULTIPART_FORM_DATA) @Path(PATH) @POST - @UnitOfWork - @Timed - public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, FormDataMultiPart form) { + @Transactional + 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.get(0))); - form.getFields().forEach((k, v) -> params.put(k, v.get(0).getValue())); + form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null))); return handle(userId, params); } diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java deleted file mode 100644 index 29df13f8..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.commafeed.frontend.servlet; - -import java.io.IOException; -import java.util.Optional; - -import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.dao.UserSettingsDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserSettings; -import com.commafeed.frontend.session.SessionHelper; - -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -abstract class AbstractCustomCodeServlet extends HttpServlet { - - private static final long serialVersionUID = 1L; - - private final transient UnitOfWork unitOfWork; - private final transient UserDAO userDAO; - private final transient UserSettingsDAO userSettingsDAO; - - @Override - protected final void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType(getMimeType()); - - SessionHelper sessionHelper = new SessionHelper(req); - Optional userId = sessionHelper.getLoggedInUserId(); - final Optional user = unitOfWork.call(() -> userId.map(userDAO::findById)); - if (user.isEmpty()) { - return; - } - - UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user.get())); - if (settings == null) { - return; - } - - String customCode = getCustomCode(settings); - if (customCode == null) { - return; - } - - resp.getWriter().write(customCode); - } - - protected abstract String getMimeType(); - - protected abstract String getCustomCode(UserSettings settings); -} 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 7bcc1f91..0f95ee28 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,28 +1,39 @@ package com.commafeed.frontend.servlet; import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; 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.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import lombok.RequiredArgsConstructor; -public class CustomCssServlet extends AbstractCustomCodeServlet { +@Path("/custom_css.css") +@Produces("text/css") +@RequiredArgsConstructor +@Singleton +public class CustomCssServlet { - private static final long serialVersionUID = 1L; + private final AuthenticationContext authenticationContext; + private final UserSettingsDAO userSettingsDAO; + private final UnitOfWork unitOfWork; - @Inject - public CustomCssServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) { - super(unitOfWork, userDAO, userSettingsDAO); - } + @GET + public String get() { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return ""; + } - @Override - protected String getMimeType() { - return "text/css"; - } + UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user)); + if (settings == null) { + return ""; + } - @Override - protected String getCustomCode(UserSettings settings) { 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 0c29b7a9..2d651db5 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,30 +1,39 @@ package com.commafeed.frontend.servlet; import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; 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.Inject; import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import lombok.RequiredArgsConstructor; +@Path("/custom_js.js") +@Produces("application/javascript") +@RequiredArgsConstructor @Singleton -public class CustomJsServlet extends AbstractCustomCodeServlet { +public class CustomJsServlet { - private static final long serialVersionUID = 1L; + private final AuthenticationContext authenticationContext; + private final UserSettingsDAO userSettingsDAO; + private final UnitOfWork unitOfWork; - @Inject - public CustomJsServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) { - super(unitOfWork, userDAO, userSettingsDAO); - } + @GET + public String get() { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return ""; + } - @Override - protected String getMimeType() { - return "application/javascript"; - } + UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user)); + if (settings == null) { + return ""; + } - @Override - protected String getCustomCode(UserSettings settings) { 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 6d405709..09a7b426 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,26 +1,35 @@ package com.commafeed.frontend.servlet; -import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.util.Date; + +import org.eclipse.microprofile.config.inject.ConfigProperty; import com.commafeed.CommaFeedConfiguration; -import jakarta.inject.Inject; +import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; -@SuppressWarnings("serial") -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Path("/logout") +@PermitAll +@RequiredArgsConstructor @Singleton -public class LogoutServlet extends HttpServlet { +public class LogoutServlet { + + @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") + String cookieName; private final CommaFeedConfiguration config; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - req.getSession().invalidate(); - resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); + @GET + public Response get() { + NewCookie removeCookie = new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build(); + return Response.temporaryRedirect(URI.create(config.publicUrl())).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 dabda582..80ee3974 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,8 +1,7 @@ package com.commafeed.frontend.servlet; -import java.io.IOException; +import java.net.URI; import java.util.List; -import java.util.Optional; import org.apache.commons.lang3.StringUtils; @@ -11,86 +10,68 @@ import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.UnitOfWork; -import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.service.FeedEntryService; -import com.commafeed.backend.service.UserService; import com.commafeed.frontend.resource.CategoryREST; -import com.commafeed.frontend.session.SessionHelper; +import com.commafeed.security.AuthenticationContext; import com.google.common.collect.Iterables; -import jakarta.inject.Inject; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +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 lombok.RequiredArgsConstructor; -@SuppressWarnings("serial") -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) +@Path("/next") +@RequiredArgsConstructor @Singleton -public class NextUnreadServlet extends HttpServlet { - - private static final String PARAM_CATEGORYID = "category"; - private static final String PARAM_READINGORDER = "order"; +public class NextUnreadServlet { private final UnitOfWork unitOfWork; private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedCategoryDAO feedCategoryDAO; - private final UserDAO userDAO; - private final UserService userService; private final FeedEntryService feedEntryService; private final CommaFeedConfiguration config; + private final AuthenticationContext authenticationContext; - @Override - protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException { - final String categoryId = req.getParameter(PARAM_CATEGORYID); - String orderParam = req.getParameter(PARAM_READINGORDER); - - SessionHelper sessionHelper = new SessionHelper(req); - Optional userId = sessionHelper.getLoggedInUserId(); - Optional user = unitOfWork.call(() -> userId.map(userDAO::findById)); - user.ifPresent(value -> unitOfWork.run(() -> userService.performPostLoginActivities(value))); - if (user.isEmpty()) { - resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); - return; + @GET + public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { + User user = authenticationContext.getCurrentUser(); + if (user == null) { + return Response.temporaryRedirect(URI.create(config.publicUrl())).build(); } - final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc; - FeedEntryStatus status = unitOfWork.call(() -> { FeedEntryStatus s = null; if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { - List subs = feedSubscriptionDAO.findAll(user.get()); - List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, - true, null, null, null); + List subs = feedSubscriptionDAO.findAll(user); + List statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, + null, null, null); s = Iterables.getFirst(statuses, null); } else { - FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); + FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId)); if (category != null) { - List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); - List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); - List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, - 1, order, true, null, null, 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 = Iterables.getFirst(statuses, null); } } if (s != null) { - feedEntryService.markEntry(user.get(), s.getEntry().getId(), true); + feedEntryService.markEntry(user, s.getEntry().getId(), true); } return s; }); - if (status == null) { - resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); - } else { - String url = status.getEntry().getUrl(); - resp.sendRedirect(resp.encodeRedirectURL(url)); - } + String url = status == null ? config.publicUrl() : status.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 b9873ffe..3e29cc6d 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,22 +1,33 @@ package com.commafeed.frontend.servlet; -import java.io.IOException; +import org.apache.hc.core5.http.HttpStatus; +import com.commafeed.CommaFeedConfiguration; + +import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +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; +@Path("/robots.txt") +@PermitAll +@Produces(MediaType.TEXT_PLAIN) +@RequiredArgsConstructor @Singleton -public class RobotsTxtDisallowAllServlet extends HttpServlet { +public class RobotsTxtDisallowAllServlet { - private static final long serialVersionUID = 1L; + private final CommaFeedConfiguration config; - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType("text/plain"); - resp.getWriter().write("User-agent: *"); - resp.getWriter().write("\n"); - resp.getWriter().write("Disallow: /"); + @GET + 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 diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java deleted file mode 100644 index a3388db4..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.commafeed.frontend.session; - -import org.eclipse.jetty.server.session.DatabaseAdaptor; -import org.eclipse.jetty.server.session.DefaultSessionCache; -import org.eclipse.jetty.server.session.JDBCSessionDataStore; -import org.eclipse.jetty.server.session.SessionCache; -import org.eclipse.jetty.server.session.SessionHandler; - -import com.codahale.metrics.MetricRegistry; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.collect.ImmutableSet; - -import io.dropwizard.db.DataSourceFactory; -import io.dropwizard.util.Duration; -import jakarta.servlet.SessionTrackingMode; - -public class SessionHandlerFactory { - - @JsonProperty - private Duration cookieMaxAge = Duration.days(30); - - @JsonProperty - private Duration cookieRefreshAge = Duration.days(1); - - @JsonProperty - private Duration maxInactiveInterval = Duration.days(30); - - @JsonProperty - private Duration savePeriod = Duration.minutes(5); - - public SessionHandler build(DataSourceFactory dataSourceFactory) { - SessionHandler sessionHandler = new SessionHandler(); - sessionHandler.setHttpOnly(true); - sessionHandler.setSessionTrackingModes(ImmutableSet.of(SessionTrackingMode.COOKIE)); - sessionHandler.setMaxInactiveInterval((int) maxInactiveInterval.toSeconds()); - sessionHandler.setRefreshCookieAge((int) cookieRefreshAge.toSeconds()); - sessionHandler.getSessionCookieConfig().setMaxAge((int) cookieMaxAge.toSeconds()); - - SessionCache sessionCache = new DefaultSessionCache(sessionHandler); - sessionHandler.setSessionCache(sessionCache); - - JDBCSessionDataStore dataStore = new JDBCSessionDataStore(); - dataStore.setSavePeriodSec((int) savePeriod.toSeconds()); - sessionCache.setSessionDataStore(dataStore); - - DatabaseAdaptor adaptor = new DatabaseAdaptor(); - adaptor.setDatasource(dataSourceFactory.build(new MetricRegistry(), "sessions")); - dataStore.setDatabaseAdaptor(adaptor); - - return sessionHandler; - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java deleted file mode 100644 index 97341ea3..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.commafeed.frontend.session; - -import java.util.Optional; - -import com.commafeed.backend.model.User; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class SessionHelper { - - public static final String SESSION_KEY_USER_ID = "user-id"; - - private final HttpServletRequest request; - - public Optional getLoggedInUserId() { - HttpSession session = request.getSession(false); - return getLoggedInUserId(session); - } - - public static Optional getLoggedInUserId(HttpSession session) { - if (session == null) { - return Optional.empty(); - } - Long userId = (Long) session.getAttribute(SESSION_KEY_USER_ID); - return Optional.ofNullable(userId); - } - - public void setLoggedInUser(User user) { - request.getSession(true).setAttribute(SESSION_KEY_USER_ID, user.getId()); - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java deleted file mode 100644 index 4ff25de6..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.commafeed.frontend.session; - -import java.util.function.Function; - -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.server.ContainerRequest; -import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider; -import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; -import org.glassfish.jersey.server.model.Parameter; -import org.glassfish.jersey.server.spi.internal.ValueParamProvider; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.core.Context; - -@Singleton -public class SessionHelperFactoryProvider extends AbstractValueParamProvider { - - private final HttpServletRequest request; - - @Inject - public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, HttpServletRequest request) { - super(() -> extractorProvider, Parameter.Source.CONTEXT); - this.request = request; - } - - @Override - protected Function createValueProvider(Parameter parameter) { - final Class classType = parameter.getRawType(); - - Context context = parameter.getAnnotation(Context.class); - if (context == null) { - return null; - } - - if (!classType.isAssignableFrom(SessionHelper.class)) { - return null; - } - - return r -> new SessionHelper(request); - } - - public static class Binder extends AbstractBinder { - - @Override - protected void configure() { - bind(SessionHelperFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class); - } - } -} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java deleted file mode 100644 index 70d6a903..00000000 --- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.commafeed.frontend.ws; - -import java.util.Optional; - -import com.commafeed.frontend.session.SessionHelper; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpSession; -import jakarta.websocket.HandshakeResponse; -import jakarta.websocket.server.HandshakeRequest; -import jakarta.websocket.server.ServerEndpointConfig; -import jakarta.websocket.server.ServerEndpointConfig.Configurator; -import lombok.RequiredArgsConstructor; - -@Singleton -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) -public class WebSocketConfigurator extends Configurator { - - public static final String SESSIONKEY_USERID = "userId"; - - private final WebSocketSessions webSocketSessions; - - @Override - public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { - HttpSession httpSession = (HttpSession) request.getHttpSession(); - if (httpSession != null) { - Optional userId = SessionHelper.getLoggedInUserId(httpSession); - userId.ifPresent(value -> config.getUserProperties().put(SESSIONKEY_USERID, value)); - } - } - - @SuppressWarnings("unchecked") - @Override - public T getEndpointInstance(Class endpointClass) { - return (T) new WebSocketEndpoint(webSocketSessions); - } -} 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 4d145a75..93202ebf 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 @@ -2,39 +2,54 @@ package com.commafeed.frontend.ws; import java.io.IOException; -import jakarta.inject.Inject; +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; -import jakarta.websocket.Endpoint; -import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton -@RequiredArgsConstructor(onConstructor = @__({ @Inject })) -public class WebSocketEndpoint extends Endpoint { +@ServerEndpoint("/ws") +@RequiredArgsConstructor +public class WebSocketEndpoint { + private final AuthenticationContext authenticationContext; + private final CommaFeedConfiguration config; private final WebSocketSessions sessions; - @Override - public void onOpen(Session session, EndpointConfig config) { - Long userId = (Long) config.getUserProperties().get(WebSocketConfigurator.SESSIONKEY_USERID); - if (userId == null) { + @OnOpen + public void onOpen(Session session) { + User user = authenticationContext.getCurrentUser(); + if (user == null) { reject(session); return; } - log.debug("created websocket session for user {}", userId); - sessions.add(userId, session); + log.debug("created websocket session for user '{}'", user.getName()); + sessions.add(user.getId(), session); + session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000); + } - session.addMessageHandler(String.class, message -> { - 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) { @@ -45,9 +60,4 @@ public class WebSocketEndpoint extends Endpoint { } } - @Override - public void onClose(Session session, CloseReason reason) { - sessions.remove(session); - } - } 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 e82563e7..93caea5b 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 @@ -8,7 +8,6 @@ import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; import com.commafeed.backend.model.User; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.websocket.Session; import lombok.extern.slf4j.Slf4j; @@ -20,7 +19,6 @@ public class WebSocketSessions { // a user may have multiple sessions (two tabs, two devices, ...) private final Map> sessions = new ConcurrentHashMap<>(); - @Inject public WebSocketSessions(MetricRegistry metrics) { metrics.register(MetricRegistry.name(getClass(), "users"), (Gauge) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count()); diff --git a/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java new file mode 100644 index 00000000..e0f5f794 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java @@ -0,0 +1,29 @@ +package com.commafeed.security; + +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; + + public User getCurrentUser() { + if (securityIdentity.isAnonymous()) { + return null; + } + + String userId = securityIdentity.getPrincipal().getName(); + if (userId == null) { + return null; + } + + return userDAO.findById(Long.valueOf(userId)); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/security/Roles.java b/commafeed-server/src/main/java/com/commafeed/security/Roles.java new file mode 100644 index 00000000..f614ca94 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/Roles.java @@ -0,0 +1,6 @@ +package com.commafeed.security; + +public class Roles { + 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 new file mode 100644 index 00000000..5c1c95e7 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java @@ -0,0 +1,50 @@ +package com.commafeed.security.identity; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +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; +import io.quarkus.security.identity.SecurityIdentity; +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 lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Singleton +public class DatabaseApiKeyIdentityProvider implements IdentityProvider { + + private final UnitOfWork unitOfWork; + private final UserService userService; + + @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"); + } + + 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 new file mode 100644 index 00000000..dc3537da --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java @@ -0,0 +1,51 @@ +package com.commafeed.security.identity; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +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; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Singleton; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Singleton +public class DatabaseUsernamePasswordIdentityProvider implements IdentityProvider { + + private final UnitOfWork unitOfWork; + private final UserService userService; + + @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"); + } + + 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 new file mode 100644 index 00000000..ecfffc57 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java @@ -0,0 +1,57 @@ +package com.commafeed.security.identity; + +import java.util.Set; +import java.util.stream.Collectors; + +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; +import io.quarkus.security.identity.SecurityIdentity; +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 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; + + @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"); + } + + // 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(); + }); + } +} 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 new file mode 100644 index 00000000..b611f827 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java @@ -0,0 +1,46 @@ +package com.commafeed.security.mechanism; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.security.credential.TokenCredential; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.TokenAuthenticationRequest; +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; + +@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()); + } + + 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); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().optional(Optional.empty()); + } + + @Override + public Set> getCredentialTypes() { + return Set.of(TokenAuthenticationRequest.class); + } +} diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java similarity index 94% rename from commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java rename to commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java index 5063770f..4b842c77 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java +++ b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java @@ -1,4 +1,4 @@ -package com.commafeed.frontend.auth; +package com.commafeed.security.password; import java.util.List; diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java similarity index 90% rename from commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java rename to commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java index 63efdb4d..fc559c50 100644 --- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java +++ b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java @@ -1,4 +1,4 @@ -package com.commafeed.frontend.auth; +package com.commafeed.security.password; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json b/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json new file mode 100644 index 00000000..a8a50b21 --- /dev/null +++ b/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json @@ -0,0 +1,10 @@ +{ + "resources": { + "includes": [ + { "pattern": "^default_banner\\.txt$" }, + { "pattern": "^images/default_favicon\\.gif$" }, + { "pattern": "^git\\.properties$" }, + { "pattern": "^rome\\.properties$" } + ] + } +} diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties new file mode 100644 index 00000000..733fd569 --- /dev/null +++ b/commafeed-server/src/main/resources/application.properties @@ -0,0 +1,43 @@ +# http +quarkus.http.port=8082 +quarkus.http.test-port=8085 + +# security +quarkus.http.auth.basic=true +quarkus.http.auth.form.enabled=true +quarkus.http.auth.form.http-only-cookie=true +quarkus.http.auth.form.timeout=P30d +quarkus.http.auth.form.landing-page= +quarkus.http.auth.form.login-page= +quarkus.http.auth.form.error-page= + +# websocket +quarkus.websocket.dispatch-to-worker=true + +# database +quarkus.datasource.db-kind=h2 +quarkus.liquibase.change-log=migrations.xml +quarkus.liquibase.migrate-at-start=true + +# shutdown +quarkus.shutdown.timeout=5s + + +# dev profile overrides +%dev.quarkus.http.port=8083 +%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890 +%dev.quarkus.log.category."com.commafeed".level=DEBUG +# %dev.quarkus.hibernate-orm.log.sql=true + + +# test profile overrides +%test.quarkus.log.category."org.mockserver".level=WARN +%test.quarkus.log.category."liquibase".level=WARN +%test.commafeed.users.create-demo-account=true +%test.commafeed.users.allow-registrations=true +%test.commafeed.smtp.host=localhost +%test.commafeed.smtp.port=3025 +%test.commafeed.smtp.tls=false +%test.commafeed.smtp.user-name=user +%test.commafeed.smtp.password=pass +%test.commafeed.smtp.from-address=noreply@commafeed.com diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml index 7b60e188..b1713439 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 7:6d3ad493d25dd9c50067e804efc9ffcc diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml index 065ead9f..094de039 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml index 3bbe49c6..c735f521 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml @@ -1,6 +1,7 @@ - + @@ -14,7 +15,7 @@ + referencedTableName="FEEDS" referencedColumnNames="id" /> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml index 691dbde5..3e5f07d2 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml @@ -1,6 +1,7 @@ - + diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml index 4920edcf..578c0bc0 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml @@ -1,6 +1,7 @@ - + @@ -31,9 +32,9 @@ + referencedTableName="FEEDENTRIES" referencedColumnNames="id" /> + referencedTableName="USERS" referencedColumnNames="id" /> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml index 09ce1137..2a8f58f7 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml @@ -1,6 +1,7 @@ - + 8:58e8060bba0ec9d448f4346eb35d815c diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml index df158560..6125c98e 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml @@ -1,6 +1,7 @@ - + diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml index 1af324a6..6fb75000 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml @@ -1,6 +1,7 @@ - + diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml index 726e4228..29e7fd95 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> @@ -26,7 +26,7 @@ 8:39e5a9ff312af90d82f87c88abf1c66d + columnDataType="VARCHAR(4096)" /> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml index 782777bc..9d337166 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml index 238b9b1b..87835d25 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml index eee22181..c3c1c916 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml index 2eaeaa95..52bd6b36 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml index 83ce42b0..f2c386a3 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml index 6e898e72..3d3d6717 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml index 7540a1fe..9c00a813 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml index a52f67d6..0af84537 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 9:bf66bf7def9ec3dab1f365f7230d92cf diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml index 806095f7..a9d696c1 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 9:663bcc7c6df5b832ec2109a3afcff5c6 diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml index c74b779c..c15ed003 100644 --- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml +++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> 9:078593b238a4639a97a3cd82f7e5e30d diff --git a/commafeed-server/src/main/resources/banner.txt b/commafeed-server/src/main/resources/default_banner.txt similarity index 100% rename from commafeed-server/src/main/resources/banner.txt rename to commafeed-server/src/main/resources/default_banner.txt diff --git a/commafeed-server/src/main/resources/migrations.xml b/commafeed-server/src/main/resources/migrations.xml index 2d951ed1..eae6c227 100644 --- a/commafeed-server/src/main/resources/migrations.xml +++ b/commafeed-server/src/main/resources/migrations.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> diff --git a/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java b/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java deleted file mode 100644 index 0cc7a0fb..00000000 --- a/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.commafeed; - -import java.io.IOException; -import java.io.InputStream; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import javax.sql.DataSource; - -import org.mockserver.socket.PortFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.MariaDBContainer; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; - -import com.codahale.metrics.MetricRegistry; -import com.commafeed.CommaFeedConfiguration.CacheType; - -import io.dropwizard.testing.ConfigOverride; -import io.dropwizard.testing.ResourceHelpers; -import io.dropwizard.testing.junit5.DropwizardAppExtension; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; - -public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension { - private static final String TEST_DATABASE = System.getenv().getOrDefault("TEST_DATABASE", "h2"); - private static final boolean REDIS_ENABLED = Boolean.parseBoolean(System.getenv().getOrDefault("REDIS", "false")); - - private static final ConfigOverride[] CONFIG_OVERRIDES; - private static final List DROP_ALL_STATEMENTS; - static { - List overrides = new ArrayList<>(); - overrides.add(ConfigOverride.config("server.applicationConnectors[0].port", String.valueOf(PortFactory.findFreePort()))); - - Properties imageNames = readProperties("/docker-images.properties"); - - DatabaseConfiguration config = buildConfiguration(TEST_DATABASE, imageNames.getProperty(TEST_DATABASE)); - JdbcDatabaseContainer container = config.container(); - if (container != null) { - container.withDatabaseName("commafeed"); - container.withEnv("TZ", "UTC"); - container.start(); - - overrides.add(ConfigOverride.config("database.url", container.getJdbcUrl())); - overrides.add(ConfigOverride.config("database.user", container.getUsername())); - overrides.add(ConfigOverride.config("database.password", container.getPassword())); - overrides.add(ConfigOverride.config("database.driverClass", container.getDriverClassName())); - } - - if (REDIS_ENABLED) { - GenericContainer redis = new GenericContainer<>(DockerImageName.parse(imageNames.getProperty("redis"))) - .withExposedPorts(6379); - redis.start(); - - overrides.add(ConfigOverride.config("app.cache", "redis")); - overrides.add(ConfigOverride.config("redis.host", redis.getHost())); - overrides.add(ConfigOverride.config("redis.port", redis.getMappedPort(6379).toString())); - } - - CONFIG_OVERRIDES = overrides.toArray(new ConfigOverride[0]); - DROP_ALL_STATEMENTS = config.dropAllStatements(); - } - - public CommaFeedDropwizardAppExtension() { - super(CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"), CONFIG_OVERRIDES); - } - - private static DatabaseConfiguration buildConfiguration(String databaseName, String imageName) { - if ("postgresql".equals(databaseName)) { - JdbcDatabaseContainer container = new PostgreSQLContainer<>(imageName).withTmpFs(Map.of("/var/lib/postgresql/data", "rw")); - return new DatabaseConfiguration(container, List.of("DROP SCHEMA public CASCADE", "CREATE SCHEMA public")); - } else if ("mysql".equals(databaseName)) { - JdbcDatabaseContainer container = new MySQLContainer<>(imageName).withTmpFs(Map.of("/var/lib/mysql", "rw")); - return new DatabaseConfiguration(container, List.of("DROP DATABASE IF EXISTS commafeed", " CREATE DATABASE commafeed")); - } else if ("mariadb".equals(databaseName)) { - JdbcDatabaseContainer container = new MariaDBContainer<>(imageName).withTmpFs(Map.of("/var/lib/mysql", "rw")); - return new DatabaseConfiguration(container, List.of("DROP DATABASE IF EXISTS commafeed", " CREATE DATABASE commafeed")); - } else { - // h2 - return new DatabaseConfiguration(null, List.of("DROP ALL OBJECTS")); - } - } - - private static Properties readProperties(String path) { - Properties properties = new Properties(); - try (InputStream is = CommaFeedDropwizardAppExtension.class.getResourceAsStream(path)) { - properties.load(is); - } catch (IOException e) { - throw new RuntimeException("could not read resource " + path, e); - } - return properties; - } - - @Override - public void after() { - super.after(); - - // clean database after each test - DataSource dataSource = getConfiguration().getDataSourceFactory().build(new MetricRegistry(), "cleanup"); - try (Connection connection = dataSource.getConnection()) { - for (String statement : DROP_ALL_STATEMENTS) { - connection.prepareStatement(statement).executeUpdate(); - } - } catch (SQLException e) { - throw new RuntimeException("could not cleanup database", e); - } - - // clean redis cache after each test - if (getConfiguration().getApplicationSettings().getCache() == CacheType.REDIS) { - try (JedisPool pool = getConfiguration().getRedisPoolFactory().build(); Jedis jedis = pool.getResource()) { - jedis.flushAll(); - } - } - } - - private record DatabaseConfiguration(JdbcDatabaseContainer container, List dropAllStatements) { - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java new file mode 100644 index 00000000..97e43ddd --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java @@ -0,0 +1,34 @@ +package com.commafeed; + +import org.kohsuke.MetaInfServices; + +import com.commafeed.backend.service.db.DatabaseStartupService; + +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback; +import io.quarkus.test.junit.callback.QuarkusTestMethodContext; +import jakarta.enterprise.inject.spi.CDI; +import liquibase.Liquibase; +import liquibase.exception.LiquibaseException; + +/** + * Resets database between tests + */ +@MetaInfServices +public class DatabaseReset implements QuarkusTestBeforeEachCallback { + + @SuppressWarnings("deprecation") + @Override + public void beforeEach(QuarkusTestMethodContext context) { + LiquibaseFactory liquibaseFactory = CDI.current().select(LiquibaseFactory.class).get(); + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.dropAll(); + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } catch (LiquibaseException e) { + throw new RuntimeException(e); + } + + DatabaseStartupService databaseStartupService = CDI.current().select(DatabaseStartupService.class).get(); + databaseStartupService.populateInitialData(); + } +} 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 06da263d..e0289599 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -1,14 +1,16 @@ package com.commafeed.backend; import java.io.IOException; +import java.math.BigInteger; import java.net.SocketTimeoutException; import java.util.Arrays; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.io.IOUtils; import org.apache.hc.client5.http.ConnectTimeoutException; -import org.eclipse.jetty.http.HttpStatus; +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; @@ -26,13 +28,13 @@ import org.mockserver.model.MediaType; import com.codahale.metrics.MetricRegistry; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; +import com.commafeed.CommaFeedVersion; import com.commafeed.backend.HttpGetter.HttpResponseException; import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.NotModifiedException; import com.google.common.net.HttpHeaders; -import io.dropwizard.util.DataSize; +import io.quarkus.runtime.configuration.MemorySize; @ExtendWith(MockServerExtension.class) class HttpGetterTest { @@ -51,21 +53,17 @@ class HttpGetterTest { this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/"; this.feedContent = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/feed/rss.xml"))); - ApplicationSettings settings = new ApplicationSettings(); - settings.setUserAgent("http-getter-test"); - settings.setBackgroundThreads(3); - settings.setMaxFeedResponseSize(DataSize.kilobytes(10)); + CommaFeedConfiguration config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(config.feedRefresh().userAgent()).thenReturn(Optional.of("http-getter-test")); + Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); + Mockito.when(config.feedRefresh().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); - CommaFeedConfiguration config = new CommaFeedConfiguration(); - config.setApplicationSettings(settings); - - this.getter = new HttpGetter(config, Mockito.mock(MetricRegistry.class)); + this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class)); } @ParameterizedTest @ValueSource( - ints = { HttpStatus.UNAUTHORIZED_401, HttpStatus.FORBIDDEN_403, HttpStatus.NOT_FOUND_404, - HttpStatus.INTERNAL_SERVER_ERROR_500 }) + 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)); @@ -93,8 +91,8 @@ class HttpGetterTest { @ParameterizedTest @ValueSource( - ints = { HttpStatus.MOVED_PERMANENTLY_301, HttpStatus.MOVED_TEMPORARILY_302, HttpStatus.TEMPORARY_REDIRECT_307, - HttpStatus.PERMANENT_REDIRECT_308 }) + 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("/")) @@ -129,7 +127,7 @@ class HttpGetterTest { void connectTimeout() { // try to connect to a non-routable address // https://stackoverflow.com/a/904609 - Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 2000)); + Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 500)); } @Test @@ -144,7 +142,7 @@ class HttpGetterTest { @Test void lastModifiedReturns304() { this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456")) - .respond(HttpResponse.response().withStatusCode(HttpStatus.NOT_MODIFIED_304)); + .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, "123456", null, TIMEOUT)); } @@ -152,7 +150,7 @@ class HttpGetterTest { @Test void eTagReturns304() { this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_NONE_MATCH, "78910")) - .respond(HttpResponse.response().withStatusCode(HttpStatus.NOT_MODIFIED_304)); + .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED)); Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, null, "78910", TIMEOUT)); } @@ -195,7 +193,7 @@ class HttpGetterTest { @Test void largeFeedWithContentLengthHeader() { - byte[] bytes = new byte[(int) DataSize.kilobytes(100).toBytes()]; + byte[] bytes = new byte[100000]; Arrays.fill(bytes, (byte) 1); this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody(bytes)); @@ -205,7 +203,7 @@ class HttpGetterTest { @Test void largeFeedWithoutContentLengthHeader() { - byte[] bytes = new byte[(int) DataSize.kilobytes(100).toBytes()]; + byte[] bytes = new byte[100000]; Arrays.fill(bytes, (byte) 1); this.mockServerClient.when(HttpRequest.request().withMethod("GET")) .respond(HttpResponse.response() 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 b47ed808..c231cdd9 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,7 +1,7 @@ package com.commafeed.backend.feed; import java.time.Instant; -import java.util.Set; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -28,7 +28,7 @@ class FeedFetcherTest { private HttpGetter getter; @Mock - private Set urlProviders; + private List urlProviders; private FeedFetcher fetcher; diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java deleted file mode 100644 index 578cbc3e..00000000 --- a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.commafeed.backend.service.db; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class H2MigrationServiceTest { - - @TempDir - private Path root; - - @Test - void testMigrateIfNeeded() throws IOException { - Path path = root.resolve("database.mv.db"); - Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/h2-migration/database-v2.1.214.mv.db")), path); - - H2MigrationService service = new H2MigrationService(); - Assertions.assertEquals(2, service.getH2FileFormat(path)); - - service.migrateIfNeeded(path, "sa", "sa"); - Assertions.assertEquals(3, service.getH2FileFormat(path)); - } - -} \ No newline at end of file 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 56f12089..2fcfbbf6 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java @@ -1,20 +1,35 @@ package com.commafeed.e2e; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import com.commafeed.CommaFeedDropwizardAppExtension; +import com.microsoft.playwright.Browser; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; import com.microsoft.playwright.assertions.PlaywrightAssertions; import com.microsoft.playwright.options.AriaRole; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.quarkus.test.junit.QuarkusTest; -@ExtendWith(DropwizardExtensionsSupport.class) -class AuthentificationIT extends PlaywrightTestBase { +@QuarkusTest +class AuthentificationIT { - private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension(); + private final Playwright playwright = Playwright.create(); + private final Browser browser = playwright.chromium().launch(); + + private Page page; + + @BeforeEach + void init() { + page = browser.newContext().newPage(); + } + + @AfterEach + void cleanup() { + playwright.close(); + } @Test void loginFail() { @@ -29,7 +44,7 @@ class AuthentificationIT extends PlaywrightTestBase { void loginSuccess() { page.navigate(getLoginPageUrl()); PlaywrightTestUtils.login(page); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all"); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); } @Test @@ -56,10 +71,10 @@ class AuthentificationIT extends PlaywrightTestBase { page.getByPlaceholder("E-mail address").fill("user@domain.com"); page.getByPlaceholder("Password").fill("MyPassword1!"); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")).click(); - PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all"); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all"); } private String getLoginPageUrl() { - return "http://localhost:" + EXT.getLocalPort() + "/#/login"; + return "http://localhost:8085/#/login"; } } diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java deleted file mode 100644 index 81735c3b..00000000 --- a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.commafeed.e2e; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestWatcher; - -import com.microsoft.playwright.Browser; -import com.microsoft.playwright.Browser.NewContextOptions; -import com.microsoft.playwright.BrowserContext; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Playwright; -import com.microsoft.playwright.Tracing; - -/** - * Base class for all Playwright tests. - * - *
    - *
  • Takes a screenshot on failure
  • - *
  • Keeps the video on failure
  • - *
  • Saves a trace file on failure
  • - *
- * - * inspired by https://github.com/microsoft/playwright-java/issues/503#issuecomment-872636373 - * - */ -@ExtendWith(PlaywrightTestBase.SaveArtifactsOnTestFailed.class) -public abstract class PlaywrightTestBase { - - private static Playwright playwright; - private static Browser browser; - - protected Page page; - private BrowserContext context; - - @BeforeAll - static void initBrowser() { - playwright = Playwright.create(); - browser = playwright.chromium().launch(); - } - - @AfterAll - static void closeBrowser() { - playwright.close(); - } - - protected void customizeNewContextOptions(NewContextOptions options) { - // override in subclasses to customize the browser context - } - - protected static class SaveArtifactsOnTestFailed implements TestWatcher, BeforeEachCallback { - - // defined in the config of maven-failsafe-plugin in pom.xml - private final String buildDirectory = System.getProperty("buildDirectory", "target"); - private final String directory = buildDirectory + "/playwright-artifacts"; - - @Override - public void beforeEach(ExtensionContext context) { - PlaywrightTestBase testInstance = getTestInstance(context); - - NewContextOptions newContextOptions = new Browser.NewContextOptions().setRecordVideoDir(Paths.get(directory)); - testInstance.customizeNewContextOptions(newContextOptions); - testInstance.context = PlaywrightTestBase.browser.newContext(newContextOptions); - testInstance.context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); - - testInstance.page = testInstance.context.newPage(); - } - - @Override - public void testFailed(ExtensionContext context, Throwable cause) { - PlaywrightTestBase testInstance = getTestInstance(context); - - String fileName = getFileName(context); - - saveScreenshot(testInstance, fileName); - saveTrace(testInstance, fileName); - - testInstance.context.close(); - - saveVideo(testInstance, fileName); - } - - @Override - public void testAborted(ExtensionContext context, Throwable cause) { - PlaywrightTestBase testInstance = getTestInstance(context); - testInstance.context.close(); - testInstance.page.video().delete(); - } - - @Override - public void testDisabled(ExtensionContext context, Optional reason) { - PlaywrightTestBase testInstance = getTestInstance(context); - testInstance.context.close(); - testInstance.page.video().delete(); - } - - @Override - public void testSuccessful(ExtensionContext context) { - PlaywrightTestBase testInstance = getTestInstance(context); - testInstance.context.close(); - testInstance.page.video().delete(); - } - - private PlaywrightTestBase getTestInstance(ExtensionContext context) { - return (PlaywrightTestBase) context.getRequiredTestInstance(); - } - - private String getFileName(ExtensionContext context) { - return String.format("%s.%s-%s", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), - DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm-ss").format(ZonedDateTime.now())); - } - - private void saveScreenshot(PlaywrightTestBase testInstance, String fileName) { - byte[] screenshot = testInstance.page.screenshot(); - try { - Files.write(Paths.get(directory, fileName + ".png"), screenshot); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void saveTrace(PlaywrightTestBase testInstance, String fileName) { - testInstance.context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get(directory, fileName + ".zip"))); - } - - private void saveVideo(PlaywrightTestBase testInstance, String fileName) { - testInstance.page.video().saveAs(Paths.get(directory, fileName + ".webm")); - testInstance.page.video().delete(); - } - } -} 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 2264701f..fa99d241 100644 --- a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java +++ b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java @@ -6,43 +6,51 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockserver.client.MockServerClient; -import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.CommaFeedDropwizardAppExtension; +import com.microsoft.playwright.Browser; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; import com.microsoft.playwright.assertions.PlaywrightAssertions; import com.microsoft.playwright.options.AriaRole; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.quarkus.test.junit.QuarkusTest; -@ExtendWith(DropwizardExtensionsSupport.class) -@ExtendWith(MockServerExtension.class) -class ReadingIT extends PlaywrightTestBase { +@QuarkusTest +class ReadingIT { - private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension(); + private final Playwright playwright = Playwright.create(); + private final Browser browser = playwright.chromium().launch(); + private Page page; private MockServerClient mockServerClient; @BeforeEach - void init(MockServerClient mockServerClient) throws IOException { - this.mockServerClient = mockServerClient; + void init() throws IOException { + this.page = browser.newContext().newPage(); + 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)); } + @AfterEach + void cleanup() { + playwright.close(); + } + @Test void scenario() { // login - page.navigate("http://localhost:" + EXT.getLocalPort()); + page.navigate("http://localhost:8085"); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Log in")).click(); PlaywrightTestUtils.login(page); diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java deleted file mode 100644 index 1069ef8f..00000000 --- a/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.commafeed.frontend.auth; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import com.commafeed.backend.dao.UserDAO; -import com.commafeed.backend.model.User; -import com.commafeed.backend.service.UserService; -import com.commafeed.backend.service.internal.PostLoginActivities; -import com.commafeed.frontend.session.SessionHelper; - -class SecurityCheckFactoryTest { - - @Test - void cookieLoginShouldPerformPostLoginActivities() { - User userInSession = new User(); - UserDAO userDAO = Mockito.mock(UserDAO.class); - Mockito.when(userDAO.findById(1L)).thenReturn(userInSession); - - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - Mockito.when(sessionHelper.getLoggedInUserId()).thenReturn(Optional.of(1L)); - - PostLoginActivities postLoginActivities = Mockito.mock(PostLoginActivities.class); - - UserService service = new UserService(null, null, null, null, null, null, null, postLoginActivities); - - SecurityCheckFactory factory = new SecurityCheckFactory(userDAO, service, null, null, null, false); - factory.cookieSessionLogin(sessionHelper); - - Mockito.verify(postLoginActivities).executeFor(userInSession); - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java deleted file mode 100644 index a1ccdb4d..00000000 --- a/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.commafeed.frontend.resource; - -import java.util.Collections; -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.InOrder; -import org.mockito.Mockito; - -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole.Role; -import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.model.request.LoginRequest; -import com.commafeed.frontend.model.request.RegistrationRequest; -import com.commafeed.frontend.session.SessionHelper; - -class UserRestTest { - - @Test - void loginShouldNotPopulateHttpSessionIfUnsuccessfull() { - // Absent user - Optional absentUser = Optional.empty(); - - // Create UserService partial mock - UserService service = Mockito.mock(UserService.class); - Mockito.when(service.login("user", "password")).thenReturn(absentUser); - - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - - LoginRequest req = new LoginRequest(); - req.setName("user"); - req.setPassword("password"); - - userREST.login(req, sessionHelper); - - Mockito.verify(sessionHelper, Mockito.never()).setLoggedInUser(Mockito.any(User.class)); - } - - @Test - void loginShouldPopulateHttpSessionIfSuccessfull() { - // Create a user - User user = new User(); - - // Create UserService mock - UserService service = Mockito.mock(UserService.class); - Mockito.when(service.login("user", "password")).thenReturn(Optional.of(user)); - - LoginRequest req = new LoginRequest(); - req.setName("user"); - req.setPassword("password"); - - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - - userREST.login(req, sessionHelper); - - Mockito.verify(sessionHelper).setLoggedInUser(user); - } - - @Test - void registerShouldRegisterAndThenLogin() { - // Create UserService mock - UserService service = Mockito.mock(UserService.class); - - RegistrationRequest req = new RegistrationRequest(); - req.setName("user"); - req.setPassword("password"); - req.setEmail("test@test.com"); - - InOrder inOrder = Mockito.inOrder(service); - - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - - userREST.registerUser(req, sessionHelper); - - inOrder.verify(service).register("user", "password", "test@test.com", Collections.singletonList(Role.USER)); - inOrder.verify(service).login("user", "password"); - } - - @Test - void registerShouldPopulateHttpSession() { - // Create a user - User user = new User(); - - // Create UserService mock - UserService service = Mockito.mock(UserService.class); - Mockito.when(service.register(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class), - ArgumentMatchers.anyList())).thenReturn(user); - Mockito.when(service.login(Mockito.any(String.class), Mockito.any(String.class))).thenReturn(Optional.of(user)); - - RegistrationRequest req = new RegistrationRequest(); - req.setName("user"); - req.setPassword("password"); - req.setEmail("test@test.com"); - - SessionHelper sessionHelper = Mockito.mock(SessionHelper.class); - UserREST userREST = new UserREST(null, null, null, service, null, null, null); - - userREST.registerUser(req, sessionHelper); - - Mockito.verify(sessionHelper).setLoggedInUser(user); - } - -} diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java deleted file mode 100644 index 6f535879..00000000 --- a/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.commafeed.frontend.session; - -import java.util.Optional; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; - -class SessionHelperTest { - - @Test - void gettingUserDoesNotCreateSession() { - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - - SessionHelper sessionHelper = new SessionHelper(request); - sessionHelper.getLoggedInUserId(); - - Mockito.verify(request).getSession(false); - } - - @Test - void gettingUserShouldNotReturnUserIfThereIsNoPreexistingHttpSession() { - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getSession(false)).thenReturn(null); - - SessionHelper sessionHelper = new SessionHelper(request); - Optional userId = sessionHelper.getLoggedInUserId(); - - Assertions.assertFalse(userId.isPresent()); - } - - @Test - void gettingUserShouldNotReturnUserIfUserNotPresentInHttpSession() { - HttpSession session = Mockito.mock(HttpSession.class); - Mockito.when(session.getAttribute(SessionHelper.SESSION_KEY_USER_ID)).thenReturn(null); - - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getSession(false)).thenReturn(session); - - SessionHelper sessionHelper = new SessionHelper(request); - Optional userId = sessionHelper.getLoggedInUserId(); - - Assertions.assertFalse(userId.isPresent()); - } - - @Test - void gettingUserShouldReturnUserIfUserPresentInHttpSession() { - HttpSession session = Mockito.mock(HttpSession.class); - Mockito.when(session.getAttribute(SessionHelper.SESSION_KEY_USER_ID)).thenReturn(1L); - - HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getSession(false)).thenReturn(session); - - SessionHelper sessionHelper = new SessionHelper(request); - Optional userId = sessionHelper.getLoggedInUserId(); - - Assertions.assertTrue(userId.isPresent()); - } - -} 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 a6f3ad68..0d2f40a0 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java @@ -1,84 +1,82 @@ package com.commafeed.integration; import java.io.IOException; +import java.net.HttpCookie; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.List; import java.util.Objects; import org.apache.commons.io.IOUtils; +import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.eclipse.jetty.http.HttpStatus; import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockserver.client.MockServerClient; -import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.CommaFeedDropwizardAppExtension; +import com.commafeed.JacksonCustomizer; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Subscription; -import com.commafeed.frontend.model.request.LoginRequest; import com.commafeed.frontend.model.request.SubscribeRequest; +import com.fasterxml.jackson.databind.ObjectMapper; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.restassured.RestAssured; +import io.restassured.http.Header; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import lombok.Getter; @Getter -@ExtendWith(DropwizardExtensionsSupport.class) -@ExtendWith(MockServerExtension.class) public abstract class BaseIT { private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/"); - private final CommaFeedDropwizardAppExtension extension = new CommaFeedDropwizardAppExtension() { - @Override - protected JerseyClientBuilder clientBuilder() { - return configureClientBuilder(super.clientBuilder().register(MultiPartFeature.class)); - } - }; - - private Client client; - - private String feedUrl; - - private String baseUrl; - - private String apiBaseUrl; - - private String webSocketUrl; - private MockServerClient mockServerClient; + private Client client; + private String feedUrl; + private String baseUrl; + private String apiBaseUrl; + private String webSocketUrl; protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { return base; } @BeforeEach - void init(MockServerClient mockServerClient) throws IOException { - this.mockServerClient = mockServerClient; + void init() throws IOException { + this.mockServerClient = ClientAndServer.startClientAndServer(0); + + ObjectMapper mapper = new ObjectMapper(); + new JacksonCustomizer().customize(mapper); + this.client = configureClientBuilder(new JerseyClientBuilder().register(new JacksonJsonProvider(mapper))).build(); + 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")); - mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); - - this.client = extension.client(); - this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/"; - this.baseUrl = "http://localhost:" + extension.getLocalPort() + "/"; - this.apiBaseUrl = this.baseUrl + "rest/"; - this.webSocketUrl = "ws://localhost:" + extension.getLocalPort() + "/ws"; + this.mockServerClient.when(FEED_REQUEST) + .respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); } @AfterEach void cleanup() { - this.client.close(); + if (this.mockServerClient != null) { + this.mockServerClient.close(); + } + + if (this.client != null) { + this.client.close(); + } } protected void feedNowReturnsMoreEntries() throws IOException { @@ -88,14 +86,20 @@ public abstract class BaseIT { mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8))); } - protected String login() { - LoginRequest req = new LoginRequest(); - req.setName("admin"); - req.setPassword("admin"); - try (Response response = client.target(apiBaseUrl + "user/login").request().post(Entity.json(req))) { - Assertions.assertEquals(HttpStatus.OK_200, response.getStatus()); - return response.getCookies().get("JSESSIONID").getValue(); - } + protected List login() { + Form form = new Form(); + form.param("j_username", "admin"); + form.param("j_password", "admin"); + + List
setCookieHeaders = RestAssured.given() + .formParams("j_username", "admin", "j_password", "admin") + .post(baseUrl + "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 Long subscribe(String feedUrl) { 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 11fe4b59..3864cd7b 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java @@ -2,25 +2,28 @@ package com.commafeed.integration; import java.util.Base64; -import org.eclipse.jetty.http.HttpStatus; +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.Entries; 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 jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class SecurityIT extends BaseIT { @Test void notLoggedIn() { try (Response response = getClient().target(getApiBaseUrl() + "user/profile").request().get()) { - Assertions.assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); } } @@ -31,18 +34,18 @@ class SecurityIT extends BaseIT { .request() .header(HttpHeaders.AUTHORIZATION, auth) .get()) { - Assertions.assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); } } @Test void missingRole() { String auth = "Basic " + Base64.getEncoder().encodeToString("demo:demo".getBytes()); - try (Response response = getClient().target(getApiBaseUrl() + "admin/settings") + try (Response response = getClient().target(getApiBaseUrl() + "admin/metrics") .request() .header(HttpHeaders.AUTHORIZATION, auth) .get()) { - Assertions.assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus()); } } @@ -84,5 +87,16 @@ class SecurityIT extends BaseIT { .request() .get(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); + try (Response markResponse = getClient().target(getApiBaseUrl() + "entry/mark") + .queryParam("apiKey", apiKey) + .request() + .post(Entity.json(markRequest))) { + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, markResponse.getStatus()); + } } } 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 7cec67db..d5acb22c 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java @@ -1,6 +1,7 @@ package com.commafeed.integration; import java.io.IOException; +import java.net.HttpCookie; import java.net.URI; import java.util.Collections; import java.util.List; @@ -8,6 +9,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import org.awaitility.Awaitility; import org.glassfish.jersey.client.JerseyClientBuilder; @@ -17,6 +19,7 @@ import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.request.FeedModificationRequest; +import io.quarkus.test.junit.QuarkusTest; import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.CloseReason; import jakarta.websocket.ContainerProvider; @@ -25,8 +28,10 @@ import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; import jakarta.websocket.Session; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.HttpHeaders; import lombok.extern.slf4j.Slf4j; +@QuarkusTest @Slf4j class WebSocketIT extends BaseIT { @@ -49,18 +54,17 @@ class WebSocketIT extends BaseIT { public void onClose(Session session, CloseReason closeReason) { closeReasonRef.set(closeReason); } - }, buildConfig("fake-session-id"), URI.create(getWebSocketUrl()))) { + }, 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); - Assertions.assertEquals(CloseReason.CloseCodes.VIOLATED_POLICY, closeReasonRef.get().getCloseCode()); } } @Test void subscribeAndGetsNotified() throws DeploymentException, IOException { - String sessionId = login(); + List cookies = login(); AtomicBoolean connected = new AtomicBoolean(); AtomicReference messageRef = new AtomicReference<>(); @@ -70,7 +74,7 @@ class WebSocketIT extends BaseIT { session.addMessageHandler(String.class, messageRef::set); connected.set(true); } - }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) { + }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); @@ -83,7 +87,7 @@ class WebSocketIT extends BaseIT { @Test void notNotifiedForFilteredEntries() throws DeploymentException, IOException { - String sessionId = login(); + List cookies = login(); Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); FeedModificationRequest req = new FeedModificationRequest(); @@ -100,7 +104,7 @@ class WebSocketIT extends BaseIT { session.addMessageHandler(String.class, messageRef::set); connected.set(true); } - }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) { + }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); @@ -115,7 +119,7 @@ class WebSocketIT extends BaseIT { @Test void pingPong() throws DeploymentException, IOException { - String sessionId = login(); + List cookies = login(); AtomicBoolean connected = new AtomicBoolean(); AtomicReference messageRef = new AtomicReference<>(); @@ -125,7 +129,7 @@ class WebSocketIT extends BaseIT { session.addMessageHandler(String.class, messageRef::set); connected.set(true); } - }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) { + }, buildConfig(cookies), URI.create(getWebSocketUrl()))) { Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected); log.info("connected to {}", session.getRequestURI()); @@ -136,11 +140,12 @@ class WebSocketIT extends BaseIT { } } - private ClientEndpointConfig buildConfig(String sessionId) { + private ClientEndpointConfig buildConfig(List cookies) { return ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() { @Override public void beforeRequest(Map> headers) { - headers.put("Cookie", Collections.singletonList("JSESSIONID=" + sessionId)); + 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/rest/AdminIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java index 138fd821..c6e53815 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 @@ -9,14 +9,15 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import com.commafeed.CommaFeedConfiguration.ApplicationSettings; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; +@QuarkusTest class AdminIT extends BaseIT { @Override @@ -24,12 +25,6 @@ class AdminIT extends BaseIT { return base.register(HttpAuthenticationFeature.basic("admin", "admin")); } - @Test - void getApplicationSettings() { - ApplicationSettings settings = getClient().target(getApiBaseUrl() + "admin/settings").request().get(ApplicationSettings.class); - Assertions.assertTrue(settings.getAllowRegistrations()); - } - @Nested class Users { @Test 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 c5c6c2a5..6b561c2d 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 @@ -9,9 +9,8 @@ import java.time.ZoneOffset; import java.util.Objects; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; +import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.eclipse.jetty.http.HttpStatus; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; @@ -30,10 +29,12 @@ import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +@QuarkusTest class FeedIT extends BaseIT { @Override @@ -69,19 +70,19 @@ class FeedIT extends BaseIT { .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) .request() .get()) { - Assertions.assertEquals(HttpStatus.TEMPORARY_REDIRECT_307, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); } } @Test void unsubscribeFromUnknownFeed() { - Assertions.assertEquals(HttpStatus.NOT_FOUND_404, unsubsribe(1L)); + Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L)); } @Test void unsubscribeFromKnownFeed() { long subscriptionId = subscribe(getFeedUrl()); - Assertions.assertEquals(HttpStatus.OK_200, unsubsribe(subscriptionId)); + Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId)); } private int unsubsribe(long subscriptionId) { @@ -212,20 +213,7 @@ class FeedIT extends BaseIT { void importExportOpml() throws IOException { importOpml(); String opml = getClient().target(getApiBaseUrl() + "feed/export").request().get(String.class); - String expextedOpml = """ - - - - admin subscriptions in CommaFeed - - - - - - - - """; - Assertions.assertEquals(StringUtils.normalizeSpace(expextedOpml), StringUtils.normalizeSpace(opml)); + Assertions.assertTrue(opml.contains("admin subscriptions in CommaFeed")); } void importOpml() throws IOException { 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 8f8ea1a0..8a52a003 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 @@ -12,9 +12,11 @@ import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.resource.fever.FeverResponse; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.Form; +@QuarkusTest class FeverIT extends BaseIT { private Long userId; @@ -26,7 +28,7 @@ class FeverIT extends BaseIT { } @BeforeEach - void init() { + void setup() { // create api key ProfileModificationRequest req = new ProfileModificationRequest(); req.setCurrentPassword("admin"); @@ -73,6 +75,7 @@ class FeverIT extends BaseIT { Form form = new Form(); form.param("api_key", Digests.md5Hex("admin:" + apiKey)); form.param(what, "1"); + return getClient().target(getApiBaseUrl() + "fever/user/{userId}") .resolveTemplate("userId", userId) .request() 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 82201f6f..2a9a6958 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 @@ -6,6 +6,9 @@ import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.ServerInfo; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest class ServerIT extends BaseIT { @Test @@ -16,7 +19,7 @@ class ServerIT extends BaseIT { Assertions.assertTrue(serverInfos.isDemoAccountEnabled()); Assertions.assertTrue(serverInfos.isWebsocketEnabled()); Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval()); - Assertions.assertEquals(10000, serverInfos.getTreeReloadInterval()); + Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval()); } } 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 3eccde06..ae077b1c 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,30 +1,38 @@ package com.commafeed.integration.rest; +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.junit.jupiter.api.extension.RegisterExtension; import com.commafeed.frontend.model.request.PasswordResetRequest; import com.commafeed.integration.BaseIT; -import com.icegreen.greenmail.junit5.GreenMailExtension; +import com.icegreen.greenmail.util.GreenMail; import com.icegreen.greenmail.util.ServerSetupTest; +import io.quarkus.test.junit.QuarkusTest; import jakarta.mail.internet.MimeMessage; import jakarta.ws.rs.client.Entity; +@QuarkusTest class UserIT extends BaseIT { @Nested class PasswordReset { - @RegisterExtension - static final GreenMailExtension GREEN_MAIL = new GreenMailExtension(ServerSetupTest.SMTP); + private GreenMail greenMail; @BeforeEach - void init() { - GREEN_MAIL.setUser("noreply@commafeed.com", "user", "pass"); + void setup() { + this.greenMail = new GreenMail(ServerSetupTest.SMTP); + this.greenMail.start(); + this.greenMail.setUser("noreply@commafeed.com", "user", "pass"); + } + + @AfterEach + void cleanup() { + this.greenMail.stop(); } @Test @@ -34,7 +42,7 @@ class UserIT extends BaseIT { getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE); - MimeMessage message = GREEN_MAIL.getReceivedMessages()[0]; + MimeMessage message = greenMail.getReceivedMessages()[0]; Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject()); Assertions.assertTrue(message.getContent().toString().startsWith("You asked for password recovery for account 'admin'")); Assertions.assertEquals("CommaFeed ", message.getFrom()[0].toString()); 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 0a903fb5..9e6229c6 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,5 +1,9 @@ package com.commafeed.integration.servlet; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; + import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.junit.jupiter.api.Assertions; @@ -8,10 +12,12 @@ import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.Settings; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class CustomCodeIT extends BaseIT { @Override @@ -30,16 +36,16 @@ class CustomCodeIT extends BaseIT { getClient().target(getApiBaseUrl() + "user/settings").request().post(Entity.json(settings), Void.TYPE); // check custom code servlets - String cookie = login(); + List cookies = login(); try (Response response = getClient().target(getBaseUrl() + "custom_js.js") .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .get()) { Assertions.assertEquals("custom-js", response.readEntity(String.class)); } try (Response response = getClient().target(getBaseUrl() + "custom_css.css") .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .get()) { Assertions.assertEquals("custom-css", response.readEntity(String.class)); } 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 10e97246..b12aedb1 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,32 +1,34 @@ package com.commafeed.integration.servlet; -import org.eclipse.jetty.http.HttpStatus; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.hc.core5.http.HttpStatus; import org.glassfish.jersey.client.ClientProperties; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.commafeed.frontend.model.UserModel; import com.commafeed.integration.BaseIT; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.client.Invocation.Builder; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class LogoutIT extends BaseIT { @Test void test() { - String cookie = login(); + List cookies = login(); try (Response response = getClient().target(getBaseUrl() + "logout") .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) .get()) { - Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); + List setCookieHeaders = response.getStringHeaders().get(HttpHeaders.SET_COOKIE); + Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0)); } - - Builder req = getClient().target(getApiBaseUrl() + "user/profile").request().header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie); - Assertions.assertThrows(NotAuthorizedException.class, () -> req.get(UserModel.class)); } } 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 cb6ada00..60331b75 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,6 +1,10 @@ package com.commafeed.integration.servlet; -import org.eclipse.jetty.http.HttpStatus; +import java.net.HttpCookie; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.hc.core5.http.HttpStatus; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; @@ -9,9 +13,11 @@ import org.junit.jupiter.api.Test; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; +@QuarkusTest class NextUnreadIT extends BaseIT { @Override @@ -23,13 +29,13 @@ class NextUnreadIT extends BaseIT { void test() { subscribeAndWaitForEntries(getFeedUrl()); - String cookie = login(); + List cookies = login(); Response response = getClient().target(getBaseUrl() + "next") .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) .request() - .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie) + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) .get(); - Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus()); + Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); Assertions.assertEquals("https://hostname.local/commafeed/2", response.getHeaderString(HttpHeaders.LOCATION)); } 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 142b0c27..f16a5fa9 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 @@ -5,8 +5,10 @@ import org.junit.jupiter.api.Test; import com.commafeed.integration.BaseIT; +import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.Response; +@QuarkusTest class RobotsTxtIT extends BaseIT { @Test void test() { diff --git a/commafeed-server/src/test/resources/config.test.yml b/commafeed-server/src/test/resources/config.test.yml deleted file mode 100644 index 209f3efc..00000000 --- a/commafeed-server/src/test/resources/config.test.yml +++ /dev/null @@ -1,138 +0,0 @@ -# CommaFeed settings -# ------------------ -app: - # url used to access commafeed - publicUrl: http://localhost:8082/ - - # whether to expose a robots.txt file that disallows web crawlers and search engine indexers - hideFromWebCrawlers: true - - # whether to allow user registrations - allowRegistrations: true - - # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char) - strictPasswordPolicy: true - - # create a demo account the first time the app starts - createDemoAccount: true - - # put your google analytics tracking code here - googleAnalyticsTrackingCode: - - # put your google server key (used for youtube favicon fetching) - googleAuthKey: - - # number of http threads - backgroundThreads: 3 - - # number of database updating threads - databaseUpdateThreads: 1 - - # rows to delete per query while cleaning up old entries - databaseCleanupBatchSize: 100 - - # settings for sending emails (password recovery) - smtpHost: localhost - smtpPort: 3025 - smtpTls: false - smtpUserName: user - smtpPassword: pass - smtpFromAddress: noreply@commafeed.com - - # Graphite Metric settings - # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds) - graphiteEnabled: false - graphitePrefix: "test.commafeed" - graphiteHost: "localhost" - graphitePort: 2003 - graphiteInterval: 60 - - # whether this commafeed instance has a lot of feeds to refresh - # leave this to false in almost all cases - heavyLoad: false - - # minimum amount of time commafeed will wait before refreshing the same feed - refreshIntervalMinutes: 5 - - # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser - # useful if commafeed is usually accessed through a restricting proxy - imageProxyEnabled: false - - # database query timeout (in milliseconds), 0 to disable - queryTimeout: 0 - - # time to keep unread statuses (in days), 0 to disable - keepStatusDays: 0 - - # entries to keep per feed, old entries will be deleted, 0 to disable - maxFeedCapacity: 500 - - # entries older than this will be deleted, 0 to disable - maxEntriesAgeDays: 0 - - # limit the number of feeds a user can subscribe to, 0 to disable - maxFeedsPerUser: 0 - - # don't parse feeds that are too large to prevent memory issues - maxFeedResponseSize: 5M - - # cache service to use, possible values are 'noop' and 'redis' - cache: noop - - # announcement string displayed on the main page - announcement: - - # user-agent string that will be used by the http client, leave empty for the default one - userAgent: - - # enable websocket connection so the server can notify the web client that there are new entries for your feeds - websocketEnabled: true - - # interval at which the client will send a ping message on the websocket to keep the connection alive - websocketPingInterval: 15m - - # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval - treeReloadInterval: 10s - -# Database connection -# ------------------- -# for MariaDB -# driverClass is org.mariadb.jdbc.Driver -# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for MySQL -# driverClass is com.mysql.cj.jdbc.Driver -# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC -# -# for PostgreSQL -# driverClass is org.postgresql.Driver -# url is jdbc:postgresql://localhost:5432/commafeed - -database: - driverClass: org.h2.Driver - url: jdbc:h2:mem:commafeed - user: sa - password: sa - properties: - charSet: UTF-8 - validationQuery: "/* CommaFeed Health Check */ SELECT 1" - minSize: 1 - maxSize: 5 - initialSize: 1 - -server: - applicationConnectors: - - type: http - port: 8083 - adminConnectors: [ ] - -logging: - level: INFO - loggers: - com.commafeed: DEBUG - liquibase: INFO - org.hibernate.SQL: INFO # or ALL for sql debugging - org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN - appenders: - - type: console - \ No newline at end of file diff --git a/commafeed-server/src/test/resources/docker-images.properties b/commafeed-server/src/test/resources/docker-images.properties deleted file mode 100644 index b40aa81e..00000000 --- a/commafeed-server/src/test/resources/docker-images.properties +++ /dev/null @@ -1,4 +0,0 @@ -postgresql=postgres:${postgresql.image.version} -mysql=mysql:${mysql.image.version} -mariadb=mariadb:${mariadb.image.version} -redis=redis:${redis.image.version} \ No newline at end of file diff --git a/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db b/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db deleted file mode 100644 index b11490bc25ae368a0b1e2fa5130b571280159217..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeIbd2}4rwJv^Y=s`nkvTVyTwyImRrIx#@x+a;SyQP*bTaz_-1k^RPEs!Ol0poiK zv~8w@8A1XvL(2wi%;bQ1Et?pUdw=h(_1=2_ zSZh^Rb@foy`<%1CZ-0BAqhwR*p)4%qu) zQyn#pdR8_hfe%QE60eO94sTLN;j`zO!y|oz1NBmEyx$zvH=4usMj+1dESHH+{GG7C z2@9OCzzGYSu)qlmoUp(N3!JdP2@9OCzzGYSu)qlm{J*upvSV@ovHgko|Nm`+oVfZ4 z3!JdP2@9OCzzGYSu)qlmoUp(N3!JdP2@9OCzzGZdU$DTjB>%A*fPQnJUZ~xq4%M@Q z2K)2s1xXLo4yhZ=k$P6E9UdGUtq($Z0L$^ZuGWtBZ8Gb{t^=vU6bAutB>vv6?z$8om||Er5^f|UOPTUSJ{5;L zj#x1nH^f*itxAGqhVn^OQWCY4YKm;ElvYi}Fk;1oX(R+v6Vya4tqCTthl-KL)Wl9D zHBm_x6S|oYXwBdhEmm4$fK6%#hd0!1DDX7`pM zv#d$gnFYj_a_p?lFtobP!Oe~8sEVm_(&a`I2roZ_$jzHL9V0k238!n~47~hwBHJ@@ zdIF(RD`OSaHrC&-UhvfEc=;tnenH{_Ov76@5VRwqrd+3>QQz8(y2zGMGdw)3Zo!@J z5fmLu;N@=-Bo$hVm#-ll&d?eLKC_-En8mEdZaCaX%qqlgGX#fyL`G(4O%kPC8>T#W zAd*1@BCxjx9v0Rh3j0=vYH;alMB;_p5gl5DeKojbHF7YE;KhaTBBJd#I^mm3w!>SG zJ|Gvf?bxyX1>2F+TF4_eb003b-+miB5Hxg0;^$pmbUX$jcgRhdtfGb7 zv>xPnJ>e$BxSDVis+Pzn1P&TRP?GtCCM5+*(*z|{OB$R8O=NI!O^)T0(C3nFq{183 zKoS}|$V#zNQq>eV8&4LKnySX~X_Jk|#ZW12DnUVji^T)77KC0Tns%QeBqQl2<+zzB zB@{CWx5P!~(xBwjxg$QUp>MVM*`!HEY-dm7vy|jaOGqw+9 zVGz)&psL_5!9xo6NF=qOJqEOZBE_Ih;)#4(=Ys(wluzkuPzK9_d5bB7g$6to#-SQl zVPqLf97d;`HUqK<4NI{+3{MzTG8Wm zTQ!3KTQcX?gQBF0qTqJwqRNXpfLFGr3bJ6bZiAKOAhRFb+-{c{;H02C&Upt)Bu#S1 zgV1qJ#&Oj@gXGn@s_Q052UTM~|~hh5n$%&dn2U*?DN zZd~$x=(%O<)#ovD02PGRBf=`V1$jb#`(3kUc7^sg&4veOHjNOX(Ij(|lZKljX_EOF zs-1F{=N*rNX%L*EY9|-;6SLfl9ISfr?+I zQBu1JcRobSnTuTyO(0zMcxu6?#N48Z)3FVCn~1DzOq+PQN)*k*w2EyQ9U$Bpm{9TZ zGl|04m^c$taPlm|lY^-kgAkX>_?l`N)Vit<6aq(VRfozV{exPk88?T`0o^Q%?#0W0 zL}X?$KfquSKc0U*)oKE~w_io%FqOnXWorP zMZS3~LavzS<;-WvBJ!T15`b^o;dHq(GPANhIk|cH1+xlg&zW0Ty=!c+M;lRD(D9cub44pNE zpdbO-G8J*$HJG&lI_e2rly zKR4VJZCDm=TN-U?U+SyMWEe#Inj3u#Q%H4&8@idNvsQ&W*EY8;t>OfsI`#tH*3;5b zo2Yh#d%BmkcfxpZH8rW@EIgp|iQ8 zyScp$XH|s*$!b?aduwa7t-A})sUW+a=vfZq@yu3MEI&28l2S?Ou6S8qV!|ml}!2M?DJz%>CcHq#`|-s ztGhGo&Bo*gvSURr_MC;O2NUOdo5;rK8cZ!oyqaY^wZL} ziO#j9k6Xo+u|>GxX*|c8&OHqUr^u(lmet^_9=3`nV^@@Sqi$vl7yqo{W1_J)QRjVl z`<3GdnAt>UXG0=6$8tRq>%#?qPEnh1K?SqhqW=y}(-iJ$scf(KOUijo>>Q%GjR{O; z7sVD5JsqXpp@)v3)8P_}%5u^6r#_o>Eoa(&r!idE8Tu6U#=KL`!W1g-&K_Tv7-UY3 zoq~Mvn_OK?Q*&omcP;aEq6O1)i0T^eFVQCxmrWMz+0}xYt@%$m8`@i1!W~_qv#gvw zsodCP)npS?omb~>T&p&uQg z&k9}W9Z1nVF)`Y>?ETd2c3(Vp3Pz`6k|U}Y`_4uePwYtLob6cF9EowC_g-boQYzqaDt3>cUS(vDn8_D=;`WiZ*Ax* z9mm80i&~G-2Sm7$SxL6GH!^o)qWYs*PbWF=eiZ$PIZSmeYhN9bypLZBJol~0t@@9) z{R+DlRp0)F>2Q5Pd!`u)`o2aT?jEuEV&xHUS$Hf@X-5Il&SIl>FN=j@i>~znD2Q{ zCL1{pJD+HHc#8O8;!!lG>J*fCFXt3YFMh9Z33gOlb#zlSu9$mgFUjwsp{?hi& zwY@gH*(i3@PgK*z0F)2W$&sw%ff74cx&dc8*EY1bbu(^eC1Haj@`YO6;BW!N0M{9c zS%Yd5^ODtbeGI^;jhKM>-;ASZLwB+~8wPJndsk0q6re7OshV#gDtHdEr?UlTFCqg$ zd7>um#IdIxlV!&sHfuS-au_uy4^cKlz**HqFo5Y{Jg;K>GO{_^g*|PAB2IKTTceF2 zIYljW5L1h-%;m8r+HElOt>RMb2vDn=8@n0H`z#e*8toh(#tt97EZV$unfKLX zsqI4M`|d-XJL}WH=G~tLwiKz%02|pW1yh-Oe61;JRb*vGHGL8w+V_cYOGHS`yPO8r z0dTfE4QI>_Onr}7(c6%X9Xh7&0{8stq~k2?e2AbIFz=!!3=e#%Hc4MY**vL~OT3L1 zmJSlv6UmmzoWW3l>}+rGl};9I2vv~4E>fP4N>842{W!GFpeLOVdtWTOhVV2eZyYDA zf)}S;W1%&nE0_lp?;!@9{dtz>7vLeS!k=NX0+UxK&a?_2q#{1r%X|;LoGOS+I~bgQ z$#ljT?PHqSJ4-WBTll1D$6B0m1I{>gn%GZwMw_CYTbV?2se1WWLI}{WOCd7WKrl; zoOjaJQk*H!4ejkKnsHW9sEzIluZlL}%%B4>ySZ~M&MYEZ!)wZhsc7e_aK-hQz6G9l zgz`~abk;O=U+G3v>f3>SohsO2+z+8}cPUEE-jUNNHa0{RB^YI8R^Ww{Ye$NjYWO^r zbsKg?cHV~ARKabST9tC|L}g`B)VB3|lhi{gGPUdZ_$;DPnReDh{vP|gO~NK1&?)x@ z&{k`1AQqU+KYWDBN-RoVy8U43JS$^C(mQb%L07<)dy{V;aq(o+ir&}(?}qWn5tkFY z-kPX;-+2wHWDL}Y^Ni;x7awT~2ODKJdG@sH%Fx-C>(0{?cHsFF*M(0)rQwL*Dyi2bS{VG;Di56QqFjC9u56Qw< zGiqCO|A)wp7L~6ju5gT>H=VOSw24tK?g}s8{>Sp$U{trq7vFjQXE#--2(M4B(m>pQwm)sJ1)CGXo+|&%MnI9m;APig7(K3m;YPWS>{Hqybpu(q+xdSG2Tt^{gscy%y1|P0(jzGbY_B zfSw!!k=&yJknMQvz@;}yMshBH?&K3yeohAx0CiM6mGyflO zcGMueEx^F833Z9wV{Uty8AhVEswj84D{U)obMO-?)T03-t)99(s%D)w|1I72{#HaniF zik%vojjAp87ntD2E3eudU17lpSaGcET*vX=41mSJ}hbUfKBq3118%87zj6BJl39B7I(vH>4y(WfPE zuyP~Vb&6HI9J{`-=v7GZu0q{b&ZvW@Z!y(U%NW)?=eJv9yPI)Oi%IBai$8jsbupmDpXobpmGu zY>ai+0WmInkAP^l_aD)0a?Bp|{fE%M{=2NMg+cex;_ zfgqo)#OS|lSY6`VjGjytY(@>tP6)~-iym+t8&AKyKjr+l@T;LKCbJ&HuKH~S5I;sH zv!4zv_x?xt#qinQuS%arC;QF@Sk9Y)7Y6RT4Oz-tgu`A-VyW~I@Nm-`o2|g z5q9jD&aX!u7uREFUGmFt-nfWe&n3N2okY;5F;}Alc<$Bdn0wac<=4k9js1R#z7(T5 zIO8%*2FF{xi&ELnrygipk$nB>Y!m|aJ~F;P`J2$+Qn}ekOgXdvft9=4lbL1S@+b7o z4#3B7_p(q<=0BjZ+vi7NuZSMAa>Vdow$WDp6RC`qmQx8ehTE+yIhB8fm3>-hx#d<+ zgqcA1;hc%5hOFW*LhD0!T8=+Y%*I(ipWv)HpNAsYy&pArKSpPaf1Gl?iL=&zKm)^m z9TT@tI&MRajD}j0T58@(f>&^kks|IU=%}|3J#7{Az3=!{Y8Ll_tJ?C7LBouHuk6W` zmqSg=Vf4NC`H!c_2g4;6)w}Br%k``At;W6Hvk7`Bv(1XGa2^FP%e5j>8+ij~TxSt0 zChDy@D?-a7gJnHa#3N{#FOJUH8ON^AVfcRsikp%ub9>73>Jjq!6xAA*ti`ReQWUJ}h{U3P%GhvjasBWu@{iNqN^nc#=b0L0CDtp&RdvwUuqnwsJG$ zdnY~rS^nA)bhSmzVdjQbV|o@0z!ly_Xs4C4=!iqI@@pz0_k14y+0GZ5lHT2yjQI+g z3z*6yd1s~E3zJRytcq6&x+U$lbLy$)#x=c|zQ<_`20%V6Cxi<=?}2oa-&omFVs*$2 z?X#!@@O5jM*ko?@$Fsh`1sk=COP^v6WAf~Dtb+0u-tBqbPUmuzoxnxh!oTLca4duC zda?*kg;2%~Z>INI#=7+0Gxn1?bLPyS>u&%xas9kIb6+6mkg0)RlLLaN3I-&qIg>Oz)rk>jx2pA<%xK6_(xz@Q zPYHK)fGilcr8dwtyFu~mYDCL}b(~N~gDBc2G3`#4W7z=LmN<>_$KxjU?nPH5_97pq z_m-G4q<%q<H=gzM92q0E*rAXg2uCk8B`6zQ-anGKz3f_ zSV)=(p31}^&fQJV#ESc@oG&Ntb&QOvqhljhX%E`T+)Ew)ePsov?=9hUNsFrnCo2NW z3lgjH2IO^QR)(CM7Uwlwbgu(6)9#+GRPG+sk@yh#OK(GlgFbT5aX5KZ@{f#Z*mpC!kGYweNM=@H z`exG91@E6A8>f^-DP307(&ZT-HfurA)L50tux2fFfy$WX9>#4`vu`9v%yTBapP~nu zJ=B{wW>#Z*PrhJ4E-@|$(9KL4@=(Em_md2kmE$s`=yiQMufeggPcw%H%3lFg+wK^% z_4*O)+56NV$>G8NROv0~0CNv@Ne)IX?`ITR9(~HHp_^w|GB!w^Bw2nL8+aZ(c2?@5qBBldM5CilA$-K832rOaJ5% zxS$FtO___wWsfg0YGo~ZcQ)&4Q4Pa?wz*#eiNa zLM|I>AADC4W_&4I+D>}DB$&R8j^Vyd>hP8o=9YKK-!ApPOL4>!hN*ToK?y_Oh5@G9 z(=a$NGCHjGL2uO5kwj`ENVgJE%Ty4_MG0osil`812=Gnpu}G)(-8W#E=S`NLMQl6d^N}w(>MS}@tVu!9P$W%*q@O73 ztSBgATnU<7kgF&rINh7rR*ITK1Bi)@#=drM80#~ZsUsVEV+E+za^6^Rco_rtjy)%n zV?#z+Co*vDlITO3`f%USC@j{|+h^dcEl1ETIQK%*=o?vH?l5}$)sa!0bA^?cAXg2W ziPtNBheozuGF@~tVxPn4I`2i)BVV}}Tl7WQQZTM)T3n2qdXO!>HA92)64_uiC0@~1 z@i)S=G%;kkwtF*CZ`EVu@Rr`15v!opLb<7&m6S0&ID~6nKtE<)pkMjiP3K|yMId$M zj}eMeu{xL46;qLPSZ74!O+izcH)tVXctW8;ocX-P{2I?Xcm&;Pd9O!1LI{|^p-zj?0NZ+Wjnhr!&*=jYS2T3W-MD|$P_ZEfzV{d7PNNb%YN3i3%xX}SL_ z7b_X6q_X;=1B9oN8E`ZV_K$5Eu*mB^W}K*lImPWC8XMW@-w3Y3dpCNPxtqT1wfS@* zrtfBUQ1Q58mcHqL%&=@I)f;H28Bznj9tST;a^MW6AG`O)J1tK$1f`1;H`AkoLw!01 z2Ks#RRx}&aw-Q06?r)wT%S&>c<)pY4m-*`T8M+))bdfdU2Clpn^sv=a?2{uB0& zqZgR*@JB!=9pg?j&dQP*sOb#=@QS*3nM0046;q(9gwzxPmyZ(xKx0?Ew?EH@)>rdO z<`(@cDG~M9qtxzi3x>wD{=Siortv=kD|-xBUp(}A$)|_C^xSW3?D3T-^{fe@r~PAu zfX+cBmJ&AuDwL_Px-1AfDTrq<=akIYnHjb5H4Uvc7-GHIbB z-jHK>iverz*bVV&avT+dFLS6tQ`0r?J9A_`E`bj;;yef4KL{NwC`qD>E63GH7T~vk zXmF&@9`E+WUrQ#@e7JO;6j00n_!FKBKs}nMNWM;5P)%87vK(&|`%lBvWCbO5m<{64!V1KD$sIm?&PY7tHW(ZJ|!0Tc_ zP!&NB26d4G#sY4yyW!1A@^?p3%r}O5CoY}ze27+sPWJc*`;GJv>Klj;j-TNnE<8U zt?0evZxDkWzwtdgo0nK9pAvbf^;3&Tk7LwsI53I8C!h9)c z1^^!fIjHdp%Nw5VMF05NVSV!--cmz8Ys&r%sX`A4>S#&^qoANRf9lx1O-T`ZUR-Q(va%C zk<FJvMA)`|Cgljw8i2a#%P0 zFFTXcpfJnVa3uRHRRFKP;Z}NRc<{U}<$KU*RW#+xMt^0->EzZ-8hGe96%YiYY=sN> zv|_Mqyy|xpxW!=QPu!*m1kMO}T1!vcdSr^sqtk4@;=h_&$KRorFk?f@7US9F*z@bK z6ZtDPXAE!g+f}uLW23nGcj#;8cQpRoM*~6qUA77JqXAV>SUo5y)$^P|1;{unRs7hY z#{+SJ!>9kw*yelBu}STRWxRYU8h+pR1#!vY$}_3gs%RjmZ#Y;};*CJnV=jd?0yg9J z?)QkA%Bi9zn4A{y3|UzZn}=-PP*A+>iz6;3^fRD`4;`sq82geKQcJr^J4tnOe+tyZ zA246i?;XjcfwF#CBI&vTRj6?usGtt0g}qJY^Wy!vKr7dIS??K&Ldd<-Q03t z4LRJ~_ikpHIrsKWQ=X;d8pDk2)#J3^jt7_(ANY#UZOkD$c|A=7WjzFSle}ELI^Qs% zo+&7DdQgo60gdx&Ek}t0FMpUnilS9Nrq|k00rM!%`Ke>=(BRNmfA#gr`;b2x!M+F3 zIP(BKalZouVETbvLxb879ZIU=yuilQfV!Q6m_T22a>a%3<`HS+bVtiy&$p5D(Rn3y zy6vAORWx%EK!lR?osW(9$6$Q_tFV;#CNBL3K6f7eyu`jA|2MuX4W9ueAV4f?s3KU7 zH90{BP%^>zL0RVI*_>dHp<_{JI_LzPWUAZ{bgmgT!KH<+&WxKQ;A+6F`8n*p3f-B! z3e{oys$xzSRA68bCYW(Sgkc4g)`Z$Zh>pAkw5of4LnKCl$qoXO9m`4MG5}4l9W-v~ zjm>h_&H#3-xWI<%bmng+4vT^}#rX})tF+Mz9G7_mI4-w5yaQLRusm_P+XflSuc19& zjlS`XOd5!*mJ4yKqL~n$0ee!RYRCI^hG@zn_&YA3_*{f18al_y?^V6lJOg`Pq))eb zin<&5#|;|-?C?6}mC-%P*P)_?ejRF^!Ov;nIavXU{h$IbsLBxT#p4Fny!C_yG$V10b9z zY2YqIIS8?*ZpuN`)0nt0<&A2Z z$Bvh%ILivYc`gnV1PH0)zKR^cV&K#ev&)=ec$yLbm@iEdPc^Pn*G?67r#zpa^}aFM z4>A9yXZAaf$|jcgcJwgwdiZt}1hZvmP*VsFM+#`=_c|0E$`#EDf0o4BafPYNaZf9A zUdsJ({y&Q!DuZ8bI0;XSU`eO+cB0_bg zpakPOP*k9Wz}F-(E-R)Wd3=UR^qW98e(~5edJB8@pf#7~>3-B#aSyR+kZHz6_aW|( zf+m>z=vO|>qD4&KSE5NA5IX_rT^wrQ4TTp4sJAt2rzM-5!rD<}bQ$Vyy3``i2t6=K zx$W@ray;jCTu@rJ9~Uf`Q6eh-#+0VAazmFh3lfbCQ*<=Egs}75;iVb-<-2qXvze@A zbkNgd^U3(xy#`G}8R~R#lZ7ru&v-AU6JIT&C5yf|JE-br zP>Cz70vxAozY{z(FKS|W)D1KX0%FfN!HiD2Gba9Ok<66GnY-@|^3b4TEvf z09I_U{ADs=a^5<(!~=U{Sx*D@{M~eV?|c8GHP~|)b%qNlo3Y`_XCZ=mmcDFP4Xt4M zStM7Aw5Ca#UiNn7!g?(9o20NI30!Df&w+9HdvlYLt5ASj7>Sf7px< zniyb<(l@;OwG>F2Tq0hlUqKLe0XV%Ajcp<(JpTq=t=fT}6nx@q^0$N?)S?p!-; zo~v#eI=C_Z(@7{heu3E;k9xN-5&ZBi*};` zbB32Blz8>QEJ0FDBLGnVXP7mACE`l$YlLTc%5yFA3K~tkn*0gQ|4j=05PKlm$!v81 zHyQ0Wk5oN_UQhl9Iu+CZfw-E#Q>>wwi>?L@0Adf5lY?ySK@w!N@>?n0@H}wjlxJcO zLUGv<$H8e=!@Ct(aLefQ$Q^V#{JZC2!aovtf-pyZKd&Q&=Abvn8_;PM-4O6*LH#qU zgJ1-8&w{s;Tfe#|VLL_uVYXQ7BiXDufQnP+RuPey#x`m#Y}cu%TZ z2PNAuQRw4!OVcDG^S7sE7Q8^tC!d&Ka$E+st-WI@12#p8b#)M#00ZI;~vofecGH@qo$ekv4KzCq?qwa^Bhk3+9z-@1s7expi+ca(sP4CTTC z$vtjs{R)W7Y!IkHLuk};-IVi>G}FhM0~9_;V;j|{;KISc|{aN=h48FMpAdI!)_P@zj8&M-J$3;-_@1PBm# zUezQ;g+UTjpx_^(a0X}H;wx|+i`8(>?b)XT+WQTlS7ZI7e(c`|f!4loD_RD;1{5aX zC9DWaVd)uj6x}9GFlB;;scS|+hly+|aJbUPEOI%XcMs~>UI7e3-7vz?rLS3#WBvSFZ3-?F}SpA zN%;J^{~gjM>W%X;9h?E$%Gd%MXt#gbFqr{hGE>ZM#J0qkiBkzkjfBskY|4$ZhioLC z_EexVeH%%Ce9ZSIv&(H`PdbPz=ZQ^2miJ2ZxA7~{a*MvQL|`?Lra<2mI0%kxIa(5Q z30Stl8XEAm$?UDDCo%~#Jb>`tJOgUw`}$48KQgHIsr~*zbJ_H~*O2gwYlzFAExm@i zD1%-BqTno+RiP&bI0a&N9`r8AFA2VBH>my)dBglYpTc}^%Dp&t@2+b#5kKoqZ8XTcbc;oA%LcIpLI93ef*?sTnfUt+Ke){?=Hl*hKC&|^ZPY2M|KrLD!(=tn8C2)!= zXGlSg1;de#dqY84I~pwu6MnsXHN9cb zPASb*5%)fkK-VT8L~WRUFk25QngF2{D?z%Ya!#&c*ebOSGO{j#loY`E51R9-naV+& z^I)zzY;GDn*Ys}~9Nf@v4)s^vRQ;k0;_ANszR@jK(Et*k8bCi}9&5M_wPX4*r1|DD z`y4=}G*PJjH=wbSY?$Cxq>3NV1}}L=A}5UxB7MWJVN?-UzDM0O>3A4>6iogc=hj=E zpQ9e{Y76}wZK`?^so47!x|#WkhQtruf$6VGQGpTD*NV0zxRHNcx1{mj#pJ!o-)*1=^;Us*>y#>Up;rQFP;0?JUIOR+|yHJ4Z6i! zM_;oqlU`}jby-pz1PVyKDKLq5(Mz-lF^g2KXTqG&00b-n-lHYX-|0Up(F879Q1&v} zYo1r55hEitcT%;?VE?Y?FKxgoDg;ikds^Vi36aP^_-pZOwywp%}pkz4o%K4a-Q2~Zf+ek zaGqp&K84)tZ08uv9o=-+Na?~A=S{-A74pyMcIMCYjW2oVE=>O!5n!aQcsCy8fN^gLO94$<(kqI(THRi1_!24G?@iK=0c# zm+poToe<@^YDX{t+67x-5vH_g98z!^99OI;2*g#-`I`ofzW5e za&)tIz&vlXcVk~KK=znLUxLwtph?y^MrLvbC6F_CjP9}J*tYw82Z(dV{5Ma|9Y_2$ z@gjQA>!APggL!n1MLQP%jZo#tK0=CDeoUJ|@C{xk%meh_@8TiDA$ndSK61A=XpR|e z_Cv%{ZG)q2iyr-uOm17P-#)QFMg0!D8gRBTNgpyfT)P1SN_f^Fwc&=qjWrJ;Qj)wjySm>fpak((P}Z3j2~Dp28%!e&a*N+`8amYXu<^;oQ%)7KZ)f|}+P@(6vb)ND=opCi z!5Z_C53Hi|kuvcRdW?A}V!<2|`XQv%oevdgT;(H3@Dnsl#WM`9goX@RP;6g{OuYCZ zwEE&m=x%Tsj;dZ_V9d^?{5*L#S_>}IZGig$>6?|S&Oqj(IWP@QlPP0Si%^R?szh8CBzi|f8_3>` z>ZHSn-8$(p(CR(m$N@BR*)1Q08JOL5y%^YrwFYH zyJx=wCrHGF{?$qZGYb~v7Jl9J+rllMJ=y*1%7KF7jejh>d-FZvT=x~hKRtIF^VQ?_ zH`!$*U%c>n$>D2Hf@e#@*Uur~d+n;0ye$RkeLnrGnf+UNMYR985FnXttZE`krh0Sg-k8xLC$ zHUTy<=}KM*Xa40@0)nr7{raQVhyNsz{?8Fd=l<6R=hZ@Lz8)4PfV3gY*MhUGKQ^lr z(-IPJj^uzzrBJ+KKP`_9I$%a2%q&ct;XFDWHCFCAHdz$YXCMjZoZ*lSUX80U31{6v zutDsRK*+ZzvD#ol2rQiNIw-|71)MJ-4(CcVjD0ASfoD8CgSpjlTrddph;r|xL5&K^ zIR70;l;YgMnHlp?Yl14Cx|g#3zLjy`v2_`CW;@f##5&$`oeTG#A-U{z9qeTtEb0i7 ztz4%A5(05u!}O0ZC4u(k8cd=6B>0|G!9o;Fx%MMIvD;x&b;YpqPleCqol0m1gP@Ei z{ejb|szv&K0g4h0s{sdh`_+Q^L1kIW5!o8lD(>2qNSkPKMU_E${G39|K zq^r{p1Seps8X6G-c_Q9YQB+V74MLCLQErK6+}C=dOjq<&_4YPt^sxk}GL zPWQp1i*UfK_*i-3{d|f0PQT@2k}JLJ2P|y?hsU~XLUPS?08YeCXx@b6nCYd7!I{a< zFf%(RHu#N^lTFDBpvO9!kdSC{LIT;IE{^F7P<5;+W7OO<1hZ-F&R&hFp<|s4J5=~B zPBpm)8B8^)tMCKW1Ww+B>}yWkL_^wGRk7oygtWB?hAo^N2cNr}FjeN?2u0PlkoCP4 zi8s~NpjcFVQMnYywl2VojD+3 zqBv7e$k2j!rd*HXOfV*tfZ3DD;mx>Mg!0>&MN=>(TE#8QPGGU)pFusf_cJ1H`0^Ty zf04q}08%XH_EhF!%ki@W+??gQXuQy&DfSWtQ&}sMl9lsW+4m_yl|bXYbL&M(Y0@<` zF*ud?R6>Fu<=VXqlQ40<>_I5irU%h%Ju_=?$Qqfxi6M zgQJ>IvSY8hpnuywDr+K}gnook7S(CSXUFE|9{=m09AE_M@@9H&AaP|P8?i|%=}BG) z+X2|FOn3wybV5q~(Bk(d1q4eJV!d%$8kPy;CgLeCv zfR}rgtt&_pU9+mPUe!c30}o$HU*R7q{M8RQ(2i$WKs~> z$w`rtV2~tMJ0OV$MRhR-R`n2=_(UcN6c$z_N+7f-gis6IvVQ`hR@igf5LyYx^{{oq z^N4F^-+kT8-d?W=&ZpNaviFAy;W^BxAKyN+w`bPF^YqMm*u%_vcs_w#_azYOg0h_l zE}q%j)wA$CT|Eo?boDIkp?Vh1Cs1DPbFiO4c^z;M-3aA%H^D&&<((Cs+5a;?f%6Cz zoc!Fk0RMQVzzN^+xWQvY0TvyS%>Vv#|HLr9`CNJ#qT>n1@z)cB-?^SJ^SPPTG_-GB zZ+4o|e0KyL|81=Noo_pK_i5Mh?QesQFWXK8HhTcr8$1_8*laff&v_j-IL8NJgC$*f z`+VAye5>a8n+jxquAJZ#pqr=Im1I3bcAb=QG+&%Poi6ZN!u-#l z`zDeB&uRW|e>!UCssFb>2_ceg>X{hn|L>Y0GXCopfRGB5DiabtgSJR~G}Zbo0smNm z`oC<5mydRqGrrXj$A@9IVrj==c5XyaLC%Lr%)>z5;Gb&gj;q=^uB7hxin_E9IPvfg a3w(F|y?^n4^kgU=f~r7~`M>a&`TqeXk;v}= diff --git a/commafeed-server/src/test/resources/logback-test.xml b/commafeed-server/src/test/resources/logback-test.xml deleted file mode 100644 index a452f81a..00000000 --- a/commafeed-server/src/test/resources/logback-test.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %-5p %d{ISO8601} [%thread] [%c{0}:%L] %m %rEx%n - - - - - - - \ No newline at end of file From 09e0a51b46e7774bc04f820062904e39cc5e56df Mon Sep 17 00:00:00 2001 From: Athou Date: Sun, 11 Aug 2024 22:04:35 +0200 Subject: [PATCH 02/50] restore Docker workflow --- .dockerignore | 7 --- .github/workflows/build.yml | 54 ++++++++++++++++++- Dockerfile | 19 ------- commafeed-server/TODO.md | 2 + .../src/main/docker/Dockerfile.jvm | 16 ++++++ .../src/main/docker/Dockerfile.native | 9 ++++ .../src/main/resources/application.properties | 6 +++ 7 files changed, 86 insertions(+), 27 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile create mode 100644 commafeed-server/src/main/docker/Dockerfile.jvm create mode 100644 commafeed-server/src/main/docker/Dockerfile.native diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 7b78e880..00000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -# ignore everything -* - -# allow only what we need -!commafeed-server/target/commafeed.jar -!commafeed-server/config.yml.example - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2ba3b56..b6801282 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Java CI +name: ci on: [ push ] @@ -43,3 +43,55 @@ jobs: with: name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }} path: commafeed-server/target/commafeed-*-runner + + # Docker + - name: Login to Container Registry + uses: docker/login-action@v3 + if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }} + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker build and push tag - native + uses: docker/build-push-action@v6 + if: ${{ github.ref_type == 'tag' }} + with: + context: commafeed-server + file: src/main/docker/Dockerfile.native + push: true + platforms: linux/amd64 + tags: | + athou/commafeed:latest + athou/commafeed:${{ github.ref_name }} + + - name: Docker build and push tag - jvm + uses: docker/build-push-action@v6 + if: ${{ github.ref_type == 'tag' }} + with: + context: commafeed-server + file: src/main/docker/Dockerfile.jvm + push: true + platforms: linux/amd64,linux/arm64/v8 + tags: | + athou/commafeed:latest-jvm + athou/commafeed:${{ github.ref_name }}-jvm + + - name: Docker build and push master - native + uses: docker/build-push-action@v6 + if: ${{ github.ref_name == 'master' }} + with: + context: commafeed-server + file: src/main/docker/Dockerfile.native + push: true + platforms: linux/amd64 + tags: athou/commafeed:master + + - name: Docker build and push master - jvm + uses: docker/build-push-action@v6 + if: ${{ github.ref_name == 'master' }} + with: + context: commafeed-server + file: src/main/docker/Dockerfile.jvm + push: true + platforms: linux/amd64 + tags: athou/commafeed:master-jvm diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4535100e..00000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM ibm-semeru-runtimes:open-21.0.4_7-jre - -EXPOSE 8082 - -RUN mkdir -p /commafeed/data -VOLUME /commafeed/data - -COPY commafeed-server/config.yml.example config.yml -COPY commafeed-server/target/commafeed.jar . - -CMD ["java", \ - "-Djava.net.preferIPv4Stack=true", \ - "-Xtune:virtualized", \ - "-Xminf0.05", \ - "-Xmaxf0.1", \ - "-jar", \ - "commafeed.jar", \ - "server", \ - "config.yml"] diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 3a14ea57..228202a1 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -8,7 +8,9 @@ MVP: - cookie duration too short - https://github.com/quarkusio/quarkus/issues/42463 +- mvn profile instead of -Dquarkus.datasource.db-kind - update dockerfile + - release after tag (new job that downloads all artifacts because we need them all to create the release) - update github actions (build and copy outside target each database artifact) - update readme - update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) diff --git a/commafeed-server/src/main/docker/Dockerfile.jvm b/commafeed-server/src/main/docker/Dockerfile.jvm new file mode 100644 index 00000000..38f94ea1 --- /dev/null +++ b/commafeed-server/src/main/docker/Dockerfile.jvm @@ -0,0 +1,16 @@ +FROM ibm-semeru-runtimes:open-21.0.3_9-jre + +EXPOSE 8082 + +RUN mkdir -p /commafeed/data +VOLUME /commafeed/data + +COPY commafeed-server/target/quarkus-app/ /commafeed/app +WORKDIR /commafeed/app + +CMD ["java", \ + "-Xtune:virtualized", \ + "-Xminf0.05", \ + "-Xmaxf0.1", \ + "-jar", \ + "quarkus-run.jar"] diff --git a/commafeed-server/src/main/docker/Dockerfile.native b/commafeed-server/src/main/docker/Dockerfile.native new file mode 100644 index 00000000..9393734f --- /dev/null +++ b/commafeed-server/src/main/docker/Dockerfile.native @@ -0,0 +1,9 @@ +FROM debian:12.6 +EXPOSE 8082 + +RUN mkdir -p /commafeed/data +VOLUME /commafeed/data + +COPY commafeed-server/target/commafeed-*-runner /commafeed/app/application +WORKDIR /commafeed/app +CMD ["./application"] diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties index 733fd569..82a33f85 100644 --- a/commafeed-server/src/main/resources/application.properties +++ b/commafeed-server/src/main/resources/application.properties @@ -41,3 +41,9 @@ quarkus.shutdown.timeout=5s %test.commafeed.smtp.user-name=user %test.commafeed.smtp.password=pass %test.commafeed.smtp.from-address=noreply@commafeed.com + + +# prod profile overrides +%prod.quarkus.datasource.jdbc.url=jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE +%prod.quarkus.datasource.username=sa +%prod.quarkus.datasource.password=sa \ No newline at end of file From 1fd48a0a4099bec13af59d1384b17a3869c07d38 Mon Sep 17 00:00:00 2001 From: Athou Date: Sun, 11 Aug 2024 22:47:55 +0200 Subject: [PATCH 03/50] merge docker amd64 and arm64 tags --- .github/workflows/build.yml | 34 +++++++++++++++++++++++++++++----- commafeed-server/TODO.md | 6 +++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6801282..7496e713 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,12 @@ jobs: fetch-depth: 0 # Setup + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Set up GraalVM uses: graalvm/setup-graalvm@v1 with: @@ -52,6 +58,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + ## tags - name: Docker build and push tag - native uses: docker/build-push-action@v6 if: ${{ github.ref_type == 'tag' }} @@ -61,8 +68,8 @@ jobs: push: true platforms: linux/amd64 tags: | - athou/commafeed:latest - athou/commafeed:${{ github.ref_name }} + athou/commafeed:latest-native + athou/commafeed:${{ github.ref_name }}-native - name: Docker build and push tag - jvm uses: docker/build-push-action@v6 @@ -71,11 +78,20 @@ jobs: context: commafeed-server file: src/main/docker/Dockerfile.jvm push: true - platforms: linux/amd64,linux/arm64/v8 + platforms: linux/arm64/v8 tags: | athou/commafeed:latest-jvm athou/commafeed:${{ github.ref_name }}-jvm + - name: Docker merge tag manifests + uses: Noelware/docker-manifest-action@0.4.2 + if: ${{ github.ref_type == 'tag' }} + with: + inputs: athou/commafeed:latest + images: athou/commafeed:latest-native,athou/commafeed:latest-jvm + push: true + + ## master - name: Docker build and push master - native uses: docker/build-push-action@v6 if: ${{ github.ref_name == 'master' }} @@ -84,7 +100,7 @@ jobs: file: src/main/docker/Dockerfile.native push: true platforms: linux/amd64 - tags: athou/commafeed:master + tags: athou/commafeed:master-native - name: Docker build and push master - jvm uses: docker/build-push-action@v6 @@ -93,5 +109,13 @@ jobs: context: commafeed-server file: src/main/docker/Dockerfile.jvm push: true - platforms: linux/amd64 + platforms: linux/arm64/v8 tags: athou/commafeed:master-jvm + + - name: Docker merge master manifests + uses: Noelware/docker-manifest-action@0.4.2 + if: ${{ github.ref_name == 'master' }} + with: + inputs: athou/commafeed:master + images: athou/commafeed:master-native,athou/commafeed:master-jvm + push: true diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 228202a1..192376e1 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -9,9 +9,9 @@ MVP: - https://github.com/quarkusio/quarkus/issues/42463 - mvn profile instead of -Dquarkus.datasource.db-kind -- update dockerfile - - release after tag (new job that downloads all artifacts because we need them all to create the release) -- update github actions (build and copy outside target each database artifact) +- update github actions + - release after tag + - new job that downloads all artifacts because we need them all to create the release - update readme - update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) From aaf237d111717792b5ffb13231912ce07f00800c Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 09:41:14 +0200 Subject: [PATCH 04/50] use quarkus mailer for password recovery --- commafeed-server/TODO.md | 3 +- commafeed-server/pom.xml | 15 ++--- .../com/commafeed/CommaFeedConfiguration.java | 27 +++------ .../backend/service/MailService.java | 56 ++----------------- .../frontend/resource/ServerREST.java | 2 +- .../commafeed/frontend/resource/UserREST.java | 4 ++ .../frontend/servlet/LogoutServlet.java | 11 ++-- .../src/main/resources/application.properties | 7 +-- .../commafeed/integration/rest/UserIT.java | 52 ++++++++--------- 9 files changed, 53 insertions(+), 124 deletions(-) diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 192376e1..6c77457f 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -3,10 +3,9 @@ TODO MVP: -- quarkus mailer for smtp - - https://quarkus.io/guides/mailer - cookie duration too short - https://github.com/quarkusio/quarkus/issues/42463 + - Rewrite cookie with https://quarkus.io/guides/rest#request-or-response-filters in the mean time - mvn profile instead of -Dquarkus.datasource.db-kind - update github actions diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 4ab11efd..e01adf79 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -255,6 +255,10 @@ io.quarkus quarkus-websockets + + io.quarkus + quarkus-mailer + io.quarkus quarkus-hibernate-orm @@ -331,16 +335,11 @@ passay 1.6.4 - redis.clients jedis 5.1.4 - - com.sun.mail - jakarta.mail - com.rometools @@ -456,12 +455,6 @@ rest-assured test - - com.icegreen - greenmail-junit5 - 2.0.1 - test - org.awaitility awaitility diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index eb64073f..d1b92360 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -49,6 +49,14 @@ public interface CommaFeedConfiguration { @WithDefault("false") boolean imageProxyEnabled(); + /** + * 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. */ @@ -84,11 +92,6 @@ public interface CommaFeedConfiguration { */ Websocket websocket(); - /** - * SMTP settings for password recovery. - */ - Optional smtp(); - /** * Redis settings to enable caching. This is only really useful on instances with a lot of users. */ @@ -207,20 +210,6 @@ public interface CommaFeedConfiguration { boolean createDemoAccount(); } - interface Smtp { - String host(); - - int port(); - - boolean tls(); - - String userName(); - - String password(); - - String fromAddress(); - } - interface Websocket { /** * Enable websocket connection so the server can notify the web client that there are new entries for your feeds. 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 758945aa..cef02d1f 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,64 +1,20 @@ package com.commafeed.backend.service; -import java.util.Optional; -import java.util.Properties; - -import com.commafeed.CommaFeedConfiguration; -import com.commafeed.CommaFeedConfiguration.Smtp; import com.commafeed.backend.model.User; +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.Mailer; import jakarta.inject.Singleton; -import jakarta.mail.Authenticator; -import jakarta.mail.Message; -import jakarta.mail.PasswordAuthentication; -import jakarta.mail.Session; -import jakarta.mail.Transport; -import jakarta.mail.internet.InternetAddress; -import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; -/** - * Mailing service - * - */ @RequiredArgsConstructor @Singleton public class MailService { - private final CommaFeedConfiguration config; - - public void sendMail(User user, String subject, String content) throws Exception { - Optional settings = config.smtp(); - if (settings.isEmpty()) { - throw new IllegalArgumentException("SMTP settings not configured"); - } - - final String username = settings.get().userName(); - final String password = settings.get().password(); - final String fromAddress = Optional.ofNullable(settings.get().fromAddress()).orElse(settings.get().userName()); - - String dest = user.getEmail(); - - Properties props = new Properties(); - props.put("mail.smtp.auth", "true"); - props.put("mail.smtp.starttls.enable", String.valueOf(settings.get().tls())); - props.put("mail.smtp.host", settings.get().host()); - props.put("mail.smtp.port", String.valueOf(settings.get().port())); - - Session session = Session.getInstance(props, new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - - Message message = new MimeMessage(session); - message.setFrom(new InternetAddress(fromAddress, "CommaFeed")); - message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest)); - message.setSubject("CommaFeed - " + subject); - message.setContent(content, "text/html; charset=utf-8"); - - Transport.send(message); + 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); } } 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 881a7a04..4a6d64d7 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 @@ -56,7 +56,7 @@ public class ServerREST { infos.setGitCommit(version.getGitCommit()); infos.setAllowRegistrations(config.users().allowRegistrations()); infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null)); - infos.setSmtpEnabled(config.smtp().isPresent()); + infos.setSmtpEnabled(config.passwordRecoveryEnabled()); infos.setDemoAccountEnabled(config.users().createDemoAccount()); infos.setWebsocketEnabled(config.websocket().enabled()); infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis()); 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 b391631f..0162c089 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 @@ -267,6 +267,10 @@ public class UserREST { @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(); 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 09a7b426..592074b4 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 @@ -14,18 +14,19 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; -import lombok.RequiredArgsConstructor; @Path("/logout") @PermitAll -@RequiredArgsConstructor @Singleton public class LogoutServlet { - @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") - String cookieName; - private final CommaFeedConfiguration config; + private final String cookieName; + + public LogoutServlet(CommaFeedConfiguration config, @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) { + this.config = config; + this.cookieName = cookieName; + } @GET public Response get() { diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties index 82a33f85..c8301671 100644 --- a/commafeed-server/src/main/resources/application.properties +++ b/commafeed-server/src/main/resources/application.properties @@ -35,12 +35,7 @@ quarkus.shutdown.timeout=5s %test.quarkus.log.category."liquibase".level=WARN %test.commafeed.users.create-demo-account=true %test.commafeed.users.allow-registrations=true -%test.commafeed.smtp.host=localhost -%test.commafeed.smtp.port=3025 -%test.commafeed.smtp.tls=false -%test.commafeed.smtp.user-name=user -%test.commafeed.smtp.password=pass -%test.commafeed.smtp.from-address=noreply@commafeed.com +%test.commafeed.password-recovery-enabled=true # prod profile overrides 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 ae077b1c..60f6d317 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,52 +1,44 @@ package com.commafeed.integration.rest; -import org.junit.jupiter.api.AfterEach; +import java.util.List; + 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.frontend.model.request.PasswordResetRequest; import com.commafeed.integration.BaseIT; -import com.icegreen.greenmail.util.GreenMail; -import com.icegreen.greenmail.util.ServerSetupTest; +import io.quarkus.mailer.MockMailbox; import io.quarkus.test.junit.QuarkusTest; -import jakarta.mail.internet.MimeMessage; +import io.vertx.ext.mail.MailMessage; +import jakarta.inject.Inject; import jakarta.ws.rs.client.Entity; @QuarkusTest class UserIT extends BaseIT { - @Nested - class PasswordReset { + @Inject + MockMailbox mailbox; - private GreenMail greenMail; + @BeforeEach + void setup() { + mailbox.clear(); + } - @BeforeEach - void setup() { - this.greenMail = new GreenMail(ServerSetupTest.SMTP); - this.greenMail.start(); - this.greenMail.setUser("noreply@commafeed.com", "user", "pass"); - } + @Test + void resetPassword() { + PasswordResetRequest req = new PasswordResetRequest(); + req.setEmail("admin@commafeed.com"); - @AfterEach - void cleanup() { - this.greenMail.stop(); - } + getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE); - @Test - void resetPassword() throws Exception { - PasswordResetRequest req = new PasswordResetRequest(); - req.setEmail("admin@commafeed.com"); + List mails = mailbox.getMailMessagesSentTo("admin@commafeed.com"); + Assertions.assertEquals(1, mails.size()); - getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject()); - Assertions.assertTrue(message.getContent().toString().startsWith("You asked for password recovery for account 'admin'")); - Assertions.assertEquals("CommaFeed ", message.getFrom()[0].toString()); - Assertions.assertEquals("admin@commafeed.com", message.getAllRecipients()[0].toString()); - } + MailMessage message = mails.get(0); + 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().get(0)); } } From 6b0aa32da2e0eec217eaa3878b43a4144d60dbcf Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 10:25:25 +0200 Subject: [PATCH 05/50] use profile instead of system property to set db-kind --- .github/workflows/build.yml | 2 +- commafeed-server/TODO.md | 7 +- commafeed-server/pom.xml | 126 ++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7496e713..248c9d9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: # Build & Test - name: Build with Maven - run: mvn --batch-mode --no-transfer-progress install -Pnative -D"quarkus.datasource.db-kind"=${{ matrix.database }} + run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} # Upload artifacts - name: Upload cross-platform app diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 6c77457f..0a6d77bd 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -7,11 +7,11 @@ MVP: - https://github.com/quarkusio/quarkus/issues/42463 - Rewrite cookie with https://quarkus.io/guides/rest#request-or-response-filters in the mean time -- mvn profile instead of -Dquarkus.datasource.db-kind - update github actions - release after tag - new job that downloads all artifacts because we need them all to create the release - update readme +- update docker readme - update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) Nice to have: @@ -24,8 +24,3 @@ Nice to have: - OPML encoding is not handled correctly - remove Timers metrics page -native-image -------------- - -- https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Resources/ -- https://github.com/rometools/rome/pull/636/files \ No newline at end of file diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index e01adf79..57b1bf31 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -16,6 +16,7 @@ 6.6 2.1.0 3.1.8 + 1.2.1 h2 @@ -41,6 +42,19 @@ + + maven-help-plugin + 3.4.1 + + + initialize + + active-profiles + + + + + io.quarkus.platform quarkus-maven-plugin @@ -480,5 +494,117 @@ true + + + h2 + + true + + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-plugin.version} + + + + set-system-properties + + + + + + + quarkus.datasource.db-kind + h2 + + + + + + + + + mysql + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-plugin.version} + + + + set-system-properties + + + + + + + quarkus.datasource.db-kind + mysql + + + + + + + + + mariadb + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-plugin.version} + + + + set-system-properties + + + + + + + quarkus.datasource.db-kind + mariadb + + + + + + + + + postgresql + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-plugin.version} + + + + set-system-properties + + + + + + + quarkus.datasource.db-kind + postgresql + + + + + + + From 89405009ec42cdbc811682abadebd20cfaba2ab2 Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 13:10:35 +0200 Subject: [PATCH 06/50] set a Max-Age on the auth cookie --- commafeed-server/TODO.md | 4 - ...okieMaxAgeFormAuthenticationMechanism.java | 115 ++++++++++++++++++ .../com/commafeed/integration/SecurityIT.java | 27 ++++ 3 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 0a6d77bd..49ee11f4 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -3,10 +3,6 @@ TODO MVP: -- cookie duration too short - - https://github.com/quarkusio/quarkus/issues/42463 - - Rewrite cookie with https://quarkus.io/guides/rest#request-or-response-filters in the mean time - - update github actions - release after tag - new job that downloads all artifacts because we need them all to create the release diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java new file mode 100644 index 00000000..97445772 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java @@ -0,0 +1,115 @@ +package com.commafeed.security.mechanism; + +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Set; + +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.vertx.http.runtime.FormAuthConfig; +import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; +import io.quarkus.vertx.http.runtime.security.PersistentLoginManager; +import io.smallrye.mutiny.Uni; +import io.vertx.core.http.Cookie; +import io.vertx.core.http.impl.ServerCookie; +import io.vertx.ext.web.RoutingContext; +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; +import lombok.extern.slf4j.Slf4j; + +/** + * HttpAuthenticationMechanism that wraps FormAuthenticationMechanism and sets a Max-Age on the cookie because it has no value by default. + * + * This is a workaround for https://github.com/quarkusio/quarkus/issues/42463 + */ +@Priority(1) +@Singleton +@Slf4j +public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticationMechanism { + + // the temp encryption key, persistent across dev mode restarts + static volatile String encryptionKey; + + private final FormAuthenticationMechanism delegate; + + public CookieMaxAgeFormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { + String key; + if (httpConfiguration.encryptionKey.isEmpty()) { + if (encryptionKey != null) { + // persist across dev mode restarts + key = encryptionKey; + } else { + byte[] data = new byte[32]; + new SecureRandom().nextBytes(data); + key = encryptionKey = Base64.getEncoder().encodeToString(data); + log.warn("Encryption key was not specified for persistent FORM auth, using temporary key {}", key); + } + } else { + key = httpConfiguration.encryptionKey.get(); + } + + FormAuthConfig form = buildTimeConfig.auth.form; + FormAuthRuntimeConfig runtimeForm = httpConfiguration.auth.form; + String loginPage = startWithSlash(runtimeForm.loginPage.orElse(null)); + String errorPage = startWithSlash(runtimeForm.errorPage.orElse(null)); + String landingPage = startWithSlash(runtimeForm.landingPage.orElse(null)); + String postLocation = startWithSlash(form.postLocation); + String usernameParameter = runtimeForm.usernameParameter; + String passwordParameter = runtimeForm.passwordParameter; + String locationCookie = runtimeForm.locationCookie; + String cookiePath = runtimeForm.cookiePath.orElse(null); + boolean redirectAfterLogin = landingPage != null; + String cookieSameSite = runtimeForm.cookieSameSite.name(); + + PersistentLoginManager loginManager = new PersistentLoginManager(key, runtimeForm.cookieName, runtimeForm.timeout.toMillis(), + runtimeForm.newCookieInterval.toMillis(), runtimeForm.httpOnlyCookie, cookieSameSite, cookiePath) { + @Override + public void save(String value, RoutingContext context, String cookieName, RestoreResult restoreResult, boolean secureCookie) { + super.save(value, context, cookieName, restoreResult, secureCookie); + + // add max age to the cookie + Cookie cookie = context.request().getCookie(cookieName); + if (cookie instanceof ServerCookie sc && sc.isChanged()) { + cookie.setMaxAge(runtimeForm.timeout.toSeconds()); + } + } + }; + + this.delegate = new FormAuthenticationMechanism(loginPage, postLocation, usernameParameter, passwordParameter, errorPage, + landingPage, redirectAfterLogin, locationCookie, cookieSameSite, cookiePath, loginManager); + } + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return delegate.authenticate(context, identityProviderManager); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return delegate.getChallenge(context); + } + + @Override + public Set> getCredentialTypes() { + return delegate.getCredentialTypes(); + } + + @Override + public Uni getCredentialTransport(RoutingContext context) { + return delegate.getCredentialTransport(context); + } + + private static String startWithSlash(String page) { + if (page == null) { + return null; + } + return page.startsWith("/") ? page : "/" + page; + } +} 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 3864cd7b..5ba7c6c6 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java @@ -1,6 +1,9 @@ package com.commafeed.integration; +import java.net.HttpCookie; import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; @@ -27,6 +30,30 @@ class SecurityIT extends BaseIT { } } + @Test + void formLogin() { + List cookies = login(); + + try (Response response = getClient().target(getApiBaseUrl() + "user/profile") + .request() + .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) + .get()) { + Assertions.assertEquals(HttpStatus.SC_OK, response.getStatus()); + cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0)); + } + } + + @Test + void basicAuthLogin() { + String auth = "Basic " + Base64.getEncoder().encodeToString("admin:admin".getBytes()); + try (Response response = getClient().target(getApiBaseUrl() + "user/profile") + .request() + .header(HttpHeaders.AUTHORIZATION, auth) + .get()) { + Assertions.assertEquals(HttpStatus.SC_OK, response.getStatus()); + } + } + @Test void wrongPassword() { String auth = "Basic " + Base64.getEncoder().encodeToString("admin:wrong-password".getBytes()); From 04af355e0c10b56a85618e2222d35e865a2a5340 Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 16:40:24 +0200 Subject: [PATCH 07/50] remove unused timers page --- .../src/pages/admin/MetricsPage.tsx | 61 ++++++------------- commafeed-server/TODO.md | 2 - 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/commafeed-client/src/pages/admin/MetricsPage.tsx b/commafeed-client/src/pages/admin/MetricsPage.tsx index b0de53e1..c0e1ef01 100644 --- a/commafeed-client/src/pages/admin/MetricsPage.tsx +++ b/commafeed-client/src/pages/admin/MetricsPage.tsx @@ -1,13 +1,11 @@ -import { Accordion, Box, Tabs } from "@mantine/core" +import { Accordion, Box } from "@mantine/core" import { client } from "app/client" import { Loader } from "components/Loader" import { Gauge } from "components/metrics/Gauge" import { Meter } from "components/metrics/Meter" import { MetricAccordionItem } from "components/metrics/MetricAccordionItem" -import { Timer } from "components/metrics/Timer" import { useEffect } from "react" import { useAsync } from "react-async-hook" -import { TbChartAreaLine, TbClock } from "react-icons/tb" const shownMeters: Record = { "com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate", @@ -42,46 +40,25 @@ export function MetricsPage() { }, [query.execute]) if (!query.result) return - const { meters, gauges, timers } = query.result.data + const { meters, gauges } = query.result.data return ( - - - }> - Stats - - }> - Timers - - + <> + + {Object.keys(shownMeters).map(m => ( + + + + ))} + - - - {Object.keys(shownMeters).map(m => ( - - - - ))} - - - - {Object.keys(shownGauges).map(g => ( - - {shownGauges[g]}:  - - - ))} - - - - - - {Object.keys(timers).map(key => ( - - - - ))} - - - + + {Object.keys(shownGauges).map(g => ( + + {shownGauges[g]}:  + + + ))} + + ) } diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 49ee11f4..8b9bfe32 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -18,5 +18,3 @@ Nice to have: - rename "servlets" since they are now rest endpoints - warnings hibernate on startup - OPML encoding is not handled correctly -- remove Timers metrics page - From 78a5267198492d7b61688d552c7d5087c230bdd0 Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 16:40:43 +0200 Subject: [PATCH 08/50] fix opml import encoding issue --- commafeed-server/TODO.md | 1 - .../main/java/com/commafeed/frontend/resource/FeedREST.java | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 8b9bfe32..d83f4102 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -17,4 +17,3 @@ Nice to have: - remove rest assured or use only rest assured - rename "servlets" since they are now rest endpoints - warnings hibernate on startup -- OPML encoding is not handled correctly 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 52879805..685867e5 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 @@ -2,6 +2,7 @@ package com.commafeed.frontend.resource; import java.io.StringWriter; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Calendar; import java.util.Collections; @@ -11,6 +12,7 @@ import java.util.Objects; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; import org.jboss.resteasy.reactive.RestForm; import com.commafeed.CommaFeedApplication; @@ -505,7 +507,8 @@ public class FeedREST { return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build(); } try { - opmlImporter.importOpml(user, opml); + // 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(); } From dc16e4315412191940ad73371495b6fc372b658e Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 16:47:32 +0200 Subject: [PATCH 09/50] add release ci job --- .github/workflows/{build.yml => ci.yml} | 33 +++++++++++++++++++++++++ commafeed-server/TODO.md | 3 --- 2 files changed, 33 insertions(+), 3 deletions(-) rename .github/workflows/{build.yml => ci.yml} (78%) diff --git a/.github/workflows/build.yml b/.github/workflows/ci.yml similarity index 78% rename from .github/workflows/build.yml rename to .github/workflows/ci.yml index 248c9d9e..d131e3a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/ci.yml @@ -119,3 +119,36 @@ jobs: inputs: athou/commafeed:master images: athou/commafeed:master-native,athou/commafeed:master-jvm push: true + + release: + runs-on: ubuntu-latest + needs: build + if: github.ref_type == 'tag' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: commafeed-* + path: ./artifacts + merge-multiple: true + + - name: Extract Changelog Entry + uses: mindsers/changelog-reader-action@v2 + id: changelog_reader + with: + version: ${{ github.ref_name }} + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + name: CommaFeed ${{ github.ref_name }} + body: ${{ steps.changelog_reader.outputs.changes }} + draft: false + prerelease: false + files: ./artifacts/* diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index d83f4102..b314b87a 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -3,9 +3,6 @@ TODO MVP: -- update github actions - - release after tag - - new job that downloads all artifacts because we need them all to create the release - update readme - update docker readme - update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) From f7adef0648172df5af74dd52f4669dd83c684dad Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 17:34:01 +0200 Subject: [PATCH 10/50] add windows builds --- .github/workflows/ci.yml | 49 ++++++++++++++++--- .../com/commafeed/backend/HttpGetterTest.java | 2 +- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d131e3a0..c563ce47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,11 @@ name: ci on: [ push ] +env: + JAVA_VERSION: 21 + jobs: - build: + build-linux: runs-on: ubuntu-latest strategy: matrix: @@ -11,9 +14,6 @@ jobs: steps: # Checkout - - name: Configure git to checkout as-is - run: git config --global core.autocrlf false - - name: Checkout uses: actions/checkout@v4 with: @@ -29,7 +29,7 @@ jobs: - name: Set up GraalVM uses: graalvm/setup-graalvm@v1 with: - java-version: "21" + java-version: ${{ env.JAVA_VERSION }} distribution: "graalvm" cache: "maven" @@ -120,9 +120,46 @@ jobs: images: athou/commafeed:master-native,athou/commafeed:master-jvm push: true + build-windows: + runs-on: windows-latest + strategy: + matrix: + database: [ "h2", "postgresql", "mysql", "mariadb" ] + + steps: + # Checkout + - name: Configure git to checkout as-is + run: git config --global core.autocrlf false + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Setup + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: "graalvm" + cache: "maven" + + # Build & Test + - name: Build with Maven + run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.database != 'h2' }} + + # Upload artifacts + - name: Upload native executable + uses: actions/upload-artifact@v4 + with: + name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }} + path: commafeed-server/target/commafeed-*-runner.exe + release: runs-on: ubuntu-latest - needs: build + needs: + - build-linux + - build-windows if: github.ref_type == 'tag' steps: 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 e0289599..a10cb2a7 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -85,7 +85,7 @@ class HttpGetterTest { Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.getContentType()); Assertions.assertEquals("123456", result.getLastModifiedSince()); Assertions.assertEquals("78910", result.getETag()); - Assertions.assertTrue(result.getDuration() > 0); + Assertions.assertTrue(result.getDuration() >= 0); Assertions.assertEquals(this.feedUrl, result.getUrlAfterRedirect()); } From 3af84853265a621b4a2560d99549d504eb10c177 Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 21:08:07 +0200 Subject: [PATCH 11/50] TODO remove redis --- commafeed-server/TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index b314b87a..700b5bcc 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -3,6 +3,7 @@ TODO MVP: +- remove redis, not useful anymore - update readme - update docker readme - update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) From 044694487d6e99d0160c994c411d7bb28a0762a0 Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 21:27:21 +0200 Subject: [PATCH 12/50] remove redis as caching is no longer needed now --- .../src/pages/admin/MetricsPage.tsx | 3 +- commafeed-server/TODO.md | 1 - commafeed-server/pom.xml | 5 - .../com/commafeed/CommaFeedConfiguration.java | 54 ------ .../com/commafeed/CommaFeedProducers.java | 15 -- .../commafeed/backend/cache/CacheService.java | 39 ----- .../backend/cache/NoopCacheService.java | 54 ------ .../backend/cache/RedisCacheService.java | 154 ------------------ .../backend/feed/FeedRefreshUpdater.java | 45 +---- .../commafeed/backend/opml/OPMLImporter.java | 3 - .../backend/service/FeedEntryService.java | 6 - .../service/FeedSubscriptionService.java | 22 +-- .../frontend/resource/CategoryREST.java | 24 +-- .../commafeed/frontend/resource/FeedREST.java | 3 - .../backend/opml/OPMLImporterTest.java | 4 +- 15 files changed, 22 insertions(+), 410 deletions(-) delete mode 100644 commafeed-server/src/main/java/com/commafeed/backend/cache/CacheService.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/backend/cache/NoopCacheService.java delete mode 100644 commafeed-server/src/main/java/com/commafeed/backend/cache/RedisCacheService.java diff --git a/commafeed-client/src/pages/admin/MetricsPage.tsx b/commafeed-client/src/pages/admin/MetricsPage.tsx index c0e1ef01..d90e6227 100644 --- a/commafeed-client/src/pages/admin/MetricsPage.tsx +++ b/commafeed-client/src/pages/admin/MetricsPage.tsx @@ -11,8 +11,7 @@ const shownMeters: Record = { "com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate", "com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate", "com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate", - "com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate", - "com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate", + "com.commafeed.backend.feed.FeedRefreshUpdater.entryInserted": "Entries inserted", "com.commafeed.backend.service.db.DatabaseCleaningService.entriesDeleted": "Entries deleted", } diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md index 700b5bcc..b314b87a 100644 --- a/commafeed-server/TODO.md +++ b/commafeed-server/TODO.md @@ -3,7 +3,6 @@ TODO MVP: -- remove redis, not useful anymore - update readme - update docker readme - update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 57b1bf31..fbd54e20 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -349,11 +349,6 @@ passay 1.6.4 - - redis.clients - jedis - 5.1.4 - com.rometools diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index d1b92360..1b5c225a 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -12,12 +12,6 @@ import io.smallrye.config.WithDefault; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisClientConfig; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.Protocol; /** * CommaFeed configuration @@ -92,11 +86,6 @@ public interface CommaFeedConfiguration { */ Websocket websocket(); - /** - * Redis settings to enable caching. This is only really useful on instances with a lot of users. - */ - Redis redis(); - interface FeedRefresh { /** * Amount of time CommaFeed will wait before refreshing the same feed. @@ -230,47 +219,4 @@ public interface CommaFeedConfiguration { Duration treeReloadInterval(); } - interface Redis { - - Optional host(); - - @WithDefault("" + Protocol.DEFAULT_PORT) - int port(); - - /** - * Username is only required when using Redis ACLs - */ - Optional username(); - - Optional password(); - - @WithDefault("" + Protocol.DEFAULT_TIMEOUT) - int timeout(); - - @WithDefault("" + Protocol.DEFAULT_DATABASE) - int database(); - - @WithDefault("500") - int maxTotal(); - - default JedisPool build() { - Optional host = host(); - if (host.isEmpty()) { - throw new IllegalStateException("Redis host is required"); - } - - JedisPoolConfig poolConfig = new JedisPoolConfig(); - poolConfig.setMaxTotal(maxTotal()); - - JedisClientConfig clientConfig = DefaultJedisClientConfig.builder() - .user(username().orElse(null)) - .password(password().orElse(null)) - .timeoutMillis(timeout()) - .database(database()) - .build(); - - return new JedisPool(poolConfig, new HostAndPort(host.get(), port()), clientConfig); - } - } - } diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java index 63df69dd..7ab50216 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java @@ -1,10 +1,6 @@ package com.commafeed; import com.codahale.metrics.MetricRegistry; -import com.commafeed.CommaFeedConfiguration.Redis; -import com.commafeed.backend.cache.CacheService; -import com.commafeed.backend.cache.NoopCacheService; -import com.commafeed.backend.cache.RedisCacheService; import jakarta.enterprise.inject.Produces; import jakarta.inject.Singleton; @@ -12,17 +8,6 @@ import jakarta.inject.Singleton; @Singleton public class CommaFeedProducers { - @Produces - @Singleton - public CacheService cacheService(CommaFeedConfiguration config) { - Redis redis = config.redis(); - if (redis.host().isEmpty()) { - return new NoopCacheService(); - } - - return new RedisCacheService(redis.build()); - } - @Produces @Singleton public MetricRegistry metricRegistry() { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/cache/CacheService.java b/commafeed-server/src/main/java/com/commafeed/backend/cache/CacheService.java deleted file mode 100644 index 106927ef..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/cache/CacheService.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.commafeed.backend.cache; - -import java.util.List; -import java.util.Set; - -import com.commafeed.backend.Digests; -import com.commafeed.backend.feed.parser.FeedParserResult.Entry; -import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedSubscription; -import com.commafeed.backend.model.User; -import com.commafeed.frontend.model.Category; -import com.commafeed.frontend.model.UnreadCount; - -public abstract class CacheService { - - // feed entries for faster refresh - public abstract Set getLastEntries(Feed feed); - - public abstract void setLastEntries(Feed feed, List entries); - - public String buildUniqueEntryKey(Entry entry) { - return Digests.sha1Hex(entry.guid() + entry.url()); - } - - // user categories - public abstract Category getUserRootCategory(User user); - - public abstract void setUserRootCategory(User user, Category category); - - public abstract void invalidateUserRootCategory(User... users); - - // unread count - public abstract UnreadCount getUnreadCount(FeedSubscription sub); - - public abstract void setUnreadCount(FeedSubscription sub, UnreadCount count); - - public abstract void invalidateUnreadCount(FeedSubscription... subs); - -} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/cache/NoopCacheService.java b/commafeed-server/src/main/java/com/commafeed/backend/cache/NoopCacheService.java deleted file mode 100644 index 4b6ed9a0..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/cache/NoopCacheService.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.commafeed.backend.cache; - -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedSubscription; -import com.commafeed.backend.model.User; -import com.commafeed.frontend.model.Category; -import com.commafeed.frontend.model.UnreadCount; - -public class NoopCacheService extends CacheService { - - @Override - public Set getLastEntries(Feed feed) { - return Collections.emptySet(); - } - - @Override - public void setLastEntries(Feed feed, List entries) { - } - - @Override - public UnreadCount getUnreadCount(FeedSubscription sub) { - return null; - } - - @Override - public void setUnreadCount(FeedSubscription sub, UnreadCount count) { - - } - - @Override - public void invalidateUnreadCount(FeedSubscription... subs) { - - } - - @Override - public Category getUserRootCategory(User user) { - return null; - } - - @Override - public void setUserRootCategory(User user, Category category) { - - } - - @Override - public void invalidateUserRootCategory(User... users) { - - } - -} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisCacheService.java b/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisCacheService.java deleted file mode 100644 index 2f1e2913..00000000 --- a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisCacheService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.commafeed.backend.cache; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import com.commafeed.backend.model.Feed; -import com.commafeed.backend.model.FeedSubscription; -import com.commafeed.backend.model.Models; -import com.commafeed.backend.model.User; -import com.commafeed.frontend.model.Category; -import com.commafeed.frontend.model.UnreadCount; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.Pipeline; - -@Slf4j -@RequiredArgsConstructor -public class RedisCacheService extends CacheService { - - private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); - - private final JedisPool pool; - - @Override - public Set getLastEntries(Feed feed) { - try (Jedis jedis = pool.getResource()) { - String key = buildRedisEntryKey(feed); - return jedis.smembers(key); - } - } - - @Override - public void setLastEntries(Feed feed, List entries) { - try (Jedis jedis = pool.getResource()) { - String key = buildRedisEntryKey(feed); - - Pipeline pipe = jedis.pipelined(); - pipe.del(key); - for (String entry : entries) { - pipe.sadd(key, entry); - } - pipe.expire(key, (int) TimeUnit.DAYS.toSeconds(7)); - pipe.sync(); - } - } - - @Override - public Category getUserRootCategory(User user) { - Category cat = null; - try (Jedis jedis = pool.getResource()) { - String key = buildRedisUserRootCategoryKey(user); - String json = jedis.get(key); - if (json != null) { - cat = MAPPER.readValue(json, Category.class); - } - } catch (Exception e) { - log.error(e.getMessage(), e); - } - return cat; - } - - @Override - public void setUserRootCategory(User user, Category category) { - try (Jedis jedis = pool.getResource()) { - String key = buildRedisUserRootCategoryKey(user); - - Pipeline pipe = jedis.pipelined(); - pipe.del(key); - pipe.set(key, MAPPER.writeValueAsString(category)); - pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30)); - pipe.sync(); - } catch (JsonProcessingException e) { - log.error(e.getMessage(), e); - } - } - - @Override - public UnreadCount getUnreadCount(FeedSubscription sub) { - UnreadCount count = null; - try (Jedis jedis = pool.getResource()) { - String key = buildRedisUnreadCountKey(sub); - String json = jedis.get(key); - if (json != null) { - count = MAPPER.readValue(json, UnreadCount.class); - } - } catch (Exception e) { - log.error(e.getMessage(), e); - } - return count; - } - - @Override - public void setUnreadCount(FeedSubscription sub, UnreadCount count) { - try (Jedis jedis = pool.getResource()) { - String key = buildRedisUnreadCountKey(sub); - - Pipeline pipe = jedis.pipelined(); - pipe.del(key); - pipe.set(key, MAPPER.writeValueAsString(count)); - pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30)); - pipe.sync(); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - } - - @Override - public void invalidateUserRootCategory(User... users) { - try (Jedis jedis = pool.getResource()) { - Pipeline pipe = jedis.pipelined(); - if (users != null) { - for (User user : users) { - String key = buildRedisUserRootCategoryKey(user); - pipe.del(key); - } - } - pipe.sync(); - } - } - - @Override - public void invalidateUnreadCount(FeedSubscription... subs) { - try (Jedis jedis = pool.getResource()) { - Pipeline pipe = jedis.pipelined(); - if (subs != null) { - for (FeedSubscription sub : subs) { - String key = buildRedisUnreadCountKey(sub); - pipe.del(key); - } - } - pipe.sync(); - } - } - - private String buildRedisEntryKey(Feed feed) { - return "f:" + Models.getId(feed); - } - - private String buildRedisUserRootCategoryKey(User user) { - return "c:" + Models.getId(user); - } - - private String buildRedisUnreadCountKey(FeedSubscription sub) { - return "u:" + Models.getId(sub); - } - -} 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 b2237929..b5df83cc 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,6 +1,5 @@ package com.commafeed.backend.feed; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -16,7 +15,6 @@ import org.apache.commons.lang3.StringUtils; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.commafeed.backend.Digests; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.feed.parser.FeedParserResult.Content; @@ -25,7 +23,6 @@ import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.Models; -import com.commafeed.backend.model.User; import com.commafeed.backend.service.FeedEntryService; import com.commafeed.backend.service.FeedService; import com.commafeed.frontend.ws.WebSocketMessageBuilder; @@ -47,29 +44,23 @@ public class FeedRefreshUpdater { private final FeedService feedService; private final FeedEntryService feedEntryService; private final FeedSubscriptionDAO feedSubscriptionDAO; - private final CacheService cache; private final WebSocketSessions webSocketSessions; private final Striped locks; - private final Meter entryCacheMiss; - private final Meter entryCacheHit; private final Meter feedUpdated; private final Meter entryInserted; public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics, - FeedSubscriptionDAO feedSubscriptionDAO, CacheService cache, WebSocketSessions webSocketSessions) { + FeedSubscriptionDAO feedSubscriptionDAO, WebSocketSessions webSocketSessions) { this.unitOfWork = unitOfWork; this.feedService = feedService; this.feedEntryService = feedEntryService; this.feedSubscriptionDAO = feedSubscriptionDAO; - this.cache = cache; this.webSocketSessions = webSocketSessions; locks = Striped.lazyWeakLock(100000); - entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss")); - entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit")); feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated")); entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted")); } @@ -139,39 +130,21 @@ public class FeedRefreshUpdater { Map unreadCountBySubscription = new HashMap<>(); if (!entries.isEmpty()) { - Set lastEntries = cache.getLastEntries(feed); - List currentEntries = new ArrayList<>(); - List subscriptions = null; for (Entry entry : entries) { - String cacheKey = cache.buildUniqueEntryKey(entry); - if (!lastEntries.contains(cacheKey)) { - log.debug("cache miss for {}", entry.url()); - if (subscriptions == null) { - subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed)); - } - AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions); - processed &= addEntryResult.processed; - inserted += addEntryResult.inserted ? 1 : 0; - addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum)); - - entryCacheMiss.mark(); - } else { - log.debug("cache hit for {}", entry.url()); - entryCacheHit.mark(); + if (subscriptions == null) { + subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed)); } - - currentEntries.add(cacheKey); + AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions); + processed &= addEntryResult.processed; + inserted += addEntryResult.inserted ? 1 : 0; + addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum)); } - cache.setLastEntries(feed, currentEntries); - if (subscriptions == null) { + if (inserted == 0) { feed.setMessage("No new entries found"); } else if (inserted > 0) { - log.debug("inserted {} entries for feed {}", inserted, feed.getId()); - List users = subscriptions.stream().map(FeedSubscription::getUser).toList(); - cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); - cache.invalidateUserRootCategory(users.toArray(new User[0])); + feed.setMessage("Found %s new entries".formatted(inserted)); } } 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 f28c5247..4465025c 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 @@ -6,7 +6,6 @@ import java.util.List; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.model.FeedCategory; @@ -28,7 +27,6 @@ public class OPMLImporter { private final FeedCategoryDAO feedCategoryDAO; private final FeedSubscriptionService feedSubscriptionService; - private final CacheService cache; public void importOpml(User user, String xml) throws IllegalArgumentException, FeedException { xml = xml.substring(xml.indexOf('<')); @@ -79,6 +77,5 @@ public class OPMLImporter { log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage()); } } - cache.invalidateUserRootCategory(user); } } 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 7b789042..505245a6 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 @@ -4,7 +4,6 @@ import java.time.Instant; import java.util.List; import com.commafeed.backend.Digests; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -32,7 +31,6 @@ public class FeedEntryService { private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedEntryContentService feedEntryContentService; private final FeedEntryFilteringService feedEntryFilteringService; - private final CacheService cache; public FeedEntry find(Feed feed, Entry entry) { String guidHash = Digests.sha1Hex(entry.guid()); @@ -85,8 +83,6 @@ public class FeedEntryService { if (status.isMarkable()) { status.setRead(read); feedEntryStatusDAO.saveOrUpdate(status); - cache.invalidateUnreadCount(sub); - cache.invalidateUserRootCategory(user); } } @@ -112,8 +108,6 @@ public class FeedEntryService { List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, false, null, null, null); markList(statuses, olderThan, insertedBefore); - cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); - cache.invalidateUserRootCategory(user); } public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) { 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 5f48a7c1..45d6674f 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 @@ -8,7 +8,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -17,7 +16,6 @@ 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 com.commafeed.backend.model.Models; import com.commafeed.backend.model.User; import com.commafeed.frontend.model.UnreadCount; @@ -33,17 +31,15 @@ public class FeedSubscriptionService { private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedService feedService; private final FeedRefreshEngine feedRefreshEngine; - private final CacheService cache; private final CommaFeedConfiguration config; public FeedSubscriptionService(FeedDAO feedDAO, FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO, - FeedService feedService, FeedRefreshEngine feedRefreshEngine, CacheService cache, CommaFeedConfiguration config) { + FeedService feedService, FeedRefreshEngine feedRefreshEngine, CommaFeedConfiguration config) { this.feedDAO = feedDAO; this.feedEntryStatusDAO = feedEntryStatusDAO; this.feedSubscriptionDAO = feedSubscriptionDAO; this.feedService = feedService; this.feedRefreshEngine = feedRefreshEngine; - this.cache = cache; this.config = config; // automatically refresh feeds after they are subscribed to @@ -95,7 +91,6 @@ public class FeedSubscriptionService { sub.setTitle(FeedUtils.truncate(title, 128)); feedSubscriptionDAO.saveOrUpdate(sub); - cache.invalidateUserRootCategory(user); return sub.getId(); } @@ -103,7 +98,6 @@ public class FeedSubscriptionService { FeedSubscription sub = feedSubscriptionDAO.findById(user, subId); if (sub != null) { feedSubscriptionDAO.delete(sub); - cache.invalidateUserRootCategory(user); return true; } else { return false; @@ -130,17 +124,9 @@ public class FeedSubscriptionService { } public Map getUnreadCount(User user) { - return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, this::getUnreadCount)); - } - - private UnreadCount getUnreadCount(FeedSubscription sub) { - UnreadCount count = cache.getUnreadCount(sub); - if (count == null) { - log.debug("unread count cache miss for {}", Models.getId(sub)); - count = feedEntryStatusDAO.getUnreadCount(sub); - cache.setUnreadCount(sub, count); - } - return count; + return feedSubscriptionDAO.findAll(user) + .stream() + .collect(Collectors.toMap(FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount)); } @SuppressWarnings("serial") 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 3a6fd362..668db51c 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 @@ -14,7 +14,6 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -89,7 +88,6 @@ public class CategoryREST { private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedEntryService feedEntryService; private final FeedSubscriptionService feedSubscriptionService; - private final CacheService cache; private final CommaFeedConfiguration config; @Path("/entries") @@ -291,7 +289,6 @@ public class CategoryREST { cat.setParent(parent); } feedCategoryDAO.saveOrUpdate(cat); - cache.invalidateUserRootCategory(user); return Response.ok(cat.getId()).build(); } @@ -321,7 +318,6 @@ public class CategoryREST { feedCategoryDAO.saveOrUpdate(categories); feedCategoryDAO.delete(cat); - cache.invalidateUserRootCategory(user); return Response.ok().build(); } else { return Response.status(Status.NOT_FOUND).build(); @@ -374,7 +370,6 @@ public class CategoryREST { } feedCategoryDAO.saveOrUpdate(category); - cache.invalidateUserRootCategory(user); return Response.ok().build(); } @@ -393,7 +388,6 @@ public class CategoryREST { } category.setCollapsed(req.isCollapse()); feedCategoryDAO.saveOrUpdate(category); - cache.invalidateUserRootCategory(user); return Response.ok().build(); } @@ -418,18 +412,14 @@ public class CategoryREST { responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Category.class))) }) public Response getRootCategory() { User user = authenticationContext.getCurrentUser(); - Category root = cache.getUserRootCategory(user); - if (root == null) { - log.debug("tree cache miss for {}", user.getId()); - List categories = feedCategoryDAO.findAll(user); - List subscriptions = feedSubscriptionDAO.findAll(user); - Map unreadCount = feedSubscriptionService.getUnreadCount(user); - root = buildCategory(null, categories, subscriptions, unreadCount); - root.setId("all"); - root.setName("All"); - cache.setUserRootCategory(user, root); - } + 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"); return Response.ok(root).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 685867e5..33543b78 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 @@ -17,7 +17,6 @@ import org.jboss.resteasy.reactive.RestForm; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -114,7 +113,6 @@ public class FeedREST { private final FeedRefreshEngine feedRefreshEngine; private final OPMLImporter opmlImporter; private final OPMLExporter opmlExporter; - private final CacheService cache; private final CommaFeedConfiguration config; private static FeedEntry initTestEntry() { @@ -492,7 +490,6 @@ public class FeedREST { } else { feedSubscriptionDAO.saveOrUpdate(subscription); } - cache.invalidateUserRootCategory(user); return Response.ok().build(); } 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 ae89f32e..08cd6f90 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 @@ -7,7 +7,6 @@ import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.User; @@ -39,12 +38,11 @@ class OPMLImporterTest { private void testOpmlVersion(String fileName) throws IOException, IllegalArgumentException, FeedException { FeedCategoryDAO feedCategoryDAO = Mockito.mock(FeedCategoryDAO.class); FeedSubscriptionService feedSubscriptionService = Mockito.mock(FeedSubscriptionService.class); - CacheService cacheService = Mockito.mock(CacheService.class); User user = Mockito.mock(User.class); String xml = IOUtils.toString(getClass().getResourceAsStream(fileName), StandardCharsets.UTF_8); - OPMLImporter importer = new OPMLImporter(feedCategoryDAO, feedSubscriptionService, cacheService); + OPMLImporter importer = new OPMLImporter(feedCategoryDAO, feedSubscriptionService); importer.importOpml(user, xml); Mockito.verify(feedSubscriptionService) From 21ce9db4b02922da54cf4a1f8b4c9fe28ded61dc Mon Sep 17 00:00:00 2001 From: Athou Date: Mon, 12 Aug 2024 22:43:41 +0200 Subject: [PATCH 13/50] README update --- README.md | 73 ++++++++++++++++++++++++++++++---------- commafeed-server/TODO.md | 16 --------- 2 files changed, 56 insertions(+), 33 deletions(-) delete mode 100644 commafeed-server/TODO.md diff --git a/README.md b/README.md index 3c8c0942..79095e07 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CommaFeed -Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript. +Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript. ![preview](https://user-images.githubusercontent.com/1256795/184886828-1973f148-58a9-4c6d-9587-ee5e5d3cc2cb.png) @@ -16,6 +16,12 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ - OPML import/export - REST API and a Fever-compatible API for native mobile apps - [Browser extension](https://github.com/Athou/commafeed-browser-extension) +- Compiles to native code for very fast startup and low memory usage +- Supports 4 databases + - H2 (embedded database) + - PostgreSQL + - MySQL + - MariaDB ## Deployment @@ -33,28 +39,31 @@ PikaPods shares 20% of the revenue back to CommaFeed. [![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed) -### Download precompiled package +### Download a precompiled package - mkdir commafeed && cd commafeed - wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar - wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml - java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml +Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating +system and database of choice. -The server will listen on http://localhost:8082. The default -user is `admin` and the default password is `admin`. +There are two types of packages: + +- The `linux` and `windows` packages are compiled natively and contain an executable that can be run directly. +- The `jvm` package contains a Java `.jar` file that works on all platforms and is started with + `java -jar quarkus-run.jar`. + +If available for your operating system, the native package is recommended because it has a faster startup time and lower +memory usage. ### Build from sources - git clone https://github.com/Athou/commafeed.git - cd commafeed - ./mvnw clean package - cp commafeed-server/config.yml.example config.yml - java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml + ./mvnw clean package -P [-DskipTests] [-Pnative] -The server will listen on http://localhost:8082. The default -user is `admin` and the default password is `admin`. +- `` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. +- `-DskipTests` is optional but recommended because tests require a Docker environment to run against a real database. +- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (GRAALVM_HOME environment + variable + pointing to a GraalVM installation). -### Memory management +### Memory management (`jvm` package only) The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the operating system. This is because acquiring memory from the operating system is a relatively expensive operation. @@ -86,6 +95,36 @@ IBM provides precompiled binaries for OpenJ9 named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/). This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/blob/master/Dockerfile). +## Configuration + +There are multiple ways to configure CommaFeed: + +- a properties file in `config/application.properties` (kebab-case) +- Command line arguments prefixed with `-D` (kebab-case) +- Environment variables (UPPER_CASE) +- an .env file in the working directory (UPPER_CASE) + +The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos. + +CommaFeed only requires 3 properties to be configured: + +- `quarkus.datasource.username` +- `quarkus.datasource.password` +- `quarkus.datasource.jdbc-url` + - e.g. for H2: `jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE` + - e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed` + - e.g. for MySQL: + `jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` + - e.g. for MariaDB: + `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` + +All +other [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) +are optional and have sensible default values. + +When started, the server will listen on http://localhost:8082. +The default user is `admin` and the default password is `admin`. + ## Translation Files for internationalization are @@ -108,7 +147,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6 - Open `commafeed-server` in your preferred Java IDE. - CommaFeed uses Lombok, you need the Lombok plugin for your IDE. -- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments +- run `mvn quarkus:dev` ### Frontend diff --git a/commafeed-server/TODO.md b/commafeed-server/TODO.md deleted file mode 100644 index b314b87a..00000000 --- a/commafeed-server/TODO.md +++ /dev/null @@ -1,16 +0,0 @@ -TODO ----- - -MVP: - -- update readme -- update docker readme -- update release notes (+ mention h2 migration has been removed, upgrade to last 4.x is required) - -Nice to have: - -- find a better way to scan rome classes -- remove suppresswarnings "deprecation" -- remove rest assured or use only rest assured -- rename "servlets" since they are now rest endpoints -- warnings hibernate on startup From 9a43fd434f9f4413433c58e0378ed59539824047 Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 08:08:20 +0200 Subject: [PATCH 14/50] ci for quarkus branch --- .dockerignore | 1 + .github/workflows/ci.yml | 66 ++++++++++--------- commafeed-client/pom.xml | 2 +- commafeed-server/pom.xml | 4 +- ...okieMaxAgeFormAuthenticationMechanism.java | 29 +------- pom.xml | 2 +- 6 files changed, 43 insertions(+), 61 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..cbcd6b3e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +commafeed-client \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c563ce47..bc00e4d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: [ push ] env: JAVA_VERSION: 21 + DOCKER_BUILD_SUMMARY: false jobs: build-linux: @@ -53,7 +54,7 @@ jobs: # Docker - name: Login to Container Registry uses: docker/login-action@v3 - if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }} + if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' || github.ref_name == 'quarkus' }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -63,62 +64,67 @@ jobs: uses: docker/build-push-action@v6 if: ${{ github.ref_type == 'tag' }} with: - context: commafeed-server - file: src/main/docker/Dockerfile.native + context: . + file: commafeed-server/src/main/docker/Dockerfile.native push: true platforms: linux/amd64 tags: | - athou/commafeed:latest-native - athou/commafeed:${{ github.ref_name }}-native + athou/commafeed:latest-${{ matrix.database }} + athou/commafeed:${{ github.ref_name }}-${{ matrix.database }} - name: Docker build and push tag - jvm uses: docker/build-push-action@v6 if: ${{ github.ref_type == 'tag' }} with: - context: commafeed-server - file: src/main/docker/Dockerfile.jvm + context: . + file: commafeed-server/src/main/docker/Dockerfile.jvm push: true - platforms: linux/arm64/v8 + platforms: linux/amd64,linux/arm64/v8 tags: | - athou/commafeed:latest-jvm - athou/commafeed:${{ github.ref_name }}-jvm - - - name: Docker merge tag manifests - uses: Noelware/docker-manifest-action@0.4.2 - if: ${{ github.ref_type == 'tag' }} - with: - inputs: athou/commafeed:latest - images: athou/commafeed:latest-native,athou/commafeed:latest-jvm - push: true + athou/commafeed:latest-${{ matrix.database }}-jvm + athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm ## master - name: Docker build and push master - native uses: docker/build-push-action@v6 if: ${{ github.ref_name == 'master' }} with: - context: commafeed-server - file: src/main/docker/Dockerfile.native + context: . + file: commafeed-server/src/main/docker/Dockerfile.native push: true platforms: linux/amd64 - tags: athou/commafeed:master-native + tags: athou/commafeed:master-${{ matrix.database }} - name: Docker build and push master - jvm uses: docker/build-push-action@v6 if: ${{ github.ref_name == 'master' }} with: - context: commafeed-server - file: src/main/docker/Dockerfile.jvm + context: . + file: commafeed-server/src/main/docker/Dockerfile.jvm push: true - platforms: linux/arm64/v8 - tags: athou/commafeed:master-jvm + platforms: linux/amd64,linux/arm64/v8 + tags: athou/commafeed:master-${{ matrix.database }}-jvm - - name: Docker merge master manifests - uses: Noelware/docker-manifest-action@0.4.2 - if: ${{ github.ref_name == 'master' }} + ## quarkus branch - remove when merged into master, also remove the condition in the login step + - name: Docker build and push quarkus - native + uses: docker/build-push-action@v6 + if: ${{ github.ref_name == 'quarkus' }} with: - inputs: athou/commafeed:master - images: athou/commafeed:master-native,athou/commafeed:master-jvm + context: . + file: commafeed-server/src/main/docker/Dockerfile.native push: true + platforms: linux/amd64 + tags: athou/commafeed:quarkus-${{ matrix.database }} + + - name: Docker build and push quarkus - jvm + uses: docker/build-push-action@v6 + if: ${{ github.ref_name == 'quarkus' }} + with: + context: . + file: commafeed-server/src/main/docker/Dockerfile.jvm + push: true + platforms: linux/amd64,linux/arm64/v8 + tags: athou/commafeed:quarkus-${{ matrix.database }}-jvm build-windows: runs-on: windows-latest diff --git a/commafeed-client/pom.xml b/commafeed-client/pom.xml index c55058fb..753ee1ec 100644 --- a/commafeed-client/pom.xml +++ b/commafeed-client/pom.xml @@ -6,7 +6,7 @@ com.commafeed commafeed - 4.6.0 + 5.0.0-beta commafeed-client CommaFeed Client diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index fbd54e20..95b2c255 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -6,7 +6,7 @@ com.commafeed commafeed - 4.6.0 + 5.0.0-beta commafeed-server CommaFeed Server @@ -229,7 +229,7 @@ com.commafeed commafeed-client - 4.6.0 + 5.0.0-beta diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java index 97445772..c5ffa581 100644 --- a/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java +++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/CookieMaxAgeFormAuthenticationMechanism.java @@ -2,26 +2,20 @@ package com.commafeed.security.mechanism; import java.security.SecureRandom; import java.util.Base64; -import java.util.Set; -import io.quarkus.security.identity.IdentityProviderManager; -import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.identity.request.AuthenticationRequest; import io.quarkus.vertx.http.runtime.FormAuthConfig; import io.quarkus.vertx.http.runtime.FormAuthRuntimeConfig; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; -import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; -import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; import io.quarkus.vertx.http.runtime.security.PersistentLoginManager; -import io.smallrye.mutiny.Uni; import io.vertx.core.http.Cookie; import io.vertx.core.http.impl.ServerCookie; import io.vertx.ext.web.RoutingContext; import jakarta.annotation.Priority; import jakarta.inject.Singleton; +import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; /** @@ -37,6 +31,7 @@ public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticati // the temp encryption key, persistent across dev mode restarts static volatile String encryptionKey; + @Delegate private final FormAuthenticationMechanism delegate; public CookieMaxAgeFormAuthenticationMechanism(HttpConfiguration httpConfiguration, HttpBuildTimeConfig buildTimeConfig) { @@ -86,26 +81,6 @@ public class CookieMaxAgeFormAuthenticationMechanism implements HttpAuthenticati landingPage, redirectAfterLogin, locationCookie, cookieSameSite, cookiePath, loginManager); } - @Override - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - return delegate.authenticate(context, identityProviderManager); - } - - @Override - public Uni getChallenge(RoutingContext context) { - return delegate.getChallenge(context); - } - - @Override - public Set> getCredentialTypes() { - return delegate.getCredentialTypes(); - } - - @Override - public Uni getCredentialTransport(RoutingContext context) { - return delegate.getCredentialTransport(context); - } - private static String startWithSlash(String page) { if (page == null) { return null; diff --git a/pom.xml b/pom.xml index da9fb825..7d321f30 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.commafeed commafeed - 4.6.0 + 5.0.0-beta CommaFeed pom From aabbf0a5d1a6cc9dff44fc0e904bfb7662628bfd Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 12:32:16 +0200 Subject: [PATCH 15/50] use a relative link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79095e07..54c88742 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ CommaFeed only requires 3 properties to be configured: `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` All -other [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) +other [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) are optional and have sensible default values. When started, the server will listen on http://localhost:8082. From 3b564961961c089a797481233c35ea20bbe4f46c Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 12:39:59 +0200 Subject: [PATCH 16/50] javadoc tweaks --- .../com/commafeed/CommaFeedConfiguration.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 1b5c225a..362face9 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -122,6 +122,8 @@ public interface CommaFeedConfiguration { /** * 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(); @@ -143,25 +145,33 @@ public interface CommaFeedConfiguration { interface Cleanup { /** - * Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable. + * 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. + * 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. + * 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. + * Limit the number of feeds a user can subscribe to. + * + * 0 to disable. */ @WithDefault("0") int maxFeedsPerUser(); From 77b6cf75a5ede1d86c5e35b0578440805b4cd0e6 Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 15:05:40 +0200 Subject: [PATCH 17/50] README tweaks --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 54c88742..dfa28d62 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ memory usage. - `` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. - `-DskipTests` is optional but recommended because tests require a Docker environment to run against a real database. -- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (GRAALVM_HOME environment +- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment variable pointing to a GraalVM installation). @@ -99,10 +99,10 @@ This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/bl There are multiple ways to configure CommaFeed: -- a properties file in `config/application.properties` (kebab-case) -- Command line arguments prefixed with `-D` (kebab-case) -- Environment variables (UPPER_CASE) -- an .env file in the working directory (UPPER_CASE) +- a properties file in `config/application.properties` (keys in kebab-case) +- Command line arguments prefixed with `-D` (keys in kebab-case) +- Environment variables (keys in UPPER_CASE) +- an .env file in the working directory (keys in UPPER_CASE) The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos. @@ -147,7 +147,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6 - Open `commafeed-server` in your preferred Java IDE. - CommaFeed uses Lombok, you need the Lombok plugin for your IDE. -- run `mvn quarkus:dev` +- run `./mvnw quarkus:dev` ### Frontend From f2c6734c79af9a9fedf7363713ddee0552d2a620 Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 15:57:46 +0200 Subject: [PATCH 18/50] fix warning in native mode about parser not found --- .../FeedEntryContentCleaningService.java | 3 +- .../FeedEntryContentCleaningServiceTest.java | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryContentCleaningServiceTest.java 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 f3e90352..da266b0a 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 @@ -20,6 +20,7 @@ import org.w3c.css.sac.InputSource; import org.w3c.dom.css.CSSStyleDeclaration; import com.steadystate.css.parser.CSSOMParser; +import com.steadystate.css.parser.SACParserCSS3; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @@ -149,7 +150,7 @@ public class FeedEntryContentCleaningService { } private CSSOMParser buildCssParser() { - CSSOMParser parser = new CSSOMParser(); + CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); parser.setErrorHandler(new ErrorHandler() { @Override 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 new file mode 100644 index 00000000..3a23b95b --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/FeedEntryContentCleaningServiceTest.java @@ -0,0 +1,31 @@ +package com.commafeed.backend.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class FeedEntryContentCleaningServiceTest { + + private final FeedEntryContentCleaningService feedEntryContentCleaningService = new FeedEntryContentCleaningService(); + + @Test + void testClean() { + String content = """ +

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

+ Some text + alt-desc + + aaa +

+ """.lines(), result.lines()); + } + +} \ No newline at end of file From a92df774bd3d9bc417eaa0d065350f549e7a8072 Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 16:13:05 +0200 Subject: [PATCH 19/50] rome needs to clone Date in native mode --- .../src/main/java/com/commafeed/NativeImageClasses.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java index 5f6b145b..c1715beb 100644 --- a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java +++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java @@ -15,7 +15,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection; MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class, // rome - com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class, + java.util.Date.class, com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class, com.rometools.modules.content.ContentModuleImpl.class, com.rometools.modules.mediarss.MediaModuleImpl.class, com.rometools.modules.mediarss.MediaEntryModuleImpl.class, From 857736adad18faa22a1836ea21b09d8995bbe2a3 Mon Sep 17 00:00:00 2001 From: Athou Date: Tue, 13 Aug 2024 17:26:51 +0200 Subject: [PATCH 20/50] README tweaks --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dfa28d62..b17ea2c9 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ system and database of choice. There are two types of packages: - The `linux` and `windows` packages are compiled natively and contain an executable that can be run directly. -- The `jvm` package contains a Java `.jar` file that works on all platforms and is started with - `java -jar quarkus-run.jar`. +- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all + platforms and is started with `java -jar quarkus-run.jar`. If available for your operating system, the native package is recommended because it has a faster startup time and lower memory usage. @@ -63,6 +63,14 @@ memory usage. variable pointing to a GraalVM installation). +When the build is complete: + +- if you used the native profile, the executable is located at + `commafeed-server/target/commafeed-----runner[.exe]` +- if you did not use the native profile, a zip containing all jars required to run the application is located at + `commafeed-server/target/commafeed--.zip`. Extract it and run the application with + `java -jar quarkus-run.jar` + ### Memory management (`jvm` package only) The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the @@ -118,8 +126,7 @@ CommaFeed only requires 3 properties to be configured: - e.g. for MariaDB: `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` -All -other [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) +All other [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) are optional and have sensible default values. When started, the server will listen on http://localhost:8082. From 69cd90edd8ffae0d68b491a71f1deaaf5885c560 Mon Sep 17 00:00:00 2001 From: Athou Date: Wed, 14 Aug 2024 16:00:47 +0200 Subject: [PATCH 21/50] only use rest-assured for tests --- commafeed-server/pom.xml | 19 --- .../com/commafeed/integration/BaseIT.java | 53 ++++---- .../com/commafeed/integration/SecurityIT.java | 114 +++++++++--------- .../commafeed/integration/WebSocketIT.java | 15 +-- .../commafeed/integration/rest/AdminIT.java | 38 ++++-- .../commafeed/integration/rest/FeedIT.java | 99 +++++++++------ .../commafeed/integration/rest/FeverIT.java | 36 +++--- .../commafeed/integration/rest/ServerIT.java | 3 +- .../commafeed/integration/rest/UserIT.java | 8 +- .../integration/servlet/CustomCodeIT.java | 45 +++---- .../integration/servlet/LogoutIT.java | 23 ++-- .../integration/servlet/NextUnreadIT.java | 32 ++--- .../integration/servlet/RobotsTxtIT.java | 8 +- 13 files changed, 248 insertions(+), 245 deletions(-) diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 95b2c255..f8747493 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -15,7 +15,6 @@ 3.13.1 6.6 2.1.0 - 3.1.8 1.2.1 h2 @@ -441,24 +440,6 @@ 5.15.0 test
- - org.glassfish.jersey.core - jersey-client - ${jersey.version} - test - - - org.glassfish.jersey.media - jersey-media-multipart - ${jersey.version} - test - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${jersey.version} - test - io.rest-assured rest-assured 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 0d2f40a0..00ebe118 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java @@ -11,8 +11,6 @@ import java.util.Objects; import org.apache.commons.io.IOUtils; import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.mockserver.client.MockServerClient; @@ -20,19 +18,15 @@ import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import com.commafeed.JacksonCustomizer; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Subscription; import com.commafeed.frontend.model.request.SubscribeRequest; -import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.Header; import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.MediaType; import lombok.Getter; @Getter @@ -47,17 +41,10 @@ public abstract class BaseIT { private String apiBaseUrl; private String webSocketUrl; - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base; - } - @BeforeEach void init() throws IOException { this.mockServerClient = ClientAndServer.startClientAndServer(0); - ObjectMapper mapper = new ObjectMapper(); - new JacksonCustomizer().customize(mapper); - this.client = configureClientBuilder(new JerseyClientBuilder().register(new JacksonJsonProvider(mapper))).build(); this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/"; this.baseUrl = "http://localhost:8085/"; this.apiBaseUrl = this.baseUrl + "rest/"; @@ -87,13 +74,11 @@ public abstract class BaseIT { } protected List login() { - Form form = new Form(); - form.param("j_username", "admin"); - form.param("j_password", "admin"); - List
setCookieHeaders = RestAssured.given() + .auth() + .none() .formParams("j_username", "admin", "j_password", "admin") - .post(baseUrl + "j_security_check") + .post("j_security_check") .then() .statusCode(HttpStatus.SC_OK) .extract() @@ -106,7 +91,14 @@ public abstract class BaseIT { SubscribeRequest subscribeRequest = new SubscribeRequest(); subscribeRequest.setUrl(feedUrl); subscribeRequest.setTitle("my title for this feed"); - return client.target(apiBaseUrl + "feed/subscribe").request().post(Entity.json(subscribeRequest), Long.class); + return RestAssured.given() + .body(subscribeRequest) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/feed/subscribe") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Long.class); } protected Long subscribeAndWaitForEntries(String feedUrl) { @@ -116,19 +108,24 @@ public abstract class BaseIT { } protected Subscription getSubscription(Long subscriptionId) { - return client.target(apiBaseUrl + "feed/get/{id}").resolveTemplate("id", subscriptionId).request().get(Subscription.class); + return RestAssured.given() + .get("rest/feed/get/{id}", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Subscription.class); } protected Entries getFeedEntries(long subscriptionId) { - Response response = client.target(apiBaseUrl + "feed/entries") - .queryParam("id", subscriptionId) - .queryParam("readType", "all") - .request() - .get(); - return response.readEntity(Entries.class); + return RestAssured.given() + .get("rest/feed/entries?id={id}&readType=all", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Entries.class); } protected void forceRefreshAllFeeds() { - client.target(apiBaseUrl + "feed/refreshAll").request().get(Void.class); + RestAssured.given().get("rest/feed/refreshAll").then().statusCode(HttpStatus.SC_OK); } } 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 5ba7c6c6..f2b1e124 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java @@ -1,7 +1,6 @@ package com.commafeed.integration; import java.net.HttpCookie; -import java.util.Base64; import java.util.List; import java.util.stream.Collectors; @@ -16,114 +15,117 @@ import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.SubscribeRequest; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.client.Entity; +import io.restassured.RestAssured; import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.MediaType; @QuarkusTest class SecurityIT extends BaseIT { @Test void notLoggedIn() { - try (Response response = getClient().target(getApiBaseUrl() + "user/profile").request().get()) { - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); - } + RestAssured.given().get("rest/user/profile").then().statusCode(HttpStatus.SC_UNAUTHORIZED); } @Test void formLogin() { List cookies = login(); + cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0)); - try (Response response = getClient().target(getApiBaseUrl() + "user/profile") - .request() + RestAssured.given() .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .get()) { - Assertions.assertEquals(HttpStatus.SC_OK, response.getStatus()); - cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0)); - } + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); } @Test void basicAuthLogin() { - String auth = "Basic " + Base64.getEncoder().encodeToString("admin:admin".getBytes()); - try (Response response = getClient().target(getApiBaseUrl() + "user/profile") - .request() - .header(HttpHeaders.AUTHORIZATION, auth) - .get()) { - Assertions.assertEquals(HttpStatus.SC_OK, response.getStatus()); - } + RestAssured.given().auth().preemptive().basic("admin", "admin").get("rest/user/profile").then().statusCode(HttpStatus.SC_OK); } @Test void wrongPassword() { - String auth = "Basic " + Base64.getEncoder().encodeToString("admin:wrong-password".getBytes()); - try (Response response = getClient().target(getApiBaseUrl() + "user/profile") - .request() - .header(HttpHeaders.AUTHORIZATION, auth) - .get()) { - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); - } + RestAssured.given() + .auth() + .preemptive() + .basic("admin", "wrong-password") + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); } @Test void missingRole() { - String auth = "Basic " + Base64.getEncoder().encodeToString("demo:demo".getBytes()); - try (Response response = getClient().target(getApiBaseUrl() + "admin/metrics") - .request() - .header(HttpHeaders.AUTHORIZATION, auth) - .get()) { - Assertions.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus()); - } + RestAssured.given().auth().preemptive().basic("demo", "demo").get("rest/admin/metrics").then().statusCode(HttpStatus.SC_FORBIDDEN); } @Test void apiKey() { - String auth = "Basic " + Base64.getEncoder().encodeToString("admin:admin".getBytes()); - // create api key ProfileModificationRequest req = new ProfileModificationRequest(); req.setCurrentPassword("admin"); req.setNewApiKey(true); - getClient().target(getApiBaseUrl() + "user/profile") - .request() - .header(HttpHeaders.AUTHORIZATION, auth) - .post(Entity.json(req)) - .close(); + RestAssured.given() + .auth() + .preemptive() + .basic("admin", "admin") + .body(req) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_OK); // fetch api key - String apiKey = getClient().target(getApiBaseUrl() + "user/profile") - .request() - .header(HttpHeaders.AUTHORIZATION, auth) - .get(UserModel.class) + String apiKey = RestAssured.given() + .auth() + .preemptive() + .basic("admin", "admin") + .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 = getClient().target(getApiBaseUrl() + "feed/subscribe") - .request() - .header(HttpHeaders.AUTHORIZATION, auth) - .post(Entity.json(subscribeRequest), Long.class); + long subscriptionId = RestAssured.given() + .auth() + .preemptive() + .basic("admin", "admin") + .body(subscribeRequest) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/feed/subscribe") + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(Long.class); // get entries with api key - Entries entries = getClient().target(getApiBaseUrl() + "feed/entries") + Entries entries = RestAssured.given() .queryParam("id", subscriptionId) .queryParam("readType", "unread") .queryParam("apiKey", apiKey) - .request() - .get(Entries.class); + .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); - try (Response markResponse = getClient().target(getApiBaseUrl() + "entry/mark") + RestAssured.given() + .body(markRequest) + .contentType(MediaType.APPLICATION_JSON) .queryParam("apiKey", apiKey) - .request() - .post(Entity.json(markRequest))) { - Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, markResponse.getStatus()); - } + .post("rest/entry/mark") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); } } 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 d5acb22c..bfeb64ea 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java @@ -11,15 +11,16 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.request.FeedModificationRequest; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.CloseReason; import jakarta.websocket.ContainerProvider; @@ -27,17 +28,17 @@ import jakarta.websocket.DeploymentException; import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; import jakarta.websocket.Session; -import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; import lombok.extern.slf4j.Slf4j; @QuarkusTest @Slf4j class WebSocketIT extends BaseIT { - @Override - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base.register(HttpAuthenticationFeature.basic("admin", "admin")); + @BeforeEach + void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); } @Test @@ -94,7 +95,7 @@ class WebSocketIT extends BaseIT { req.setId(subscriptionId); req.setName("feed-name"); req.setFilter("!title.contains('item 4')"); - getClient().target(getApiBaseUrl() + "feed/modify").request().post(Entity.json(req), Void.class); + RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK); AtomicBoolean connected = new AtomicBoolean(); AtomicReference messageRef = new AtomicReference<>(); 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 c6e53815..ceeef49f 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,11 +1,10 @@ package com.commafeed.integration.rest; -import java.util.Arrays; import java.util.List; -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -15,14 +14,15 @@ import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.client.Entity; +import io.restassured.RestAssured; +import jakarta.ws.rs.core.MediaType; @QuarkusTest class AdminIT extends BaseIT { - @Override - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base.register(HttpAuthenticationFeature.basic("admin", "admin")); + @BeforeEach + void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); } @Nested @@ -46,7 +46,12 @@ class AdminIT extends BaseIT { user.setName("test"); user.setPassword("test".getBytes()); user.setEmail("test@test.com"); - getClient().target(getApiBaseUrl() + "admin/user/save").request().post(Entity.json(user), Void.TYPE); + RestAssured.given() + .body(user) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/admin/user/save") + .then() + .statusCode(HttpStatus.SC_OK); } private void modifyUser() { @@ -56,7 +61,12 @@ class AdminIT extends BaseIT { .findFirst() .orElseThrow(() -> new NullPointerException("User not found")); user.setEmail("new-email@provider.com"); - getClient().target(getApiBaseUrl() + "admin/user/save").request().post(Entity.json(user), Void.TYPE); + RestAssured.given() + .body(user) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/admin/user/save") + .then() + .statusCode(HttpStatus.SC_OK); } private void deleteUser() { @@ -68,11 +78,17 @@ class AdminIT extends BaseIT { IDRequest req = new IDRequest(); req.setId(user.getId()); - getClient().target(getApiBaseUrl() + "admin/user/delete").request().post(Entity.json(req), Void.TYPE); + RestAssured.given() + .body(req) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/admin/user/delete") + .then() + .statusCode(HttpStatus.SC_OK); } private List getAllUsers() { - return Arrays.asList(getClient().target(getApiBaseUrl() + "admin/user/getAll").request().get(UserModel[].class)); + 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/FeedIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java index 6b561c2d..1e91f079 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 @@ -11,12 +11,8 @@ import java.util.Objects; import org.apache.commons.io.IOUtils; import org.apache.hc.core5.http.HttpStatus; import org.awaitility.Awaitility; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; -import org.glassfish.jersey.media.multipart.MultiPart; -import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,16 +26,15 @@ import com.commafeed.frontend.model.request.MarkRequest; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.client.Entity; +import io.restassured.RestAssured; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; @QuarkusTest class FeedIT extends BaseIT { - @Override - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base.register(HttpAuthenticationFeature.basic("admin", "admin")); + @BeforeEach + void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); } @Nested @@ -49,7 +44,14 @@ class FeedIT extends BaseIT { FeedInfoRequest req = new FeedInfoRequest(); req.setUrl(getFeedUrl()); - FeedInfo feedInfo = getClient().target(getApiBaseUrl() + "feed/fetch").request().post(Entity.json(req), FeedInfo.class); + FeedInfo feedInfo = RestAssured.given() + .body(req) + .contentType(MediaType.APPLICATION_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()); } @@ -65,13 +67,13 @@ class FeedIT extends BaseIT { @Test void subscribeFromUrl() { - try (Response response = getClient().target(getApiBaseUrl() + "feed/subscribe") + RestAssured.given() .queryParam("url", getFeedUrl()) - .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) - .request() - .get()) { - Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); - } + .redirects() + .follow(false) + .get("rest/feed/subscribe") + .then() + .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT); } @Test @@ -89,9 +91,13 @@ class FeedIT extends BaseIT { IDRequest request = new IDRequest(); request.setId(subscriptionId); - try (Response response = getClient().target(getApiBaseUrl() + "feed/unsubscribe").request().post(Entity.json(request))) { - return response.getStatus(); - } + return RestAssured.given() + .body(request) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/feed/unsubscribe") + .then() + .extract() + .statusCode(); } } @@ -137,7 +143,13 @@ class FeedIT extends BaseIT { request.setId(String.valueOf(subscriptionId)); request.setOlderThan(olderThan == null ? null : olderThan.toEpochMilli()); request.setInsertedBefore(insertedBefore == null ? null : insertedBefore.toEpochMilli()); - getClient().target(getApiBaseUrl() + "feed/mark").request().post(Entity.json(request), Void.TYPE); + + RestAssured.given() + .body(request) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/feed/mark") + .then() + .statusCode(HttpStatus.SC_OK); } } @@ -152,7 +164,12 @@ class FeedIT extends BaseIT { IDRequest request = new IDRequest(); request.setId(subscriptionId); - getClient().target(getApiBaseUrl() + "feed/refresh").request().post(Entity.json(request), Void.TYPE); + RestAssured.given() + .body(request) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/feed/refresh") + .then() + .statusCode(HttpStatus.SC_OK); Awaitility.await() .atMost(Duration.ofSeconds(15)) @@ -165,7 +182,7 @@ class FeedIT extends BaseIT { // mariadb/mysql timestamp precision is 1 second Instant threshold = Instant.now().minus(Duration.ofSeconds(1)); - getClient().target(getApiBaseUrl() + "feed/refreshAll").request().get(Void.TYPE); + forceRefreshAllFeeds(); Awaitility.await() .atMost(Duration.ofSeconds(15)) @@ -185,7 +202,12 @@ class FeedIT extends BaseIT { req.setId(subscriptionId); req.setName("new name"); req.setCategoryId(subscription.getCategoryId()); - getClient().target(getApiBaseUrl() + "feed/modify").request().post(Entity.json(req), Void.TYPE); + RestAssured.given() + .body(req) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/feed/modify") + .then() + .statusCode(HttpStatus.SC_OK); subscription = getSubscription(subscriptionId); Assertions.assertEquals("new name", subscription.getName()); @@ -198,10 +220,13 @@ class FeedIT extends BaseIT { void favicon() throws IOException { Long subscriptionId = subscribe(getFeedUrl()); - byte[] icon = getClient().target(getApiBaseUrl() + "feed/favicon/{id}") - .resolveTemplate("id", subscriptionId) - .request() - .get(byte[].class); + byte[] icon = RestAssured.given() + .get("rest/feed/favicon/{id}", subscriptionId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .response() + .asByteArray(); byte[] defaultFavicon = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif"))); Assertions.assertArrayEquals(defaultFavicon, icon); } @@ -210,22 +235,20 @@ class FeedIT extends BaseIT { @Nested class Opml { @Test - void importExportOpml() throws IOException { + void importExportOpml() { importOpml(); - String opml = getClient().target(getApiBaseUrl() + "feed/export").request().get(String.class); + 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() throws IOException { + void importOpml() { InputStream stream = Objects.requireNonNull(getClass().getResourceAsStream("/opml/opml_v2.0.xml")); - try (MultiPart multiPart = new MultiPart()) { - multiPart.bodyPart(new StreamDataBodyPart("file", stream)); - multiPart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); - getClient().target(getApiBaseUrl() + "feed/import") - .request() - .post(Entity.entity(multiPart, multiPart.getMediaType()), Void.TYPE); - } + RestAssured.given() + .multiPart("file", "opml_v2.0.xml", stream, MediaType.MULTIPART_FORM_DATA) + .post("rest/feed/import") + .then() + .statusCode(HttpStatus.SC_OK); } } 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 8a52a003..533e034b 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,7 +1,6 @@ package com.commafeed.integration.rest; -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +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; @@ -13,8 +12,8 @@ import com.commafeed.frontend.resource.fever.FeverResponse; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Form; +import io.restassured.RestAssured; +import jakarta.ws.rs.core.MediaType; @QuarkusTest class FeverIT extends BaseIT { @@ -22,21 +21,18 @@ class FeverIT extends BaseIT { private Long userId; private String apiKey; - @Override - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base.register(HttpAuthenticationFeature.basic("admin", "admin")); - } - @BeforeEach void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); + // create api key ProfileModificationRequest req = new ProfileModificationRequest(); req.setCurrentPassword("admin"); req.setNewApiKey(true); - getClient().target(getApiBaseUrl() + "user/profile").request().post(Entity.json(req), Void.TYPE); + RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/user/profile").then().statusCode(HttpStatus.SC_OK); // retrieve api key - UserModel user = getClient().target(getApiBaseUrl() + "user/profile").request().get(UserModel.class); + UserModel user = RestAssured.given().get("rest/user/profile").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel.class); this.apiKey = user.getApiKey(); this.userId = user.getId(); } @@ -72,13 +68,15 @@ class FeverIT extends BaseIT { } private FeverResponse fetch(String what, String apiKey) { - Form form = new Form(); - form.param("api_key", Digests.md5Hex("admin:" + apiKey)); - form.param(what, "1"); - - return getClient().target(getApiBaseUrl() + "fever/user/{userId}") - .resolveTemplate("userId", userId) - .request() - .post(Entity.form(form), FeverResponse.class); + return RestAssured.given() + .auth() + .none() + .formParam("api_key", Digests.md5Hex("admin:" + apiKey)) + .formParam(what, 1) + .post("rest/fever/user/{userId}", userId) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .as(FeverResponse.class); } } 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 2a9a6958..3977d967 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 @@ -7,13 +7,14 @@ import com.commafeed.frontend.model.ServerInfo; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; @QuarkusTest class ServerIT extends BaseIT { @Test void getServerInfos() { - ServerInfo serverInfos = getClient().target(getApiBaseUrl() + "server/get").request().get(ServerInfo.class); + 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()); 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 60f6d317..2e7b6cb4 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 @@ -11,9 +11,10 @@ import com.commafeed.integration.BaseIT; import io.quarkus.mailer.MockMailbox; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import io.vertx.ext.mail.MailMessage; import jakarta.inject.Inject; -import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; @QuarkusTest class UserIT extends BaseIT { @@ -23,6 +24,8 @@ class UserIT extends BaseIT { @BeforeEach void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); + mailbox.clear(); } @@ -30,8 +33,7 @@ class UserIT extends BaseIT { void resetPassword() { PasswordResetRequest req = new PasswordResetRequest(); req.setEmail("admin@commafeed.com"); - - getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE); + RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/user/passwordReset").then().statusCode(200); List mails = mailbox.getMailMessagesSentTo("admin@commafeed.com"); Assertions.assertEquals(1, mails.size()); 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 9e6229c6..de6947e1 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,53 +1,42 @@ package com.commafeed.integration.servlet; -import java.net.HttpCookie; -import java.util.List; -import java.util.stream.Collectors; - -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; -import org.junit.jupiter.api.Assertions; +import org.apache.hc.core5.http.HttpStatus; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.commafeed.frontend.model.Settings; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; +import io.restassured.RestAssured; +import jakarta.ws.rs.core.MediaType; @QuarkusTest class CustomCodeIT extends BaseIT { - @Override - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base.register(HttpAuthenticationFeature.basic("admin", "admin")); + @BeforeEach + void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); } @Test void test() { // get settings - Settings settings = getClient().target(getApiBaseUrl() + "user/settings").request().get(Settings.class); + 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"); - getClient().target(getApiBaseUrl() + "user/settings").request().post(Entity.json(settings), Void.TYPE); + RestAssured.given() + .body(settings) + .contentType(MediaType.APPLICATION_JSON) + .post("rest/user/settings") + .then() + .statusCode(HttpStatus.SC_OK); // check custom code servlets - List cookies = login(); - try (Response response = getClient().target(getBaseUrl() + "custom_js.js") - .request() - .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .get()) { - Assertions.assertEquals("custom-js", response.readEntity(String.class)); - } - try (Response response = getClient().target(getBaseUrl() + "custom_css.css") - .request() - .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .get()) { - Assertions.assertEquals("custom-css", response.readEntity(String.class)); - } + 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 b12aedb1..9feb018c 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 @@ -5,15 +5,15 @@ import java.util.List; import java.util.stream.Collectors; import org.apache.hc.core5.http.HttpStatus; -import org.glassfish.jersey.client.ClientProperties; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; 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 jakarta.ws.rs.core.Response; @QuarkusTest class LogoutIT extends BaseIT { @@ -21,14 +21,17 @@ class LogoutIT extends BaseIT { @Test void test() { List cookies = login(); - try (Response response = getClient().target(getBaseUrl() + "logout") - .request() + Headers responseHeaders = RestAssured.given() .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) - .get()) { - Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); - List setCookieHeaders = response.getStringHeaders().get(HttpHeaders.SET_COOKIE); - Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0)); - } + .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)); } } 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 60331b75..661275fd 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,42 +1,34 @@ package com.commafeed.integration.servlet; -import java.net.HttpCookie; -import java.util.List; -import java.util.stream.Collectors; - import org.apache.hc.core5.http.HttpStatus; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; -import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; @QuarkusTest class NextUnreadIT extends BaseIT { - @Override - protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) { - return base.register(HttpAuthenticationFeature.basic("admin", "admin")); + @BeforeEach + void setup() { + RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin"); } @Test void test() { subscribeAndWaitForEntries(getFeedUrl()); - List cookies = login(); - Response response = getClient().target(getBaseUrl() + "next") - .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) - .request() - .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))) - .get(); - Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus()); - Assertions.assertEquals("https://hostname.local/commafeed/2", response.getHeaderString(HttpHeaders.LOCATION)); + 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 f16a5fa9..aa153217 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,19 +1,17 @@ package com.commafeed.integration.servlet; -import org.junit.jupiter.api.Assertions; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; -import jakarta.ws.rs.core.Response; +import io.restassured.RestAssured; @QuarkusTest class RobotsTxtIT extends BaseIT { @Test void test() { - try (Response response = getClient().target(getBaseUrl() + "robots.txt").request().get()) { - Assertions.assertEquals("User-agent: *\nDisallow: /", response.readEntity(String.class)); - } + RestAssured.given().get("robots.txt").then().statusCode(200).body(CoreMatchers.is("User-agent: *\nDisallow: /")); } } From e170dfe60b4674418249a59ddc54479ff0bfe9f5 Mon Sep 17 00:00:00 2001 From: Athou Date: Wed, 14 Aug 2024 20:42:28 +0200 Subject: [PATCH 22/50] prepare 5.0.0 changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ README.md | 15 ++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebdeb4bf..321e3b2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [5.0.0] + +CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in +the [announcement](https://github.com/Athou/commafeed/discussions/1517). +The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around +0.3s) and very low memory footprint (< 50M). + +- CommaFeed now has a different package for each supported database. + - If you are deploying CommaFeed with a precompiled package, please + read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package). + - If you are building CommaFeed from sources, please + read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources). + - If you are using the Docker image, please read the instructions on + the [Docker Hub page](https://hub.docker.com/r/athou/commafeed). +- Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone). + Please + read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration). + Note that some configuration elements have been removed or renamed for consistency. +- Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB. +- Use a different icon for filtering unread entries and marking an entry as read (#1506) +- Added various HTML attributes to ease custom JS/CSS customization (#1507) +- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer + needed, even for instances with a large number of feeds. +- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using + the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0. + ## [4.6.0] - switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50% diff --git a/README.md b/README.md index b17ea2c9..8111211c 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,11 @@ memory usage. When the build is complete: -- if you used the native profile, the executable is located at - `commafeed-server/target/commafeed-----runner[.exe]` -- if you did not use the native profile, a zip containing all jars required to run the application is located at +- a zip containing all jars required to run the application is located at `commafeed-server/target/commafeed--.zip`. Extract it and run the application with `java -jar quarkus-run.jar` +- if you used the native profile, the executable is located at + `commafeed-server/target/commafeed-----runner[.exe]` ### Memory management (`jvm` package only) @@ -107,7 +107,7 @@ This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/bl There are multiple ways to configure CommaFeed: -- a properties file in `config/application.properties` (keys in kebab-case) +- a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case) - Command line arguments prefixed with `-D` (keys in kebab-case) - Environment variables (keys in UPPER_CASE) - an .env file in the working directory (keys in UPPER_CASE) @@ -126,8 +126,13 @@ CommaFeed only requires 3 properties to be configured: - e.g. for MariaDB: `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` +When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, +meaning that you will have to log back in after each restart of the application. To prevent this, you can set the +`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters). + All other [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) -are optional and have sensible default values. +are optional and have sensible default values. Quarkus settings can be +found [here](https://quarkus.io/guides/all-config). When started, the server will listen on http://localhost:8082. The default user is `admin` and the default password is `admin`. From d612d83874febe6151ef3e8e2fdfc9e1f9c81d50 Mon Sep 17 00:00:00 2001 From: Athou Date: Wed, 14 Aug 2024 21:07:45 +0200 Subject: [PATCH 23/50] resolve public url dynamically, remove publicUrl config element --- .../java/com/commafeed/CommaFeedConfiguration.java | 10 ---------- .../backend/service/FeedSubscriptionService.java | 11 ----------- .../com/commafeed/frontend/resource/CategoryREST.java | 4 +++- .../com/commafeed/frontend/resource/FeedREST.java | 7 ++++--- .../com/commafeed/frontend/resource/UserREST.java | 6 ++++-- .../com/commafeed/frontend/servlet/LogoutServlet.java | 9 ++++++--- .../commafeed/frontend/servlet/NextUnreadServlet.java | 6 ++++-- 7 files changed, 21 insertions(+), 32 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 362face9..57c5ae14 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -10,7 +10,6 @@ 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.NotBlank; import jakarta.validation.constraints.Positive; /** @@ -20,15 +19,6 @@ import jakarta.validation.constraints.Positive; */ @ConfigMapping(prefix = "commafeed") public interface CommaFeedConfiguration { - - /** - * URL used to access commafeed, used for various redirects. - * - */ - @NotBlank - @WithDefault("http://localhost:8082") - String publicUrl(); - /** * Whether to expose a robots.txt file that disallows web crawlers and search engine indexers. */ 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 45d6674f..a3d69ba5 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 @@ -5,8 +5,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; - import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; @@ -56,15 +54,6 @@ public class FeedSubscriptionService { } public long subscribe(User user, String url, String title, FeedCategory category, int position) { - - final String pubUrl = config.publicUrl(); - if (StringUtils.isBlank(pubUrl)) { - throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set"); - } - if (url.startsWith(pubUrl)) { - throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance"); - } - 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)", 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 668db51c..0d83332d 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 @@ -66,6 +66,7 @@ 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -89,6 +90,7 @@ public class CategoryREST { private final FeedEntryService feedEntryService; private final FeedSubscriptionService feedSubscriptionService; private final CommaFeedConfiguration config; + private final UriInfo uri; @Path("/entries") @GET @@ -215,7 +217,7 @@ public class CategoryREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(config.publicUrl()); + feed.setLink(uri.getBaseUri().toString()); feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList()); SyndFeedOutput output = new SyndFeedOutput(); 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 33543b78..91befeb6 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,7 +1,6 @@ package com.commafeed.frontend.resource; import java.io.StringWriter; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Calendar; @@ -86,6 +85,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.UriInfo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -114,6 +114,7 @@ public class FeedREST { private final OPMLImporter opmlImporter; private final OPMLExporter opmlExporter; private final CommaFeedConfiguration config; + private final UriInfo uri; private static FeedEntry initTestEntry() { FeedEntry entry = new FeedEntry(); @@ -217,7 +218,7 @@ public class FeedREST { feed.setFeedType("rss_2.0"); feed.setTitle("CommaFeed - " + entries.getName()); feed.setDescription("CommaFeed - " + entries.getName()); - feed.setLink(config.publicUrl()); + feed.setLink(uri.getBaseUri().toString()); feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList()); SyndFeedOutput output = new SyndFeedOutput(); @@ -413,7 +414,7 @@ public class FeedREST { } catch (Exception e) { log.info("Could not subscribe to url {} : {}", url, e.getMessage()); } - return Response.temporaryRedirect(URI.create(config.publicUrl())).build(); + return Response.temporaryRedirect(uri.getBaseUri()).build(); } private String prependHttp(String url) { 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 0162c089..22744fe7 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 @@ -58,6 +58,7 @@ 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -79,6 +80,7 @@ public class UserREST { private final PasswordEncryptionService encryptionService; private final MailService mailService; private final CommaFeedConfiguration config; + private final UriInfo uri; @Path("/settings") @GET @@ -289,7 +291,7 @@ public class UserREST { } private String buildEmailContent(User user) throws Exception { - String publicUrl = FeedUtils.removeTrailingSlash(config.publicUrl()); + String publicUrl = FeedUtils.removeTrailingSlash(uri.getBaseUri().toString()); publicUrl += "/rest/user/passwordResetCallback"; 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.", @@ -337,7 +339,7 @@ public class UserREST { String message = "Your new password is: " + passwd; message += "
"; - message += String.format("Back to Homepage", config.publicUrl()); + message += String.format("Back to Homepage", uri.getBaseUri()); return Response.ok(message).build(); } 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 592074b4..7ef11c9d 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,6 +1,5 @@ package com.commafeed.frontend.servlet; -import java.net.URI; import java.time.Instant; import java.util.Date; @@ -14,6 +13,7 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; @Path("/logout") @PermitAll @@ -21,16 +21,19 @@ import jakarta.ws.rs.core.Response; public class LogoutServlet { private final CommaFeedConfiguration config; + private final UriInfo uri; private final String cookieName; - public LogoutServlet(CommaFeedConfiguration config, @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) { + public LogoutServlet(CommaFeedConfiguration config, UriInfo uri, + @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) { this.config = config; + this.uri = uri; this.cookieName = cookieName; } @GET public Response get() { NewCookie removeCookie = new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build(); - return Response.temporaryRedirect(URI.create(config.publicUrl())).cookie(removeCookie).build(); + 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 80ee3974..5e831744 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 @@ -26,6 +26,7 @@ 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 lombok.RequiredArgsConstructor; @Path("/next") @@ -40,12 +41,13 @@ public class NextUnreadServlet { private final FeedEntryService feedEntryService; private final CommaFeedConfiguration config; private final AuthenticationContext authenticationContext; + private final UriInfo uri; @GET public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) { User user = authenticationContext.getCurrentUser(); if (user == null) { - return Response.temporaryRedirect(URI.create(config.publicUrl())).build(); + return Response.temporaryRedirect(uri.getBaseUri()).build(); } FeedEntryStatus status = unitOfWork.call(() -> { @@ -71,7 +73,7 @@ public class NextUnreadServlet { return s; }); - String url = status == null ? config.publicUrl() : status.getEntry().getUrl(); + String url = status == null ? uri.getBaseUri().toString() : status.getEntry().getUrl(); return Response.temporaryRedirect(URI.create(url)).build(); } } From e757e61b79a6dc2b6056c29364d4feb6caf19a54 Mon Sep 17 00:00:00 2001 From: Athou Date: Wed, 14 Aug 2024 21:34:05 +0200 Subject: [PATCH 24/50] config comment tweaks --- .../main/java/com/commafeed/CommaFeedConfiguration.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 57c5ae14..de0224a0 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -15,7 +15,7 @@ import jakarta.validation.constraints.Positive; /** * CommaFeed configuration * - * Default values are for production, they can be overridden in application.properties + * Default values are for production, they can be overridden in application.properties for other profiles */ @ConfigMapping(prefix = "commafeed") public interface CommaFeedConfiguration { @@ -52,7 +52,7 @@ public interface CommaFeedConfiguration { Optional googleAnalyticsTrackingCode(); /** - * Google Auth key for fetching Youtube favicons. + * Google Auth key for fetching Youtube channel favicons. */ Optional googleAuthKey(); @@ -127,6 +127,8 @@ public interface CommaFeedConfiguration { interface Database { /** * Database query timeout. + * + * 0 to disable. */ @WithDefault("0") int queryTimeout(); @@ -201,7 +203,7 @@ public interface CommaFeedConfiguration { interface Websocket { /** - * Enable websocket connection so the server can notify the web client that there are new entries for your feeds. + * Enable websocket connection so the server can notify web clients that there are new entries for feeds. */ @WithDefault("true") boolean enabled(); From c18ed829aa6413c1e5cd87ad270ae8013ae335cb Mon Sep 17 00:00:00 2001 From: Athou Date: Wed, 14 Aug 2024 23:08:24 +0200 Subject: [PATCH 25/50] generate jvm package with a -jvm suffix --- README.md | 8 ++++---- commafeed-server/pom.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8111211c..16c22925 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ system and database of choice. There are two types of packages: -- The `linux` and `windows` packages are compiled natively and contain an executable that can be run directly. +- The `linux-x86_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run + directly. - The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all platforms and is started with `java -jar quarkus-run.jar`. @@ -60,13 +61,12 @@ memory usage. - `` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. - `-DskipTests` is optional but recommended because tests require a Docker environment to run against a real database. - `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment - variable - pointing to a GraalVM installation). + variable pointing to a GraalVM installation). When the build is complete: - a zip containing all jars required to run the application is located at - `commafeed-server/target/commafeed--.zip`. Extract it and run the application with + `commafeed-server/target/commafeed---jvm.zip`. Extract it and run the application with `java -jar quarkus-run.jar` - if you used the native profile, the executable is located at `commafeed-server/target/commafeed-----runner[.exe]` diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index f8747493..6c1ba4de 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -89,7 +89,7 @@ single - commafeed-${project.version}-${quarkus.datasource.db-kind} + commafeed-${project.version}-${quarkus.datasource.db-kind}-jvm false src/main/assembly/zip-quarkus-app.xml From 47d39831d3c73c30a692d0ce3f8291f86521c8c6 Mon Sep 17 00:00:00 2001 From: Athou Date: Thu, 15 Aug 2024 09:05:56 +0200 Subject: [PATCH 26/50] use Duration for query timeout --- .../main/java/com/commafeed/CommaFeedConfiguration.java | 2 +- .../main/java/com/commafeed/backend/dao/GenericDAO.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index de0224a0..1188d517 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -131,7 +131,7 @@ public interface CommaFeedConfiguration { * 0 to disable. */ @WithDefault("0") - int queryTimeout(); + Duration queryTimeout(); Cleanup cleanup(); 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 8041295b..850144c8 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,5 +1,6 @@ package com.commafeed.backend.dao; +import java.time.Duration; import java.util.Collection; import org.hibernate.Session; @@ -57,9 +58,9 @@ public abstract class GenericDAO { return objects.size(); } - protected void setTimeout(JPAQuery query, int timeoutMs) { - if (timeoutMs > 0) { - query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, timeoutMs); + protected void setTimeout(JPAQuery query, Duration timeout) { + if (!timeout.isZero()) { + query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, timeout.toMillis()); } } From 815093f1c6fa0a89fd1765f0ba6bf08ddc00e93a Mon Sep 17 00:00:00 2001 From: Athou Date: Thu, 15 Aug 2024 12:17:28 +0200 Subject: [PATCH 27/50] remove STARTUP_TIME because static fields are initialized at compile time in native mode --- .../com/commafeed/CommaFeedApplication.java | 4 ---- .../commafeed/frontend/resource/FeedREST.java | 20 +++---------------- .../commafeed/integration/rest/FeedIT.java | 2 ++ 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java index 222b6548..e404fb90 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java @@ -1,7 +1,5 @@ package com.commafeed; -import java.time.Instant; - import com.commafeed.backend.feed.FeedRefreshEngine; import com.commafeed.backend.service.db.DatabaseStartupService; import com.commafeed.backend.task.TaskScheduler; @@ -20,8 +18,6 @@ public class CommaFeedApplication { public static final String USERNAME_ADMIN = "admin"; public static final String USERNAME_DEMO = "demo"; - public static final Instant STARTUP_TIME = Instant.now(); - private final DatabaseStartupService databaseStartupService; private final FeedRefreshEngine feedRefreshEngine; private final TaskScheduler taskScheduler; 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 91befeb6..9309594f 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 @@ -3,15 +3,14 @@ package com.commafeed.frontend.resource; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; import java.util.List; import java.util.Objects; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; +import org.jboss.resteasy.reactive.Cache; import org.jboss.resteasy.reactive.RestForm; import com.commafeed.CommaFeedApplication; @@ -80,10 +79,8 @@ 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.CacheControl; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.UriInfo; import lombok.RequiredArgsConstructor; @@ -343,6 +340,7 @@ public class FeedREST { @GET @Path("/favicon/{id}") + @Cache(maxAge = 2592000) @Transactional @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) { @@ -356,19 +354,7 @@ public class FeedREST { Feed feed = subscription.getFeed(); Favicon icon = feedService.fetchFavicon(feed); - ResponseBuilder builder = Response.ok(icon.getIcon(), icon.getMediaType()); - - CacheControl cacheControl = new CacheControl(); - cacheControl.setMaxAge(2592000); - cacheControl.setPrivate(false); - builder.cacheControl(cacheControl); - - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.MONTH, 1); - builder.expires(calendar.getTime()); - builder.lastModified(Date.from(CommaFeedApplication.STARTUP_TIME)); - - return builder.build(); + return Response.ok(icon.getIcon(), icon.getMediaType()).build(); } @POST 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 1e91f079..271b3556 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 @@ -27,6 +27,7 @@ import com.commafeed.integration.BaseIT; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; @QuarkusTest @@ -224,6 +225,7 @@ class FeedIT extends BaseIT { .get("rest/feed/favicon/{id}", subscriptionId) .then() .statusCode(HttpStatus.SC_OK) + .header(HttpHeaders.CACHE_CONTROL, "max-age=2592000") .extract() .response() .asByteArray(); From 643954f7c94e94def6f9d07482204903276ba0c0 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 05:48:01 +0200 Subject: [PATCH 28/50] timeout should be an Integer --- .../src/main/java/com/commafeed/backend/dao/GenericDAO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 850144c8..dff4307b 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 @@ -60,7 +60,7 @@ public abstract class GenericDAO { protected void setTimeout(JPAQuery query, Duration timeout) { if (!timeout.isZero()) { - query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, timeout.toMillis()); + query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis())); } } From da910ac336a3f4705237c99fa81527b415e3b581 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 05:53:23 +0200 Subject: [PATCH 29/50] mute MediaModuleParser warnings --- commafeed-server/src/main/resources/application.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties index c8301671..d75d3453 100644 --- a/commafeed-server/src/main/resources/application.properties +++ b/commafeed-server/src/main/resources/application.properties @@ -19,6 +19,10 @@ quarkus.datasource.db-kind=h2 quarkus.liquibase.change-log=migrations.xml quarkus.liquibase.migrate-at-start=true +# logging +# MediaModuleParser is very verbose +quarkus.log.category."com.rometools.modules.mediarss.io.MediaModuleParser".level=ERROR + # shutdown quarkus.shutdown.timeout=5s From 2f51547f0d73de553e6cdfbfa21b5ecbbc91a097 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 06:15:52 +0200 Subject: [PATCH 30/50] add missing rome classes --- commafeed-server/pom.xml | 7 ++++ .../com/commafeed/NativeImageClasses.java | 21 ++++++++-- .../com/commafeed/NativeImageClassesTest.java | 38 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 6c1ba4de..270bdd1c 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -456,6 +456,13 @@ 1.46.0 test + + org.reflections + reflections + 0.10.2 + test + + diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java index c1715beb..9f5211d9 100644 --- a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java +++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java @@ -14,10 +14,23 @@ import io.quarkus.runtime.annotations.RegisterForReflection; // metrics MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class, - // rome - java.util.Date.class, com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class, - com.rometools.modules.content.ContentModuleImpl.class, com.rometools.modules.mediarss.MediaModuleImpl.class, - com.rometools.modules.mediarss.MediaEntryModuleImpl.class, + // rome modules + java.util.Date.class, 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, diff --git a/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java b/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java new file mode 100644 index 00000000..8b00dd90 --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java @@ -0,0 +1,38 @@ +package com.commafeed; + +import java.lang.reflect.Modifier; +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.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())); + + for (Class clazz : Set.of(Module.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())); + moduleClasses.removeAll(classesInAnnotation); + + moduleClasses.forEach(c -> System.out.println(c.getName() + ".class,")); + Assertions.assertEquals(Set.of(), moduleClasses); + } + + } + +} \ No newline at end of file From 4ef53eab3af687ec80f953704778a4af0a1db877 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 06:18:27 +0200 Subject: [PATCH 31/50] mute rome modules warnings in production --- commafeed-server/src/main/resources/application.properties | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties index d75d3453..84819871 100644 --- a/commafeed-server/src/main/resources/application.properties +++ b/commafeed-server/src/main/resources/application.properties @@ -19,10 +19,6 @@ quarkus.datasource.db-kind=h2 quarkus.liquibase.change-log=migrations.xml quarkus.liquibase.migrate-at-start=true -# logging -# MediaModuleParser is very verbose -quarkus.log.category."com.rometools.modules.mediarss.io.MediaModuleParser".level=ERROR - # shutdown quarkus.shutdown.timeout=5s @@ -45,4 +41,5 @@ quarkus.shutdown.timeout=5s # prod profile overrides %prod.quarkus.datasource.jdbc.url=jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE %prod.quarkus.datasource.username=sa -%prod.quarkus.datasource.password=sa \ No newline at end of file +%prod.quarkus.datasource.password=sa +%prod.quarkus.log.category."com.rometools.modules".level=ERROR From 214dfe580a4536fb7fa89a8aa5b24b9fd935c0a0 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 06:40:41 +0200 Subject: [PATCH 32/50] more rome classes --- .../com/commafeed/NativeImageClasses.java | 50 ++++++++++++++++++- .../com/commafeed/NativeImageClassesTest.java | 15 ++++-- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java index 9f5211d9..278f83e3 100644 --- a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java +++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java @@ -14,8 +14,56 @@ import io.quarkus.runtime.annotations.RegisterForReflection; // 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 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 - java.util.Date.class, com.rometools.modules.sse.modules.Conflict.class, com.rometools.modules.sse.modules.Conflicts.class, + 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, diff --git a/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java b/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java index 8b00dd90..4c961d02 100644 --- a/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java +++ b/commafeed-server/src/test/java/com/commafeed/NativeImageClassesTest.java @@ -1,6 +1,8 @@ package com.commafeed; 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; @@ -10,6 +12,7 @@ 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; @@ -24,15 +27,17 @@ class NativeImageClassesTest { Set> classesInAnnotation = Set .copyOf(List.of(NativeImageClasses.class.getAnnotation(RegisterForReflection.class).targets())); - for (Class clazz : Set.of(Module.class, WireFeedParser.class, WireFeedGenerator.class)) { + 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())); + moduleClasses.removeIf(c -> c.isInterface() || Modifier.isAbstract(c.getModifiers()) || !Modifier.isPublic(c.getModifiers())); moduleClasses.removeAll(classesInAnnotation); - - moduleClasses.forEach(c -> System.out.println(c.getName() + ".class,")); - Assertions.assertEquals(Set.of(), moduleClasses); + 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 From ab334a7bc6820ca1b54861395557d9856168ad64 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 07:31:22 +0200 Subject: [PATCH 33/50] feed needs to be known to be deleted --- .../src/main/java/com/commafeed/backend/dao/FeedDAO.java | 4 ++++ .../commafeed/backend/service/db/DatabaseCleaningService.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 cdaa690e..ac1349b4 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 @@ -24,6 +24,10 @@ public class FeedDAO extends GenericDAO { super(entityManager, Feed.class); } + 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).where(FEED.disabledUntil.isNull().or(FEED.disabledUntil.lt(Instant.now()))); if (lastLoginThreshold != null) { 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 d64bb25b..310f6926 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 @@ -12,6 +12,7 @@ import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity; 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; @@ -61,7 +62,7 @@ public class DatabaseCleaningService { log.info("removed {} entries for feeds without subscriptions", entriesTotal); } while (entriesDeleted > 0); } - deleted = unitOfWork.call(() -> feedDAO.delete(feeds)); + deleted = unitOfWork.call(() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList()))); total += deleted; log.info("removed {} feeds without subscriptions", total); } while (deleted != 0); From 720eddeb6651b524ea1d683b778ab1790d7ccf4c Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 08:31:23 +0200 Subject: [PATCH 34/50] CHANGELOG tweaks --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 321e3b2e..0eb1c3c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The gist of it is that CommaFeed can now be compiled to a native binary, resulti - Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone). Please read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration). - Note that some configuration elements have been removed or renamed for consistency. + Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature. - Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB. - Use a different icon for filtering unread entries and marking an entry as read (#1506) - Added various HTML attributes to ease custom JS/CSS customization (#1507) From 2694fea2119797ce3fc72262596397d4a0af2882 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 08:45:30 +0200 Subject: [PATCH 35/50] cleanup --- .../java/com/commafeed/frontend/servlet/LogoutServlet.java | 7 +------ .../com/commafeed/frontend/servlet/NextUnreadServlet.java | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) 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 7ef11c9d..7114de35 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 @@ -5,8 +5,6 @@ import java.util.Date; import org.eclipse.microprofile.config.inject.ConfigProperty; -import com.commafeed.CommaFeedConfiguration; - import jakarta.annotation.security.PermitAll; import jakarta.inject.Singleton; import jakarta.ws.rs.GET; @@ -20,13 +18,10 @@ import jakarta.ws.rs.core.UriInfo; @Singleton public class LogoutServlet { - private final CommaFeedConfiguration config; private final UriInfo uri; private final String cookieName; - public LogoutServlet(CommaFeedConfiguration config, UriInfo uri, - @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) { - this.config = config; + public LogoutServlet(UriInfo uri, @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) { this.uri = uri; this.cookieName = cookieName; } 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 5e831744..ae4d3ca9 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 @@ -5,7 +5,6 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; -import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO; @@ -39,7 +38,6 @@ public class NextUnreadServlet { private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedCategoryDAO feedCategoryDAO; private final FeedEntryService feedEntryService; - private final CommaFeedConfiguration config; private final AuthenticationContext authenticationContext; private final UriInfo uri; From 1bfa3ebb8ea5fe35545f00567315c44e0aac0493 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 13:47:15 +0200 Subject: [PATCH 36/50] set the default h2 path to a relative one next to the executable --- commafeed-server/src/main/docker/Dockerfile.jvm | 4 ++-- commafeed-server/src/main/docker/Dockerfile.native | 4 ++-- commafeed-server/src/main/resources/application.properties | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/commafeed-server/src/main/docker/Dockerfile.jvm b/commafeed-server/src/main/docker/Dockerfile.jvm index 38f94ea1..043eb572 100644 --- a/commafeed-server/src/main/docker/Dockerfile.jvm +++ b/commafeed-server/src/main/docker/Dockerfile.jvm @@ -5,8 +5,8 @@ EXPOSE 8082 RUN mkdir -p /commafeed/data VOLUME /commafeed/data -COPY commafeed-server/target/quarkus-app/ /commafeed/app -WORKDIR /commafeed/app +COPY commafeed-server/target/quarkus-app/ /commafeed +WORKDIR /commafeed CMD ["java", \ "-Xtune:virtualized", \ diff --git a/commafeed-server/src/main/docker/Dockerfile.native b/commafeed-server/src/main/docker/Dockerfile.native index 9393734f..f66f706f 100644 --- a/commafeed-server/src/main/docker/Dockerfile.native +++ b/commafeed-server/src/main/docker/Dockerfile.native @@ -4,6 +4,6 @@ EXPOSE 8082 RUN mkdir -p /commafeed/data VOLUME /commafeed/data -COPY commafeed-server/target/commafeed-*-runner /commafeed/app/application -WORKDIR /commafeed/app +COPY commafeed-server/target/commafeed-*-runner /commafeed/application +WORKDIR /commafeed CMD ["./application"] diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties index 84819871..9da0b50f 100644 --- a/commafeed-server/src/main/resources/application.properties +++ b/commafeed-server/src/main/resources/application.properties @@ -39,7 +39,7 @@ quarkus.shutdown.timeout=5s # prod profile overrides -%prod.quarkus.datasource.jdbc.url=jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE +%prod.quarkus.datasource.jdbc.url=jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE %prod.quarkus.datasource.username=sa %prod.quarkus.datasource.password=sa %prod.quarkus.log.category."com.rometools.modules".level=ERROR From 5c69daec08b992527b1f070e32a3278c3c8a616d Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 14:02:49 +0200 Subject: [PATCH 37/50] restore welcome page on 401 --- .../java/com/commafeed/ExceptionMappers.java | 32 ++++++++++++++----- .../com/commafeed/integration/SecurityIT.java | 9 +++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java index 68618094..4519743c 100644 --- a/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java +++ b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java @@ -4,30 +4,46 @@ import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.RestResponse.Status; import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +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.ext.Provider; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor @Provider @Priority(1) public class ExceptionMappers { - // display a message when the user fails to authenticate + private final CommaFeedConfiguration config; + + @ServerExceptionMapper(UnauthorizedException.class) + public RestResponse unauthorized(UnauthorizedException e) { + return RestResponse.status(RestResponse.Status.UNAUTHORIZED, + new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations())); + } + @ServerExceptionMapper(AuthenticationFailedException.class) - public RestResponse authenticationFailed(AuthenticationFailedException e) { - return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationExceptionInfo(e.getMessage())); + public RestResponse authenticationFailed(AuthenticationFailedException e) { + return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage())); } - // display a message for validation errors @ServerExceptionMapper(ValidationException.class) - public RestResponse validationException(ValidationException e) { - return RestResponse.status(Status.BAD_REQUEST, new ValidationExceptionInfo(e.getMessage())); + public RestResponse validationFailed(ValidationException e) { + return RestResponse.status(Status.BAD_REQUEST, new ValidationFailed(e.getMessage())); } - public record AuthenticationExceptionInfo(String message) { + @RegisterForReflection + public record UnauthorizedResponse(String message, boolean allowRegistrations) { } - public record ValidationExceptionInfo(String message) { + @RegisterForReflection + public record AuthenticationFailed(String message) { + } + + @RegisterForReflection + public record ValidationFailed(String message) { } } 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 f2b1e124..171672e5 100644 --- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java +++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java @@ -8,6 +8,7 @@ import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import com.commafeed.ExceptionMappers.UnauthorizedResponse; import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.request.MarkRequest; @@ -24,7 +25,13 @@ class SecurityIT extends BaseIT { @Test void notLoggedIn() { - RestAssured.given().get("rest/user/profile").then().statusCode(HttpStatus.SC_UNAUTHORIZED); + UnauthorizedResponse info = RestAssured.given() + .get("rest/user/profile") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED) + .extract() + .as(UnauthorizedResponse.class); + Assertions.assertTrue(info.allowRegistrations()); } @Test From ab6457ef3f1374c107b349e3f381c9fa5ea1048b Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 14:23:18 +0200 Subject: [PATCH 38/50] README tweaks --- README.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 16c22925..8fb216bb 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,21 @@ This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/bl ## Configuration +CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in +the `data` directory in the working directory. + +To use a different database, you will need to configure the following properties: + +- `quarkus.datasource.jdbc-url` + - e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE` + - e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed` + - e.g. for MySQL: + `jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` + - e.g. for MariaDB: + `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` +- `quarkus.datasource.username` +- `quarkus.datasource.password` + There are multiple ways to configure CommaFeed: - a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case) @@ -114,23 +129,11 @@ There are multiple ways to configure CommaFeed: The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos. -CommaFeed only requires 3 properties to be configured: - -- `quarkus.datasource.username` -- `quarkus.datasource.password` -- `quarkus.datasource.jdbc-url` - - e.g. for H2: `jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE` - - e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed` - - e.g. for MySQL: - `jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` - - e.g. for MariaDB: - `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` - When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, meaning that you will have to log back in after each restart of the application. To prevent this, you can set the `quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters). -All other [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) +All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) are optional and have sensible default values. Quarkus settings can be found [here](https://quarkus.io/guides/all-config). From b333e8d90a2f4a7510dfdc2777cc7bfce93b5fa0 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 14:40:16 +0200 Subject: [PATCH 39/50] set a timeout on ssl handshakes --- .../src/main/java/com/commafeed/backend/HttpGetter.java | 2 ++ 1 file changed, 2 insertions(+) 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 2d5a59f0..e7ed5b5d 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; @@ -167,6 +168,7 @@ public class HttpGetter { .setSSLSocketFactory(Apache5SslUtils.toSocketFactory(sslFactory)) .setDefaultConnectionConfig( ConnectionConfig.custom().setConnectTimeout(Timeout.ofSeconds(5)).setTimeToLive(TimeValue.ofSeconds(30)).build()) + .setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.ofSeconds(5)).build()) .setMaxConnPerRoute(poolSize) .setMaxConnTotal(poolSize) .build(); From 5b77860189023a0e2e826f1571dd2e00ec7e79be Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 15:45:17 +0200 Subject: [PATCH 40/50] use join to speed up cleanup --- .../commafeed/backend/dao/FeedEntryContentDAO.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 0b742e64..7e4f180e 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 @@ -5,8 +5,6 @@ import java.util.List; import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntryContent; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.JPQLSubQuery; import jakarta.inject.Singleton; import jakarta.persistence.EntityManager; @@ -26,9 +24,13 @@ public class FeedEntryContentDAO extends GenericDAO { } public long deleteWithoutEntries(int max) { - JPQLSubQuery subQuery = JPAExpressions.selectOne().from(ENTRY).where(ENTRY.content.id.eq(CONTENT.id)); - List ids = query().select(CONTENT.id).from(CONTENT).where(subQuery.notExists()).limit(max).fetch(); - + 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(); } } From 1b1a3f49c10e779d527314c8db494a9564510e85 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 19:01:44 +0200 Subject: [PATCH 41/50] keep using the same css parser as before --- .../backend/service/FeedEntryContentCleaningService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 da266b0a..a776c452 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 @@ -20,7 +20,7 @@ import org.w3c.css.sac.InputSource; import org.w3c.dom.css.CSSStyleDeclaration; import com.steadystate.css.parser.CSSOMParser; -import com.steadystate.css.parser.SACParserCSS3; +import com.steadystate.css.parser.SACParserCSS21; import jakarta.inject.Singleton; import lombok.RequiredArgsConstructor; @@ -150,7 +150,7 @@ public class FeedEntryContentCleaningService { } private CSSOMParser buildCssParser() { - CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); + CSSOMParser parser = new CSSOMParser(new SACParserCSS21()); parser.setErrorHandler(new ErrorHandler() { @Override From 012ce71134893e3a4539cc3cfd2702f9d2ceb767 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 21:12:54 +0200 Subject: [PATCH 42/50] configurable http client timeouts --- .../com/commafeed/CommaFeedConfiguration.java | 65 +++++++++++++++---- .../com/commafeed/backend/HttpGetter.java | 49 +++++++------- .../favicon/AbstractFaviconFetcher.java | 2 - .../favicon/DefaultFaviconFetcher.java | 6 +- .../favicon/FacebookFaviconFetcher.java | 2 +- .../favicon/YoutubeFaviconFetcher.java | 2 +- .../commafeed/backend/feed/FeedFetcher.java | 11 ++-- .../frontend/resource/ServerREST.java | 2 +- .../com/commafeed/backend/HttpGetterTest.java | 48 +++++++------- .../backend/feed/FeedFetcherTest.java | 4 +- 10 files changed, 118 insertions(+), 73 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 1188d517..ef6abff5 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -56,6 +56,11 @@ public interface CommaFeedConfiguration { */ Optional googleAuthKey(); + /** + * HTTP client configuration + */ + HttpClient httpClient(); + /** * Feed refresh engine settings. */ @@ -76,6 +81,55 @@ public interface CommaFeedConfiguration { */ Websocket websocket(); + 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 SSL handshake to complete. + */ + @WithDefault("5s") + Duration sslHandshakeTimeout(); + + /** + * 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 live for a connection in the pool. + */ + @WithDefault("30s") + Duration connectionTimeToLive(); + + /** + * 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(); + } + interface FeedRefresh { /** * Amount of time CommaFeed will wait before refreshing the same feed. @@ -104,12 +158,6 @@ public interface CommaFeedConfiguration { @WithDefault("1") int databaseThreads(); - /** - * If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed. - */ - @WithDefault("5M") - MemorySize maxResponseSize(); - /** * Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. * @@ -117,11 +165,6 @@ public interface CommaFeedConfiguration { */ @WithDefault("0") Duration userInactivityPeriod(); - - /** - * User-Agent string that will be used by the http client, leave empty for the default one. - */ - Optional userAgent(); } interface Database { 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 e7ed5b5d..61ac226e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java @@ -3,10 +3,10 @@ package com.commafeed.backend; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.config.ConnectionConfig; @@ -36,7 +36,6 @@ import com.google.common.collect.Iterables; import com.google.common.io.ByteStreams; import com.google.common.net.HttpHeaders; -import io.quarkus.runtime.configuration.MemorySize; import jakarta.inject.Singleton; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -51,16 +50,18 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils; @Slf4j public class HttpGetter { + private final CommaFeedConfiguration config; private final CloseableHttpClient client; - private final MemorySize maxResponseSize; public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) { - PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.feedRefresh().httpThreads()); - String userAgent = config.feedRefresh() + this.config = config; + + PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config); + String userAgent = config.httpClient() .userAgent() .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion())); - this.client = newClient(connectionManager, userAgent); - this.maxResponseSize = config.feedRefresh().maxResponseSize(); + + this.client = newClient(connectionManager, userAgent, config.httpClient().idleConnectionsEvictionInterval()); metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax()); metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"), @@ -69,8 +70,8 @@ public class HttpGetter { metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "pending"), () -> connectionManager.getTotalStats().getPending()); } - public HttpResult getBinary(String url, int timeout) throws IOException, NotModifiedException { - return getBinary(url, null, null, timeout); + public HttpResult getBinary(String url) throws IOException, NotModifiedException { + return getBinary(url, null, null); } /** @@ -83,10 +84,9 @@ public class HttpGetter { * @throws NotModifiedException * if the url hasn't changed since we asked for it last time */ - public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException { + public HttpResult getBinary(String url, String lastModified, String eTag) throws IOException, NotModifiedException { log.debug("fetching {}", url); - long start = System.currentTimeMillis(); ClassicHttpRequest request = ClassicRequestBuilder.get(url).build(); if (lastModified != null) { request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified); @@ -96,10 +96,10 @@ public class HttpGetter { } HttpClientContext context = HttpClientContext.create(); - context.setRequestConfig(RequestConfig.custom().setResponseTimeout(timeout, TimeUnit.MILLISECONDS).build()); - + context.setRequestConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build()); HttpResponse response = client.execute(request, context, resp -> { - byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.asLongValue()); + 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) @@ -137,8 +137,7 @@ public class HttpGetter { throw new NotModifiedException("eTagHeader is the same"); } - long duration = System.currentTimeMillis() - start; - return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader, duration, + return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader, response.getUrlAfterRedirect()); } @@ -161,21 +160,26 @@ public class HttpGetter { } } - private static PoolingHttpClientConnectionManager newConnectionManager(int poolSize) { + private static PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config) { SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build(); + int poolSize = config.feedRefresh().httpThreads(); return PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(Apache5SslUtils.toSocketFactory(sslFactory)) - .setDefaultConnectionConfig( - ConnectionConfig.custom().setConnectTimeout(Timeout.ofSeconds(5)).setTimeToLive(TimeValue.ofSeconds(30)).build()) - .setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.ofSeconds(5)).build()) + .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) .build(); } - private static CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent) { + private static CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent, + Duration idleConnectionsEvictionInterval) { List
headers = new ArrayList<>(); headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en")); headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache")); @@ -189,7 +193,7 @@ public class HttpGetter { .setDefaultHeaders(headers) .setConnectionManager(connectionManager) .evictExpiredConnections() - .evictIdleConnections(TimeValue.ofMinutes(1)) + .evictIdleConnections(TimeValue.of(idleConnectionsEvictionInterval)) .build(); } @@ -249,7 +253,6 @@ public class HttpGetter { private final String contentType; private final String lastModifiedSince; private final String eTag; - private final long duration; private final String urlAfterRedirect; } diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java index 06e8054b..c33fc5f0 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/AbstractFaviconFetcher.java @@ -12,8 +12,6 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class AbstractFaviconFetcher { - protected static final int TIMEOUT = 4000; - private static final List ICON_MIMETYPE_BLACKLIST = Arrays.asList("application/xml", "text/html"); private static final long MIN_ICON_LENGTH = 100; private static final long MAX_ICON_LENGTH = 100000; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java index 32a0ee11..1dc94a1c 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java @@ -69,7 +69,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { try { url = FeedUtils.removeTrailingSlash(url) + "/favicon.ico"; log.debug("getting root icon at {}", url); - HttpResult result = getter.getBinary(url, TIMEOUT); + HttpResult result = getter.getBinary(url); bytes = result.getContent(); contentType = result.getContentType(); } catch (Exception e) { @@ -87,7 +87,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { Document doc; try { - HttpResult result = getter.getBinary(url, TIMEOUT); + HttpResult result = getter.getBinary(url); doc = Jsoup.parse(new String(result.getContent()), url); } catch (Exception e) { log.debug("Failed to retrieve page to find icon"); @@ -113,7 +113,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher { byte[] bytes; String contentType; try { - HttpResult result = getter.getBinary(href, TIMEOUT); + HttpResult result = getter.getBinary(href); bytes = result.getContent(); contentType = result.getContentType(); } catch (Exception e) { 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 fb0f0fb9..fad9d1ef 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 @@ -43,7 +43,7 @@ public class FacebookFaviconFetcher extends AbstractFaviconFetcher { try { log.debug("Getting Facebook user's icon, {}", url); - HttpResult iconResult = getter.getBinary(iconUrl, TIMEOUT); + HttpResult iconResult = getter.getBinary(iconUrl); bytes = iconResult.getContent(); contentType = iconResult.getContentType(); } catch (Exception e) { 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 ea219e3e..02613278 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 @@ -80,7 +80,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { Thumbnail thumbnail = channel.getSnippet().getThumbnails().getDefault(); log.debug("fetching favicon"); - HttpResult iconResult = getter.getBinary(thumbnail.getUrl(), TIMEOUT); + HttpResult iconResult = getter.getBinary(thumbnail.getUrl()); bytes = iconResult.getContent(); contentType = iconResult.getContentType(); } catch (Exception e) { 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 03be80a9..8accfdf5 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 @@ -40,9 +40,7 @@ public class FeedFetcher { Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException { log.debug("Fetching feed {}", feedUrl); - int timeout = 20000; - - HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout); + HttpResult result = getter.getBinary(feedUrl, lastModified, eTag); byte[] content = result.getContent(); FeedParserResult parserResult; @@ -54,7 +52,7 @@ public class FeedFetcher { if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) { feedUrl = extractedUrl; - result = getter.getBinary(extractedUrl, lastModified, eTag, timeout); + result = getter.getBinary(extractedUrl, lastModified, eTag); content = result.getContent(); parserResult = parser.parse(result.getUrlAfterRedirect(), content); } else { @@ -87,8 +85,7 @@ public class FeedFetcher { etagHeaderValueChanged ? result.getETag() : null); } - return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash, - result.getDuration()); + return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash); } private static String extractFeedUrl(List urlProviders, String url, String urlContent) { @@ -103,7 +100,7 @@ public class FeedFetcher { } public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader, - String contentHash, long fetchDuration) { + String contentHash) { } } 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 4a6d64d7..272b79f1 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 @@ -77,7 +77,7 @@ public class ServerREST { url = FeedUtils.imageProxyDecoder(url); try { - HttpResult result = httpGetter.getBinary(url, 20000); + HttpResult result = httpGetter.getBinary(url); return Response.ok(result.getContent()).build(); } catch (Exception e) { return Response.status(Status.SERVICE_UNAVAILABLE).entity(e.getMessage()).build(); 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 a10cb2a7..ff2d0409 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -3,6 +3,7 @@ package com.commafeed.backend; import java.io.IOException; import java.math.BigInteger; import java.net.SocketTimeoutException; +import java.time.Duration; import java.util.Arrays; import java.util.Objects; import java.util.Optional; @@ -39,11 +40,11 @@ import io.quarkus.runtime.configuration.MemorySize; @ExtendWith(MockServerExtension.class) class HttpGetterTest { - private static final int TIMEOUT = 10000; - private MockServerClient mockServerClient; private String feedUrl; private byte[] feedContent; + private CommaFeedConfiguration config; + private HttpGetter getter; @BeforeEach @@ -53,10 +54,15 @@ class HttpGetterTest { this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/"; this.feedContent = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/feed/rss.xml"))); - CommaFeedConfiguration config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); - Mockito.when(config.feedRefresh().userAgent()).thenReturn(Optional.of("http-getter-test")); + 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.ofMillis(300)); + Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(5)); + Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofMillis(300)); + Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(300)); + Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(config.httpClient().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3); - Mockito.when(config.feedRefresh().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class)); } @@ -67,7 +73,7 @@ class HttpGetterTest { void errorCodes(int code) { this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withStatusCode(code)); - HttpResponseException e = Assertions.assertThrows(HttpResponseException.class, () -> getter.getBinary(this.feedUrl, TIMEOUT)); + HttpResponseException e = Assertions.assertThrows(HttpResponseException.class, () -> getter.getBinary(this.feedUrl)); Assertions.assertEquals(code, e.getCode()); } @@ -80,12 +86,11 @@ class HttpGetterTest { .withHeader(HttpHeaders.LAST_MODIFIED, "123456") .withHeader(HttpHeaders.ETAG, "78910")); - HttpResult result = getter.getBinary(this.feedUrl, TIMEOUT); + HttpResult result = getter.getBinary(this.feedUrl); Assertions.assertArrayEquals(feedContent, result.getContent()); Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.getContentType()); Assertions.assertEquals("123456", result.getLastModifiedSince()); Assertions.assertEquals("78910", result.getETag()); - Assertions.assertTrue(result.getDuration() >= 0); Assertions.assertEquals(this.feedUrl, result.getUrlAfterRedirect()); } @@ -110,24 +115,23 @@ class HttpGetterTest { this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/redirected-2")) .respond(HttpResponse.response().withBody(feedContent).withContentType(MediaType.APPLICATION_ATOM_XML)); - HttpResult result = getter.getBinary(this.feedUrl, TIMEOUT); + HttpResult result = getter.getBinary(this.feedUrl); Assertions.assertEquals("http://localhost:" + this.mockServerClient.getPort() + "/redirected-2", result.getUrlAfterRedirect()); } @Test void dataTimeout() { - int smallTimeout = 500; this.mockServerClient.when(HttpRequest.request().withMethod("GET")) - .respond(HttpResponse.response().withDelay(Delay.milliseconds(smallTimeout * 2))); + .respond(HttpResponse.response().withDelay(Delay.milliseconds(1000))); - Assertions.assertThrows(SocketTimeoutException.class, () -> getter.getBinary(this.feedUrl, smallTimeout)); + Assertions.assertThrows(SocketTimeoutException.class, () -> getter.getBinary(this.feedUrl)); } @Test void connectTimeout() { // try to connect to a non-routable address // https://stackoverflow.com/a/904609 - Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 500)); + Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1")); } @Test @@ -135,7 +139,7 @@ class HttpGetterTest { this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.USER_AGENT, "http-getter-test")) .respond(HttpResponse.response().withBody("ok")); - HttpResult result = getter.getBinary(this.feedUrl, TIMEOUT); + HttpResult result = getter.getBinary(this.feedUrl); Assertions.assertEquals("ok", new String(result.getContent())); } @@ -144,7 +148,7 @@ class HttpGetterTest { 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.getBinary(this.feedUrl, "123456", null, TIMEOUT)); + Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, "123456", null)); } @Test @@ -152,7 +156,7 @@ class HttpGetterTest { 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.getBinary(this.feedUrl, null, "78910", TIMEOUT)); + Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, null, "78910")); } @Test @@ -169,8 +173,8 @@ class HttpGetterTest { return HttpResponse.response().withBody("ok").withHeader(HttpHeaders.SET_COOKIE, "foo=bar"); }); - Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl, TIMEOUT)); - Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl, TIMEOUT)); + Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl)); + Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl)); Assertions.assertEquals(2, calls.get()); } @@ -188,7 +192,7 @@ class HttpGetterTest { return HttpResponse.response().withBody("ok"); }); - Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl, TIMEOUT)); + Assertions.assertDoesNotThrow(() -> getter.getBinary(this.feedUrl)); } @Test @@ -197,7 +201,7 @@ class HttpGetterTest { Arrays.fill(bytes, (byte) 1); this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody(bytes)); - IOException e = Assertions.assertThrows(IOException.class, () -> getter.getBinary(this.feedUrl, TIMEOUT)); + IOException e = Assertions.assertThrows(IOException.class, () -> getter.getBinary(this.feedUrl)); Assertions.assertEquals("Response size (100000 bytes) exceeds the maximum allowed size (10000 bytes)", e.getMessage()); } @@ -210,7 +214,7 @@ class HttpGetterTest { .withBody(bytes) .withConnectionOptions(ConnectionOptions.connectionOptions().withSuppressContentLengthHeader(true))); - IOException e = Assertions.assertThrows(IOException.class, () -> getter.getBinary(this.feedUrl, TIMEOUT)); + IOException e = Assertions.assertThrows(IOException.class, () -> getter.getBinary(this.feedUrl)); Assertions.assertEquals("Response size exceeds the maximum allowed size (10000 bytes)", e.getMessage()); } @@ -218,7 +222,7 @@ class HttpGetterTest { void ignoreInvalidSsl() throws Exception { this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody("ok")); - HttpResult result = getter.getBinary("https://localhost:" + this.mockServerClient.getPort(), TIMEOUT); + HttpResult result = getter.getBinary("https://localhost:" + this.mockServerClient.getPort()); Assertions.assertEquals("ok", new String(result.getContent())); } 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 c231cdd9..089f6e3c 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 @@ -45,8 +45,8 @@ class FeedFetcherTest { byte[] content = "content".getBytes(); String lastContentHash = Hashing.sha1().hashBytes(content).toString(); - Mockito.when(getter.getBinary(url, lastModified, etag, 20000)) - .thenReturn(new HttpResult(content, "content-type", "last-modified-2", "etag-2", 20, null)); + Mockito.when(getter.getBinary(url, lastModified, etag)) + .thenReturn(new HttpResult(content, "content-type", "last-modified-2", "etag-2", null)); NotModifiedException e = Assertions.assertThrows(NotModifiedException.class, () -> fetcher.fetch(url, false, lastModified, etag, Instant.now(), lastContentHash)); From e7748d787f3a4111950cd368f7acaeb248c0ee42 Mon Sep 17 00:00:00 2001 From: Athou Date: Fri, 16 Aug 2024 22:26:30 +0200 Subject: [PATCH 43/50] no need to start a transaction to fetch favicons --- .../src/main/java/com/commafeed/frontend/resource/FeedREST.java | 1 - 1 file changed, 1 deletion(-) 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 9309594f..9c233817 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 @@ -341,7 +341,6 @@ public class FeedREST { @GET @Path("/favicon/{id}") @Cache(maxAge = 2592000) - @Transactional @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); From 2395a2670ed63e02d9fdacce31e9e8a4be79bf18 Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 00:43:32 +0200 Subject: [PATCH 44/50] add ResourceBundle needed for cssparser in native image --- .../META-INF/native-image/cssparser/resource-config.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 commafeed-server/src/main/resources/META-INF/native-image/cssparser/resource-config.json diff --git a/commafeed-server/src/main/resources/META-INF/native-image/cssparser/resource-config.json b/commafeed-server/src/main/resources/META-INF/native-image/cssparser/resource-config.json new file mode 100644 index 00000000..29a9854d --- /dev/null +++ b/commafeed-server/src/main/resources/META-INF/native-image/cssparser/resource-config.json @@ -0,0 +1,7 @@ +{ + "bundles": [ + { + "name": "com.steadystate.css.parser.SACParserMessages" + } + ] +} \ No newline at end of file From e38ca66c5139fb716d80001919fd3d1fb8797472 Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 16:22:46 +0200 Subject: [PATCH 45/50] try to fix "Illegal attempt to associate a ManagedEntity with two open persistence contexts" --- .../main/java/com/commafeed/backend/dao/GenericDAO.java | 8 ++++++++ .../com/commafeed/backend/feed/FeedRefreshUpdater.java | 2 +- .../java/com/commafeed/backend/service/FeedService.java | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) 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 dff4307b..1b5306c6 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 @@ -43,6 +43,14 @@ public abstract class GenericDAO { models.forEach(this::saveOrUpdate); } + public void persist(T model) { + entityManager.persist(model); + } + + public T merge(T model) { + return entityManager.merge(model); + } + public T findById(Long id) { return entityManager.find(entityClass, id); } 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 b5df83cc..6bf9d009 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 @@ -157,7 +157,7 @@ public class FeedRefreshUpdater { feedUpdated.mark(); } - unitOfWork.run(() -> feedService.save(feed)); + unitOfWork.run(() -> feedService.update(feed)); notifyOverWebsocket(unreadCountBySubscription); 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 49cfd653..302c692c 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 @@ -47,18 +47,18 @@ public class FeedService { feed.setNormalizedUrl(normalizedUrl); feed.setNormalizedUrlHash(normalizedUrlHash); feed.setDisabledUntil(Models.MINIMUM_INSTANT); - feedDAO.saveOrUpdate(feed); + feedDAO.persist(feed); } return feed; } - public void save(Feed feed) { + public void update(Feed feed) { String normalized = FeedUtils.normalizeURL(feed.getUrl()); feed.setNormalizedUrl(normalized); feed.setNormalizedUrlHash(Digests.sha1Hex(normalized)); feed.setLastUpdated(Instant.now()); feed.setEtagHeader(FeedUtils.truncate(feed.getEtagHeader(), 255)); - feedDAO.saveOrUpdate(feed); + feedDAO.merge(feed); } public Favicon fetchFavicon(Feed feed) { From 9218f198321466d6ec4d289999261d7600521d1d Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 22:11:47 +0200 Subject: [PATCH 46/50] javadoc tweaks --- .../src/main/java/com/commafeed/CommaFeedConfiguration.java | 6 ++++-- .../backend/feed/FeedRefreshIntervalCalculator.java | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index ef6abff5..033c4c8a 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -138,8 +138,10 @@ public interface CommaFeedConfiguration { Duration interval(); /** - * If true, CommaFeed will calculate the next refresh time based on the feed's average entry interval and the time since the last - * entry was published. See {@link FeedRefreshIntervalCalculator} for details. + * If true, 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 somewhere between the default refresh interval and 24h. + * + * See {@link FeedRefreshIntervalCalculator} for details. */ @WithDefault("false") boolean intervalEmpirical(); 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 8aea399e..48051719 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 @@ -21,7 +21,7 @@ public class FeedRefreshIntervalCalculator { public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) { Instant defaultRefreshInterval = getDefaultRefreshInterval(); - return empiricalInterval ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval) + return empiricalInterval ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval, defaultRefreshInterval) : defaultRefreshInterval; } @@ -43,7 +43,7 @@ public class FeedRefreshIntervalCalculator { return Instant.now().plus(refreshInterval); } - private Instant computeRefreshIntervalForHeavyLoad(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) { + private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) { Instant now = Instant.now(); if (publishedDate == null) { From c577e77f8ffc0872daa764fc026699403c00957d Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 22:21:21 +0200 Subject: [PATCH 47/50] README tweaks --- README.md | 88 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 8fb216bb..1261bcae 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,17 @@ Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeSc - 4 different layouts - Light/Dark theme -- Fully responsive +- Fully responsive, works great on both mobile and desktop - Keyboard shortcuts for almost everything - Support for right-to-left feeds - Translated in 25+ languages - Supports thousands of users and millions of feeds - OPML import/export -- REST API and a Fever-compatible API for native mobile apps +- REST API +- Fever-compatible API for native mobile apps +- Can automatically mark articles as read based on user-defined rules - [Browser extension](https://github.com/Athou/commafeed-browser-extension) -- Compiles to native code for very fast startup and low memory usage +- Compiles to native code for blazing fast startup and low memory usage - Supports 4 databases - H2 (embedded database) - PostgreSQL @@ -56,12 +58,12 @@ memory usage. ### Build from sources - ./mvnw clean package -P [-DskipTests] [-Pnative] + ./mvnw clean package [-P] [-Pnative] [-DskipTests] -- `` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. -- `-DskipTests` is optional but recommended because tests require a Docker environment to run against a real database. +- `` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`. - `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment variable pointing to a GraalVM installation). +- `-DskipTests` to speed up the build process by skipping tests. When the build is complete: @@ -71,6 +73,43 @@ When the build is complete: - if you used the native profile, the executable is located at `commafeed-server/target/commafeed-----runner[.exe]` +## Configuration + +CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in +the `data` directory of the current directory. + +To use a different database, you will need to configure the following properties: + +- `quarkus.datasource.jdbc-url` + - e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE` + - e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed` + - e.g. for MySQL: + `jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` + - e.g. for MariaDB: + `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` +- `quarkus.datasource.username` +- `quarkus.datasource.password` + +There are multiple ways to configure CommaFeed: + +- a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case) +- Command line arguments prefixed with `-D` (keys in kebab-case) +- Environment variables (keys in UPPER_CASE) +- an .env file in the working directory (keys in UPPER_CASE) + +The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos. + +When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, +meaning that you will have to log back in after each restart of the application. To prevent this, you can set the +`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters). + +All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) +are optional and have sensible default values. +Other Quarkus settings can be found [here](https://quarkus.io/guides/all-config). + +When started, the server will listen on http://localhost:8082. +The default user is `admin` and the default password is `admin`. + ### Memory management (`jvm` package only) The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the @@ -103,43 +142,6 @@ IBM provides precompiled binaries for OpenJ9 named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/). This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/blob/master/Dockerfile). -## Configuration - -CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in -the `data` directory in the working directory. - -To use a different database, you will need to configure the following properties: - -- `quarkus.datasource.jdbc-url` - - e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE` - - e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed` - - e.g. for MySQL: - `jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` - - e.g. for MariaDB: - `jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC` -- `quarkus.datasource.username` -- `quarkus.datasource.password` - -There are multiple ways to configure CommaFeed: - -- a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case) -- Command line arguments prefixed with `-D` (keys in kebab-case) -- Environment variables (keys in UPPER_CASE) -- an .env file in the working directory (keys in UPPER_CASE) - -The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos. - -When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, -meaning that you will have to log back in after each restart of the application. To prevent this, you can set the -`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters). - -All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) -are optional and have sensible default values. Quarkus settings can be -found [here](https://quarkus.io/guides/all-config). - -When started, the server will listen on http://localhost:8082. -The default user is `admin` and the default password is `admin`. - ## Translation Files for internationalization are From c4c41d14947578acd0bd7de8dbfb551413f877c9 Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 22:29:40 +0200 Subject: [PATCH 48/50] increase timeout a little bit because github actions are laggy --- .../src/test/java/com/commafeed/backend/HttpGetterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ff2d0409..d2833a8e 100644 --- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java +++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java @@ -56,9 +56,9 @@ class HttpGetterTest { 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.ofMillis(300)); + Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500)); Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(5)); - Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofMillis(300)); + Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofMillis(500)); Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(300)); Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30)); Mockito.when(config.httpClient().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000"))); From 3627ee369d38810715d960c66fd7ef6dda4e1859 Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 22:58:23 +0200 Subject: [PATCH 49/50] version dockerhub readme and update it automatically on release --- .github/workflows/dockerhub.yml | 18 +++++ README.md | 1 - commafeed-server/src/main/docker/README.md | 86 ++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/dockerhub.yml create mode 100644 commafeed-server/src/main/docker/README.md diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml new file mode 100644 index 00000000..8392b5fb --- /dev/null +++ b/.github/workflows/dockerhub.yml @@ -0,0 +1,18 @@ +name: Update Docker Hub Description + +on: release + +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: athou/commafeed + short-description: ${{ github.event.repository.description }} + readme-filepath: commafeed-server/src/main/docker/README.md \ No newline at end of file diff --git a/README.md b/README.md index 1261bcae..56c49a24 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,6 @@ meaning that you will have to log back in after each restart of the application. All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) are optional and have sensible default values. -Other Quarkus settings can be found [here](https://quarkus.io/guides/all-config). When started, the server will listen on http://localhost:8082. The default user is `admin` and the default password is `admin`. diff --git a/commafeed-server/src/main/docker/README.md b/commafeed-server/src/main/docker/README.md new file mode 100644 index 00000000..5f7ed37d --- /dev/null +++ b/commafeed-server/src/main/docker/README.md @@ -0,0 +1,86 @@ +# CommaFeed + +Official docker images for https://github.com/Athou/commafeed/ + +## Quickstart + +Start CommaFeed with an embedded database. Then login as `admin/admin` on http://localhost:8082/ + +### docker + +`docker run --name commafeed --detach --publish 8082:8082 --restart unless-stopped --volume /path/to/commafeed/db:/commafeed/data --memory 256M athou/commafeed:latest-h2` + +### docker-compose + +``` +services: + commafeed: + image: athou/commafeed:latest-h2 + restart: unless-stopped + volumes: + - /path/to/commafeed/db:/commafeed/data + deploy: + resources: + limits: + memory: 256M + ports: + - 8082:8082 +``` + +## Advanced + +While using the embedded database is perfectly fine for small instances, you may want to have more control over the +database. Here's an example that uses postgresql (note the different docker tag): + +``` +services: + commafeed: + image: athou/commafeed:latest-postgresql + restart: unless-stopped + environment: + - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql:5432/commafeed + - QUARKUS_DATASOURCE_USERNAME=commafeed + - QUARKUS_DATASOURCE_PASSWORD=commafeed + deploy: + resources: + limits: + memory: 256M + ports: + - 8082:8082 + + postgresql: + image: postgres:latest + restart: unless-stopped + environment: + POSTGRES_USER: commafeed + POSTGRES_PASSWORD: commafeed + POSTGRES_DB: commafeed + volumes: + - /path/to/commafeed/db:/var/lib/postgresql/data +``` + +## Configuration + +All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java) +are optional and have sensible default values. + +Settings are overrideable with environment variables. For instance, `config.feedRefresh().intervalEmpirical()` can be +set +with the `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL=true` variable. + +When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup, +meaning that you will have to log back in after each restart of the application. To prevent this, you can set the +`QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY` property to a fixed value (min. 16 characters). + +## Docker tags + +Tags are of the form `-[-jvm]` where: + +- `` is either: + - a specific CommaFeed version (e.g. `4.6.0`) + - `latest` (always points to the latest version) + - `master` (always points to the latest git commit) +- `` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`) +- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports + the + arm64 platform which is not yet supported by the native image. \ No newline at end of file From ede7834cb8910403a61b42b8d850978cfbb4d8a1 Mon Sep 17 00:00:00 2001 From: Athou Date: Sat, 17 Aug 2024 23:26:42 +0200 Subject: [PATCH 50/50] configurable filtering expression evaluation timeout --- .../main/java/com/commafeed/CommaFeedConfiguration.java | 6 ++++++ .../backend/service/FeedEntryFilteringService.java | 5 ++++- .../backend/service/FeedEntryFilteringServiceTest.java | 9 ++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java index 033c4c8a..cc016038 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java @@ -167,6 +167,12 @@ public interface CommaFeedConfiguration { */ @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(); } interface Database { 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 6832c58a..dd8857d6 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 @@ -23,6 +23,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.LogFactory; import org.jsoup.Jsoup; +import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.model.FeedEntry; import jakarta.inject.Singleton; @@ -35,6 +36,7 @@ public class FeedEntryFilteringService { private static final JexlEngine ENGINE = initEngine(); private final ExecutorService executor = Executors.newCachedThreadPool(); + private final CommaFeedConfiguration config; private static JexlEngine initEngine() { // classloader that prevents object creation @@ -96,8 +98,9 @@ public class FeedEntryFilteringService { Future future = executor.submit(callable); Object result; try { - result = future.get(500, TimeUnit.MILLISECONDS); + 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); 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 5987dd07..34b1dd11 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,9 +1,13 @@ package com.commafeed.backend.service; +import java.time.Duration; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; 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; @@ -16,7 +20,10 @@ class FeedEntryFilteringServiceTest { @BeforeEach public void init() { - service = new FeedEntryFilteringService(); + CommaFeedConfiguration config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofSeconds(2)); + + service = new FeedEntryFilteringService(config); entry = new FeedEntry(); entry.setUrl("https://github.com/Athou/commafeed");