Compare commits

...

78 Commits
4.4.1 ... 4.5.0

Author SHA1 Message Date
Athou
4d83173dbd release 4.5.0 2024-07-06 21:29:21 +02:00
Athou
f13368cb96 remove unnecessary joins 2024-07-06 13:40:53 +02:00
Athou
ec7e97e1de abort current request if we're changing what we're going to display 2024-07-04 07:19:16 +02:00
Athou
d4c9bd1dd7 remove warnings 2024-07-03 20:11:51 +02:00
renovate[bot]
6bff657d4d Update linguijs monorepo to ^4.11.2 2024-07-03 17:13:52 +00:00
renovate[bot]
613d286be1 Update dependency react-router-dom to ^6.24.1 2024-07-03 16:05:19 +00:00
Athou
fd48108f8b don't rely on dates to know if an entry has been inserted in the database 2024-07-03 17:53:41 +02:00
Athou
c3cbd18df9 add debug logging 2024-07-03 17:40:23 +02:00
Athou
6685057dae notify over websocket after everything has been committed 2024-07-03 17:27:17 +02:00
Athou
0dec0e3788 fix a race condition where a feed could be refreshed before it was created 2024-07-03 14:21:40 +02:00
Athou
1a73dd4004 the feed refresh engine is now fast enough, it doesn't need workarounds anymore 2024-07-03 13:30:25 +02:00
Athou
eae80a6450 remove support for microsoft sqlserver, AFAIK nobody's using it and it's not covered with integration tests 2024-07-03 13:02:20 +02:00
Jérémie Panzer
21a32ce0eb Merge pull request #1479 from Athou/renovate/mysql-9.x
Update mysql Docker tag to v9
2024-07-03 11:35:35 +02:00
renovate[bot]
325533c5d9 Update mysql Docker tag to v9 2024-07-03 09:10:22 +00:00
renovate[bot]
7d819022f6 Update redis Docker tag to v7.2.5 2024-07-03 09:10:19 +00:00
Athou
dba944874b add redis integration test 2024-07-03 11:09:23 +02:00
Jérémie Panzer
ce9c12ec92 Merge pull request #1478 from Athou/renovate/postgres-16.x
Update postgres Docker tag to v16.3
2024-07-03 09:23:35 +02:00
Jérémie Panzer
22dfc5774f Merge pull request #1477 from Athou/renovate/mariadb-11.x
Update mariadb Docker tag to v11.4.2
2024-07-03 09:23:24 +02:00
renovate[bot]
d59091ab2b Update postgres Docker tag to v16.3 2024-07-03 07:08:14 +00:00
renovate[bot]
f69146a6bf Update mariadb Docker tag to v11.4.2 2024-07-03 07:08:11 +00:00
Athou
43cdf3db3b add integration tests for postgresql, mysql and mariadb using testcontainers 2024-07-03 09:02:18 +02:00
renovate[bot]
280a354228 Update dependency vite-plugin-biome to ^1.0.12 2024-07-03 06:39:02 +00:00
renovate[bot]
573b0431f9 Update dependency vite to ^5.3.3 2024-07-03 06:32:04 +00:00
renovate[bot]
9878b60e97 Update dependency io.github.git-commit-id:git-commit-id-maven-plugin to v9.0.1 2024-07-02 18:02:54 +00:00
renovate[bot]
964033c2a7 Update mantine monorepo to ^7.11.1 2024-07-02 14:12:43 +00:00
Jérémie Panzer
d2e45aca91 Merge pull request #1475 from Athou/renovate/com.mysql-mysql-connector-j-9.x
Update dependency com.mysql:mysql-connector-j to v9
2024-07-02 16:11:43 +02:00
renovate[bot]
daa99a2efc Update dependency com.mysql:mysql-connector-j to v9 2024-07-02 10:08:39 +00:00
Jérémie Panzer
e986e9999a Merge pull request #1473 from Athou/renovate/node-20.x
Update dependency node to v20.15.0
2024-07-02 09:19:55 +02:00
Jérémie Panzer
98d302cb94 Merge pull request #1474 from Athou/renovate/npm-10.x
Update dependency npm to v10.8.1
2024-07-02 09:19:44 +02:00
renovate[bot]
bf11c4a7e4 Update dependency npm to v10.8.1 2024-07-02 07:13:46 +00:00
renovate[bot]
e1cab952f8 Update dependency node to v20.15.0 2024-07-02 07:13:42 +00:00
Athou
bc28d4de27 renovate can now update node and npm 2024-07-02 09:12:42 +02:00
renovate[bot]
bb901564e3 Update dependency typescript to ^5.5.3 2024-07-01 22:21:16 +00:00
Athou
93acc9ded1 we don't need the user we already have the subscription 2024-07-01 13:55:50 +02:00
renovate[bot]
9b1c6a371e Lock file maintenance 2024-07-01 07:50:40 +00:00
Athou
82bf8cd807 fetch all tags at once 2024-07-01 08:52:05 +02:00
Athou
c2f2780c3f remove unused onlyIds parameter 2024-07-01 08:52:05 +02:00
Athou
08f71d1f6f implement faster querying by fetching directly what we need 2024-07-01 08:52:05 +02:00
Athou
f498088beb no need to insert statuses that will be collected during next cleanup 2024-06-30 21:56:42 +02:00
Athou
347b41cf35 fix exception when trying to mark starred entries as read 2024-06-30 16:50:37 +02:00
renovate[bot]
61ae90ad28 Update dependency @reduxjs/toolkit to ^2.2.6 2024-06-29 14:12:13 +00:00
Athou
9a42fbafb2 only automerge patch updates, keep creating pull requests for minor updates 2024-06-29 08:47:49 +02:00
Athou
938f9e9434 remove automergePr because we now specify automergeBranch 2024-06-28 07:27:58 +02:00
Jérémie Panzer
9004e453c2 Merge pull request #1470 from Athou/renovate/querydsl.version
Update querydsl.version to v6.5
2024-06-28 07:26:21 +02:00
renovate[bot]
7d33542691 Update querydsl.version to v6.5 2024-06-28 05:25:56 +00:00
Athou
c99348862c reduce renovate noise by automerging if tests pass 2024-06-28 07:25:10 +02:00
Jérémie Panzer
ac86db3966 Merge pull request #1463 from Athou/renovate/com.manticore-projects.tools-h2migrationtool-1.x
Update dependency com.manticore-projects.tools:h2migrationtool to v1.6
2024-06-28 07:21:29 +02:00
Athou
e368810731 adapt script to new H2MigrationTool file output name 2024-06-28 07:16:42 +02:00
renovate[bot]
edae2f5a61 Update dependency com.manticore-projects.tools:h2migrationtool to v1.6 2024-06-28 03:34:34 +00:00
renovate[bot]
ab17c6f44e Update dependency org.projectlombok:lombok to v1.18.34 2024-06-28 03:34:21 +00:00
renovate[bot]
59dbae4f66 Update dependency com.microsoft.playwright:playwright to v1.45.0 2024-06-28 01:23:51 +00:00
renovate[bot]
d7956292df Update dependency vite-plugin-biome to ^1.0.11 2024-06-27 21:19:36 +00:00
renovate[bot]
1075497559 Update dependency vite to ^5.3.2 2024-06-27 19:44:59 +00:00
renovate[bot]
2d99fa03d3 Update dependency @biomejs/biome to v1.8.3 2024-06-27 16:49:21 +00:00
Jérémie Panzer
72b64b6f0d Merge pull request #1465 from Athou/renovate/mantine-monorepo
Update mantine monorepo to ^7.11.0
2024-06-27 09:35:00 +02:00
Jérémie Panzer
a2096d3622 Merge pull request #1457 from Athou/renovate/monaco-editor-0.x
Update dependency monaco-editor to ^0.50.0
2024-06-27 09:34:52 +02:00
Jérémie Panzer
c81f9fb7b1 Merge pull request #1459 from Athou/renovate/throttle-debounce-5.x
Update dependency throttle-debounce to ^5.0.2
2024-06-27 09:34:44 +02:00
Jérémie Panzer
cc7e9e21fb Merge pull request #1461 from Athou/renovate/react-router-monorepo
Update dependency react-router-dom to ^6.24.0
2024-06-27 09:34:35 +02:00
Jérémie Panzer
803d537e51 Merge pull request #1456 from Athou/renovate/biomejs-biome-1.x
Update dependency @biomejs/biome to v1.8.2
2024-06-27 09:34:20 +02:00
Jérémie Panzer
9a83e5b6ef Merge pull request #1458 from Athou/renovate/typescript-5.x
Update dependency typescript to ^5.5.2
2024-06-27 09:34:08 +02:00
renovate[bot]
4323da9007 Update mantine monorepo to ^7.11.0 2024-06-27 07:28:56 +00:00
renovate[bot]
30b9b24be4 Update dependency typescript to ^5.5.2 2024-06-27 07:28:42 +00:00
renovate[bot]
b191b00003 Update dependency react-router-dom to ^6.24.0 2024-06-27 07:28:30 +00:00
renovate[bot]
7e5cdcba34 Update dependency monaco-editor to ^0.50.0 2024-06-27 07:28:16 +00:00
renovate[bot]
45b30ad333 Update dependency throttle-debounce to ^5.0.2 2024-06-27 07:27:58 +00:00
renovate[bot]
7ca087b0a6 Update dependency @biomejs/biome to v1.8.2 2024-06-27 07:27:47 +00:00
Jérémie Panzer
188e4594fd automerge small dependency updates if they pass tests 2024-06-27 09:26:48 +02:00
Jérémie Panzer
2da80ce7d8 Merge pull request #1453 from Athou/renovate/org.apache.maven.plugins-maven-jar-plugin-3.x
Update dependency org.apache.maven.plugins:maven-jar-plugin to v3.4.2
2024-06-20 14:09:39 +02:00
renovate[bot]
d5820f9aa5 Update dependency org.apache.maven.plugins:maven-jar-plugin to v3.4.2 2024-06-19 18:42:15 +00:00
Athou
b1a0aae0a5 treat javac warnings as errors 2024-06-19 19:41:09 +02:00
Athou
cdd4d4b063 change BaseIT test class so that authentication with the "admin" user is not the default 2024-06-19 19:41:02 +02:00
Jérémie Panzer
21f675e80b Merge pull request #1451 from Athou/renovate/querydsl.version
Update querydsl.version to v6.4
2024-06-19 18:53:35 +02:00
renovate[bot]
380724d73e Update querydsl.version to v6.4 2024-06-19 16:49:05 +00:00
Athou
2d26c5dee3 remove empty file 2024-06-18 20:40:36 +02:00
Jérémie Panzer
29bcc5ccf5 Merge pull request #1437 from Athou/renovate/maven-3.x
Update dependency maven to v3.9.8
2024-06-17 22:31:07 +02:00
Jérémie Panzer
91497ab45a Merge pull request #1450 from Athou/renovate/docker-build-push-action-6.x
Update docker/build-push-action action to v6
2024-06-17 22:30:49 +02:00
renovate[bot]
be77968570 Update docker/build-push-action action to v6 2024-06-17 10:38:49 +00:00
renovate[bot]
a42dacc48d Update dependency maven to v3.9.8 2024-06-17 10:38:45 +00:00
55 changed files with 2650 additions and 1442 deletions

View File

@@ -29,10 +29,34 @@ jobs:
distribution: "temurin" distribution: "temurin"
cache: "maven" cache: "maven"
# Build # Build & Test
- name: Build with Maven - name: Build with Maven
run: mvn --batch-mode --update-snapshots verify 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
# Upload artifacts
- name: Upload JAR - name: Upload JAR
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ matrix.java == '17' }} if: ${{ matrix.java == '17' }}
@@ -57,7 +81,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker build and push tag - name: Docker build and push tag
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }} if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with: with:
context: . context: .
@@ -68,7 +92,7 @@ jobs:
athou/commafeed:${{ github.ref_name }} athou/commafeed:${{ github.ref_name }}
- name: Docker build and push master - name: Docker build and push master
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
if: ${{ matrix.java == '17' && github.ref_name == 'master' }} if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
with: with:
context: . context: .

View File

@@ -15,4 +15,4 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
distributionType=only-script distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip

View File

@@ -1,5 +1,17 @@
# Changelog # Changelog
## [4.5.0]
- significantly reduce the time needed to retrieve entries or mark them as read, especially when there are a lot of
entries (#1452)
- fix a race condition where a feed could be refreshed before it was created in the database
- fix an issue that could cause the websocket notification to contain the wrong number of unread entries when using
mysql/mariadb
- fix an error when trying to mark all starred entries as read
- remove the `onlyIds` parameter from REST endpoints since retrieving all the entries is now just as fast
- remove support for microsoft sqlserver because it's not covered with integration tests (please open an issue if you'd
like it back)
## [4.4.1] ## [4.4.1]
- fix vertical scrolling issues with Safari (#1168) - fix vertical scrolling issues with Safari (#1168)

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json", "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"formatter": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 4,

File diff suppressed because it is too large Load Diff

View File

@@ -17,22 +17,22 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@fontsource/open-sans": "^5.0.28", "@fontsource/open-sans": "^5.0.28",
"@lingui/core": "^4.11.1", "@lingui/core": "^4.11.2",
"@lingui/macro": "^4.11.1", "@lingui/macro": "^4.11.2",
"@lingui/react": "^4.11.1", "@lingui/react": "^4.11.2",
"@mantine/core": "^7.10.2", "@mantine/core": "^7.11.1",
"@mantine/form": "^7.10.2", "@mantine/form": "^7.11.1",
"@mantine/hooks": "^7.10.2", "@mantine/hooks": "^7.11.1",
"@mantine/modals": "^7.10.2", "@mantine/modals": "^7.11.1",
"@mantine/notifications": "^7.10.2", "@mantine/notifications": "^7.11.1",
"@mantine/spotlight": "^7.10.2", "@mantine/spotlight": "^7.11.1",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.6",
"axios": "^1.7.2", "axios": "^1.7.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.49.0", "monaco-editor": "^0.50.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
@@ -45,20 +45,20 @@
"react-icons": "^5.2.1", "react-icons": "^5.2.1",
"react-infinite-scroller": "^1.2.6", "react-infinite-scroller": "^1.2.6",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.24.1",
"react-swipeable": "^7.0.1", "react-swipeable": "^7.0.1",
"redoc": "^2.1.5", "redoc": "^2.1.5",
"throttle-debounce": "^5.0.0", "throttle-debounce": "^5.0.2",
"tinycon": "^0.6.8", "tinycon": "^0.6.8",
"tss-react": "^4.9.10", "tss-react": "^4.9.10",
"use-local-storage": "^3.0.0", "use-local-storage": "^3.0.0",
"vite-plugin-biome": "^1.0.10", "vite-plugin-biome": "^1.0.12",
"websocket-heartbeat-js": "^1.1.3" "websocket-heartbeat-js": "^1.1.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.8.1", "@biomejs/biome": "^1.8.3",
"@lingui/cli": "^4.11.1", "@lingui/cli": "^4.11.2",
"@lingui/vite-plugin": "^4.11.1", "@lingui/vite-plugin": "^4.11.2",
"@types/mousetrap": "^1.6.15", "@types/mousetrap": "^1.6.15",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
@@ -70,8 +70,8 @@
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.4.5", "typescript": "^5.5.3",
"vite": "^5.3.1", "vite": "^5.3.3",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"vitest-mock-extended": "^1.3.1" "vitest-mock-extended": "^1.3.1"

View File

@@ -1,15 +1,23 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>4.4.1</version> <version>4.5.0</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>
<properties>
<!-- renovate: datasource=node-version depName=node -->
<node.version>v20.15.0</node.version>
<!-- renovate: datasource=npm depName=npm -->
<npm.version>10.8.1</npm.version>
</properties>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
@@ -25,8 +33,8 @@
</goals> </goals>
<phase>compile</phase> <phase>compile</phase>
<configuration> <configuration>
<nodeVersion>v20.10.0</nodeVersion> <nodeVersion>${node.version}</nodeVersion>
<npmVersion>10.2.5</npmVersion> <npmVersion>${npm.version}</npmVersion>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>

View File

@@ -117,7 +117,6 @@ export interface GetEntriesRequest {
newerThan?: number newerThan?: number
order?: ReadingOrder order?: ReadingOrder
keywords?: string keywords?: string
onlyIds?: boolean
excludedSubscriptionIds?: string excludedSubscriptionIds?: string
tag?: string tag?: string
} }

View File

@@ -64,7 +64,7 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
// biome-ignore lint/correctness/useExhaustiveDependencies: we subscribe to state.timestamp because we want to reload entries even if the props are the same // biome-ignore lint/correctness/useExhaustiveDependencies: we subscribe to state.timestamp because we want to reload entries even if the props are the same
useEffect(() => { useEffect(() => {
dispatch( const promise = dispatch(
loadEntries({ loadEntries({
source: { source: {
type: props.sourceType, type: props.sourceType,
@@ -73,6 +73,7 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
clearSearch: true, clearSearch: true,
}) })
) )
return () => promise.abort()
}, [dispatch, props.sourceType, id, location.state?.timestamp]) }, [dispatch, props.sourceType, id, location.state?.timestamp])
const noSubscriptions = rootCategory && flattenCategoryTree(rootCategory).every(c => c.feeds.length === 0) const noSubscriptions = rootCategory && flattenCategoryTree(rootCategory).every(c => c.feeds.length === 0)

View File

@@ -104,10 +104,6 @@ app:
# for PostgreSQL # for PostgreSQL
# driverClass is org.postgresql.Driver # driverClass is org.postgresql.Driver
# url is jdbc:postgresql://localhost:5432/commafeed # url is jdbc:postgresql://localhost:5432/commafeed
#
# for Microsoft SQL Server
# driverClass is net.sourceforge.jtds.jdbc.Driver
# url is jdbc:jtds:sqlserver://localhost:1433/commafeed;instance=<instanceName, remove if not needed>
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver

View File

@@ -104,10 +104,6 @@ app:
# for PostgreSQL # for PostgreSQL
# driverClass is org.postgresql.Driver # driverClass is org.postgresql.Driver
# url is jdbc:postgresql://localhost:5432/commafeed # url is jdbc:postgresql://localhost:5432/commafeed
#
# for Microsoft SQL Server
# driverClass is net.sourceforge.jtds.jdbc.Driver
# url is jdbc:jtds:sqlserver://localhost:1433/commafeed;instance=<instanceName, remove if not needed>
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver

View File

@@ -7,7 +7,7 @@ services:
- MYSQL_ROOT_PASSWORD=root - MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=commafeed - MYSQL_DATABASE=commafeed
ports: ports:
- 3306:3306 - "3306:3306"
postgresql: postgresql:
image: postgres image: postgres
@@ -16,4 +16,4 @@ services:
- POSTGRES_PASSWORD=root - POSTGRES_PASSWORD=root
- POSTGRES_DB=commafeed - POSTGRES_DB=commafeed
ports: ports:
- 5432:5432 - "5432:5432"

View File

@@ -6,15 +6,25 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>4.4.1</version> <version>4.5.0</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
<properties> <properties>
<guice.version>7.0.0</guice.version> <guice.version>7.0.0</guice.version>
<querydsl.version>6.3</querydsl.version> <querydsl.version>6.5</querydsl.version>
<rome.version>2.1.0</rome.version> <rome.version>2.1.0</rome.version>
<testcontainers.version>1.19.8</testcontainers.version>
<!-- renovate: datasource=docker depName=postgres -->
<postgresql.image.version>16.3</postgresql.image.version>
<!-- renovate: datasource=docker depName=mysql -->
<mysql.image.version>9.0.0</mysql.image.version>
<!-- renovate: datasource=docker depName=mariadb -->
<mariadb.image.version>11.4.2</mariadb.image.version>
<!-- renovate: datasource=docker depName=redis -->
<redis.image.version>7.2.5</redis.image.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -31,6 +41,19 @@
<build> <build>
<finalName>commafeed</finalName> <finalName>commafeed</finalName>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>false</filtering>
</testResource>
<testResource>
<directory>src/test/resources</directory>
<includes>
<include>docker-images.properties</include>
</includes>
<filtering>true</filtering>
</testResource>
</testResources>
<plugins> <plugins>
<plugin> <plugin>
@@ -54,7 +77,7 @@
<plugin> <plugin>
<groupId>io.github.git-commit-id</groupId> <groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId> <artifactId>git-commit-id-maven-plugin</artifactId>
<version>9.0.0</version> <version>9.0.1</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>
@@ -64,7 +87,8 @@
</executions> </executions>
<configuration> <configuration>
<generateGitPropertiesFile>true</generateGitPropertiesFile> <generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename> <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties
</generateGitPropertiesFilename>
<failOnNoGitDirectory>false</failOnNoGitDirectory> <failOnNoGitDirectory>false</failOnNoGitDirectory>
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo> <failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
</configuration> </configuration>
@@ -86,6 +110,7 @@
<filter> <filter>
<artifact>*:*</artifact> <artifact>*:*</artifact>
<excludes> <excludes>
<exclude>module-info.class</exclude>
<exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude> <exclude>META-INF/*.RSA</exclude>
@@ -145,7 +170,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.4.1</version> <version>3.4.2</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>
@@ -211,13 +236,13 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>4.4.1</version> <version>4.5.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.32</version> <version>1.18.34</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -403,13 +428,13 @@
<dependency> <dependency>
<groupId>com.manticore-projects.tools</groupId> <groupId>com.manticore-projects.tools</groupId>
<artifactId>h2migrationtool</artifactId> <artifactId>h2migrationtool</artifactId>
<version>1.4</version> <version>1.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version> <version>9.0.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mariadb.jdbc</groupId> <groupId>org.mariadb.jdbc</groupId>
@@ -421,11 +446,6 @@
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<version>42.7.3</version> <version>42.7.3</version>
</dependency> </dependency>
<dependency>
<groupId>net.sourceforge.jtds</groupId>
<artifactId>jtds</artifactId>
<version>1.3.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
@@ -467,9 +487,33 @@
<dependency> <dependency>
<groupId>com.microsoft.playwright</groupId> <groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId> <artifactId>playwright</artifactId>
<version>1.44.0</version> <version>1.45.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mariadb</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -98,9 +98,9 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
configureObjectMapper(bootstrap.getObjectMapper()); configureObjectMapper(bootstrap.getObjectMapper());
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized // run h2 migration as the first bundle because we need to migrate before hibernate is initialized
bootstrap.addBundle(new ConfiguredBundle<CommaFeedConfiguration>() { bootstrap.addBundle(new ConfiguredBundle<>() {
@Override @Override
public void run(CommaFeedConfiguration config, Environment environment) throws Exception { public void run(CommaFeedConfiguration config, Environment environment) {
DataSourceFactory dataSourceFactory = config.getDataSourceFactory(); DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
String url = dataSourceFactory.getUrl(); String url = dataSourceFactory.getUrl();
if (isFileBasedH2(url)) { if (isFileBasedH2(url)) {

View File

@@ -1,49 +0,0 @@
package com.commafeed.backend;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* List wrapper that sorts its elements in the order provided by given comparator and ensure a maximum capacity.
*
*
*/
public class FixedSizeSortedList<E> {
private final List<E> inner;
private final Comparator<? super E> comparator;
private final int capacity;
public FixedSizeSortedList(int capacity, Comparator<? super E> comparator) {
this.inner = new ArrayList<>(Math.max(0, capacity));
this.capacity = capacity < 0 ? Integer.MAX_VALUE : capacity;
this.comparator = comparator;
}
public void add(E e) {
int position = Math.abs(Collections.binarySearch(inner, e, comparator) + 1);
if (isFull()) {
if (position < inner.size()) {
inner.remove(inner.size() - 1);
inner.add(position, e);
}
} else {
inner.add(position, e);
}
}
public E last() {
return inner.get(inner.size() - 1);
}
public boolean isFull() {
return inner.size() == capacity;
}
public List<E> asList() {
return inner;
}
}

View File

@@ -17,7 +17,7 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> { public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
private final QFeedCategory category = QFeedCategory.feedCategory; private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
@Inject @Inject
public FeedCategoryDAO(SessionFactory sessionFactory) { public FeedCategoryDAO(SessionFactory sessionFactory) {
@@ -25,31 +25,31 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} }
public List<FeedCategory> findAll(User user) { public List<FeedCategory> findAll(User user) {
return query().selectFrom(category).where(category.user.eq(user)).join(category.user, QUser.user).fetchJoin().fetch(); return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user)).join(CATEGORY.user, QUser.user).fetchJoin().fetch();
} }
public FeedCategory findById(User user, Long id) { public FeedCategory findById(User user, Long id) {
return query().selectFrom(category).where(category.user.eq(user), category.id.eq(id)).fetchOne(); return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.id.eq(id)).fetchOne();
} }
public FeedCategory findByName(User user, String name, FeedCategory parent) { public FeedCategory findByName(User user, String name, FeedCategory parent) {
Predicate parentPredicate; Predicate parentPredicate;
if (parent == null) { if (parent == null) {
parentPredicate = category.parent.isNull(); parentPredicate = CATEGORY.parent.isNull();
} else { } else {
parentPredicate = category.parent.eq(parent); parentPredicate = CATEGORY.parent.eq(parent);
} }
return query().selectFrom(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).fetchOne(); return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), CATEGORY.name.eq(name), parentPredicate).fetchOne();
} }
public List<FeedCategory> findByParent(User user, FeedCategory parent) { public List<FeedCategory> findByParent(User user, FeedCategory parent) {
Predicate parentPredicate; Predicate parentPredicate;
if (parent == null) { if (parent == null) {
parentPredicate = category.parent.isNull(); parentPredicate = CATEGORY.parent.isNull();
} else { } else {
parentPredicate = category.parent.eq(parent); parentPredicate = CATEGORY.parent.eq(parent);
} }
return query().selectFrom(category).where(category.user.eq(user), parentPredicate).fetch(); return query().selectFrom(CATEGORY).where(CATEGORY.user.eq(user), parentPredicate).fetch();
} }
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) { public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {

View File

@@ -18,8 +18,8 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
private final QFeed feed = QFeed.feed; private static final QFeed FEED = QFeed.feed;
private final QFeedSubscription subscription = QFeedSubscription.feedSubscription; private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
@Inject @Inject
public FeedDAO(SessionFactory sessionFactory) { public FeedDAO(SessionFactory sessionFactory) {
@@ -27,25 +27,25 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) { public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {
JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(Instant.now()))); JPAQuery<Feed> query = query().selectFrom(FEED).where(FEED.disabledUntil.isNull().or(FEED.disabledUntil.lt(Instant.now())));
if (lastLoginThreshold != null) { if (lastLoginThreshold != null) {
query.where(JPAExpressions.selectOne() query.where(JPAExpressions.selectOne()
.from(subscription) .from(SUBSCRIPTION)
.join(subscription.user) .join(SUBSCRIPTION.user)
.where(subscription.feed.id.eq(feed.id), subscription.user.lastLogin.gt(lastLoginThreshold)) .where(SUBSCRIPTION.feed.id.eq(FEED.id), SUBSCRIPTION.user.lastLogin.gt(lastLoginThreshold))
.exists()); .exists());
} }
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch(); return query.orderBy(FEED.disabledUntil.asc()).limit(count).fetch();
} }
public void setDisabledUntil(List<Long> feedIds, Instant date) { public void setDisabledUntil(List<Long> feedIds, Instant date) {
updateQuery(feed).set(feed.disabledUntil, date).where(feed.id.in(feedIds)).execute(); updateQuery(FEED).set(FEED.disabledUntil, date).where(FEED.id.in(feedIds)).execute();
} }
public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) { public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
return query().selectFrom(feed) return query().selectFrom(FEED)
.where(feed.normalizedUrlHash.eq(normalizedUrlHash)) .where(FEED.normalizedUrlHash.eq(normalizedUrlHash))
.fetch() .fetch()
.stream() .stream()
.filter(f -> StringUtils.equals(normalizedUrl, f.getNormalizedUrl())) .filter(f -> StringUtils.equals(normalizedUrl, f.getNormalizedUrl()))
@@ -55,6 +55,6 @@ public class FeedDAO extends GenericDAO<Feed> {
public List<Feed> findWithoutSubscriptions(int max) { public List<Feed> findWithoutSubscriptions(int max) {
QFeedSubscription sub = QFeedSubscription.feedSubscription; QFeedSubscription sub = QFeedSubscription.feedSubscription;
return query().selectFrom(feed).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max).fetch(); return query().selectFrom(FEED).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(FEED)).notExists()).limit(max).fetch();
} }
} }

View File

@@ -16,8 +16,8 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> { public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
private final QFeedEntryContent content = QFeedEntryContent.feedEntryContent; private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
private final QFeedEntry entry = QFeedEntry.feedEntry; private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
@Inject @Inject
public FeedEntryContentDAO(SessionFactory sessionFactory) { public FeedEntryContentDAO(SessionFactory sessionFactory) {
@@ -25,13 +25,13 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
} }
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) { public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
return query().select(content).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).fetch(); return query().select(CONTENT).from(CONTENT).where(CONTENT.contentHash.eq(contentHash), CONTENT.titleHash.eq(titleHash)).fetch();
} }
public long deleteWithoutEntries(int max) { public long deleteWithoutEntries(int max) {
JPQLSubQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id)); JPQLSubQuery<Integer> subQuery = JPAExpressions.selectOne().from(ENTRY).where(ENTRY.content.id.eq(CONTENT.id));
List<Long> ids = query().select(content.id).from(content).where(subQuery.notExists()).limit(max).fetch(); List<Long> ids = query().select(CONTENT.id).from(CONTENT).where(subQuery.notExists()).limit(max).fetch();
return deleteQuery(content).where(content.id.in(ids)).execute(); return deleteQuery(CONTENT).where(CONTENT.id.in(ids)).execute();
} }
} }

View File

@@ -19,7 +19,7 @@ import lombok.Getter;
@Singleton @Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
private final QFeedEntry entry = QFeedEntry.feedEntry; private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
@Inject @Inject
public FeedEntryDAO(SessionFactory sessionFactory) { public FeedEntryDAO(SessionFactory sessionFactory) {
@@ -27,22 +27,22 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
public FeedEntry findExisting(String guidHash, Feed feed) { public FeedEntry findExisting(String guidHash, Feed feed) {
return query().select(entry).from(entry).where(entry.guidHash.eq(guidHash), entry.feed.eq(feed)).limit(1).fetchOne(); return query().select(ENTRY).from(ENTRY).where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed)).limit(1).fetchOne();
} }
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) { public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
NumberExpression<Long> count = entry.id.count(); NumberExpression<Long> count = ENTRY.id.count();
List<Tuple> tuples = query().select(entry.feed.id, count) List<Tuple> tuples = query().select(ENTRY.feed.id, count)
.from(entry) .from(ENTRY)
.groupBy(entry.feed) .groupBy(ENTRY.feed)
.having(count.gt(maxCapacity)) .having(count.gt(maxCapacity))
.limit(max) .limit(max)
.fetch(); .fetch();
return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).toList(); return tuples.stream().map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count))).toList();
} }
public int delete(Long feedId, long max) { public int delete(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch(); List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).limit(max).fetch();
return delete(list); return delete(list);
} }
@@ -50,7 +50,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
* Delete entries older than a certain date * Delete entries older than a certain date
*/ */
public int deleteEntriesOlderThan(Instant olderThan, long max) { public int deleteEntriesOlderThan(Instant olderThan, long max) {
List<FeedEntry> list = query().selectFrom(entry).where(entry.updated.lt(olderThan)).orderBy(entry.updated.asc()).limit(max).fetch(); List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.updated.lt(olderThan)).orderBy(ENTRY.updated.asc()).limit(max).fetch();
return delete(list); return delete(list);
} }
@@ -58,7 +58,7 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
* Delete the oldest entries of a feed * Delete the oldest entries of a feed
*/ */
public int deleteOldEntries(Long feedId, long max) { public int deleteOldEntries(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).fetch(); List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.updated.asc()).limit(max).fetch();
return delete(list); return delete(list);
} }

View File

@@ -2,23 +2,20 @@ package com.commafeed.backend.dao;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.FixedSizeSortedList;
import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode; import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent; import com.commafeed.backend.model.QFeedEntryContent;
import com.commafeed.backend.model.QFeedEntryStatus; import com.commafeed.backend.model.QFeedEntryStatus;
@@ -27,7 +24,6 @@ import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder; import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.UnreadCount; import com.commafeed.frontend.model.UnreadCount;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple; import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
@@ -38,39 +34,30 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> { public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = (o1, o2) -> { private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
CompareToBuilder builder = new CompareToBuilder(); private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated()); private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
builder.append(o2.getId(), o1.getId()); private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
return builder.toComparison();
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
private final FeedEntryDAO feedEntryDAO;
private final FeedEntryTagDAO feedEntryTagDAO; private final FeedEntryTagDAO feedEntryTagDAO;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final QFeedEntryStatus status = QFeedEntryStatus.feedEntryStatus;
private final QFeedEntry entry = QFeedEntry.feedEntry;
private final QFeedEntryContent content = QFeedEntryContent.feedEntryContent;
private final QFeedEntryTag entryTag = QFeedEntryTag.feedEntryTag;
@Inject @Inject
public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryDAO feedEntryDAO, FeedEntryTagDAO feedEntryTagDAO, public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
CommaFeedConfiguration config) {
super(sessionFactory); super(sessionFactory);
this.feedEntryDAO = feedEntryDAO;
this.feedEntryTagDAO = feedEntryTagDAO; this.feedEntryTagDAO = feedEntryTagDAO;
this.config = config; this.config = config;
} }
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) { public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
List<FeedEntryStatus> statuses = query().selectFrom(status).where(status.entry.eq(entry), status.subscription.eq(sub)).fetch(); List<FeedEntryStatus> statuses = query().selectFrom(STATUS).where(STATUS.entry.eq(entry), STATUS.subscription.eq(sub)).fetch();
FeedEntryStatus status = Iterables.getFirst(statuses, null); FeedEntryStatus status = Iterables.getFirst(statuses, null);
return handleStatus(user, status, sub, entry); return handleStatus(user, status, sub, entry);
} }
/**
* creates an artificial "unread" status if status is null
*/
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) { private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) { if (status == null) {
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
@@ -84,101 +71,103 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return status; return status;
} }
private FeedEntryStatus fetchTags(User user, FeedEntryStatus status) { private void fetchTags(User user, List<FeedEntryStatus> statuses) {
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, status.getEntry()); Map<Long, List<FeedEntryTag>> tagsByEntryIds = feedEntryTagDAO.findByEntries(user,
status.setTags(tags); statuses.stream().map(FeedEntryStatus::getEntry).toList());
return status; for (FeedEntryStatus status : statuses) {
List<FeedEntryTag> tags = tagsByEntryIds.get(status.getEntry().getId());
status.setTags(tags == null ? List.of() : tags);
}
} }
public List<FeedEntryStatus> findStarred(User user, Instant newerThan, int offset, int limit, ReadingOrder order, public List<FeedEntryStatus> findStarred(User user, Instant newerThan, int offset, int limit, ReadingOrder order,
boolean includeContent) { boolean includeContent) {
JPAQuery<FeedEntryStatus> query = query().selectFrom(status).where(status.user.eq(user), status.starred.isTrue()); JPAQuery<FeedEntryStatus> query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
if (includeContent) {
query.join(STATUS.entry.content).fetchJoin();
}
if (newerThan != null) { if (newerThan != null) {
query.where(status.entryInserted.gt(newerThan)); query.where(STATUS.entryInserted.gt(newerThan));
} }
if (order == ReadingOrder.asc) { if (order == ReadingOrder.asc) {
query.orderBy(status.entryUpdated.asc(), status.id.asc()); query.orderBy(STATUS.entryUpdated.asc(), STATUS.id.asc());
} else { } else {
query.orderBy(status.entryUpdated.desc(), status.id.desc()); query.orderBy(STATUS.entryUpdated.desc(), STATUS.id.desc());
}
if (offset > -1) {
query.offset(offset);
}
if (limit > -1) {
query.limit(limit);
} }
query.offset(offset).limit(limit);
setTimeout(query, config.getApplicationSettings().getQueryTimeout()); setTimeout(query, config.getApplicationSettings().getQueryTimeout());
List<FeedEntryStatus> statuses = query.fetch(); List<FeedEntryStatus> statuses = query.fetch();
for (FeedEntryStatus status : statuses) { statuses.forEach(s -> s.setMarkable(true));
status = handleStatus(user, status, status.getSubscription(), status.getEntry()); if (includeContent) {
fetchTags(user, status); fetchTags(user, statuses);
} }
return lazyLoadContent(includeContent, statuses);
return statuses;
} }
private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords, public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
Instant newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId, List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
Long maxEntryId) { String tag, Long minEntryId, Long maxEntryId) {
Map<Long, List<FeedSubscription>> subsByFeedId = subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId()));
JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed())); JPAQuery<Tuple> query = query().select(ENTRY, STATUS).from(ENTRY);
query.leftJoin(ENTRY.statuses, STATUS).on(STATUS.subscription.in(subs));
query.where(ENTRY.feed.id.in(subsByFeedId.keySet()));
if (includeContent || CollectionUtils.isNotEmpty(keywords)) {
query.join(ENTRY.content, CONTENT).fetchJoin();
}
if (CollectionUtils.isNotEmpty(keywords)) { if (CollectionUtils.isNotEmpty(keywords)) {
query.join(entry.content, content);
for (FeedEntryKeyword keyword : keywords) { for (FeedEntryKeyword keyword : keywords) {
BooleanBuilder or = new BooleanBuilder(); BooleanBuilder or = new BooleanBuilder();
or.or(content.content.containsIgnoreCase(keyword.getKeyword())); or.or(CONTENT.content.containsIgnoreCase(keyword.getKeyword()));
or.or(content.title.containsIgnoreCase(keyword.getKeyword())); or.or(CONTENT.title.containsIgnoreCase(keyword.getKeyword()));
if (keyword.getMode() == Mode.EXCLUDE) { if (keyword.getMode() == Mode.EXCLUDE) {
or.not(); or.not();
} }
query.where(or); query.where(or);
} }
} }
query.leftJoin(entry.statuses, status).on(status.subscription.id.eq(sub.getId()));
if (unreadOnly && tag == null) { if (unreadOnly && tag == null) {
BooleanBuilder or = new BooleanBuilder(); query.where(buildUnreadPredicate());
or.or(status.read.isNull());
or.or(status.read.isFalse());
query.where(or);
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
if (unreadThreshold != null) {
query.where(entry.updated.goe(unreadThreshold));
}
} }
if (tag != null) { if (tag != null) {
BooleanBuilder and = new BooleanBuilder(); BooleanBuilder and = new BooleanBuilder();
and.and(entryTag.user.id.eq(user.getId())); and.and(TAG.user.id.eq(user.getId()));
and.and(entryTag.name.eq(tag)); and.and(TAG.name.eq(tag));
query.join(entry.tags, entryTag).on(and); query.join(ENTRY.tags, TAG).on(and);
} }
if (newerThan != null) { if (newerThan != null) {
query.where(entry.inserted.goe(newerThan)); query.where(ENTRY.inserted.goe(newerThan));
} }
if (minEntryId != null) { if (minEntryId != null) {
query.where(entry.id.gt(minEntryId)); query.where(ENTRY.id.gt(minEntryId));
} }
if (maxEntryId != null) { if (maxEntryId != null) {
query.where(entry.id.lt(maxEntryId)); query.where(ENTRY.id.lt(maxEntryId));
}
if (last != null) {
if (order == ReadingOrder.desc) {
query.where(entry.updated.gt(last.getEntryUpdated()));
} else {
query.where(entry.updated.lt(last.getEntryUpdated()));
}
} }
if (order != null) { if (order != null) {
if (order == ReadingOrder.asc) { if (order == ReadingOrder.asc) {
query.orderBy(entry.updated.asc(), entry.id.asc()); query.orderBy(ENTRY.updated.asc(), ENTRY.id.asc());
} else { } else {
query.orderBy(entry.updated.desc(), entry.id.desc()); query.orderBy(ENTRY.updated.desc(), ENTRY.id.desc());
} }
} }
@@ -191,100 +180,58 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
setTimeout(query, config.getApplicationSettings().getQueryTimeout()); setTimeout(query, config.getApplicationSettings().getQueryTimeout());
return query;
}
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, List<FeedEntryStatus> statuses = new ArrayList<>();
List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, List<Tuple> tuples = query.fetch();
boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) { for (Tuple tuple : tuples) {
int capacity = offset + limit; FeedEntry e = tuple.get(ENTRY);
FeedEntryStatus s = tuple.get(STATUS);
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC; for (FeedSubscription sub : subsByFeedId.get(e.getFeed().getId())) {
statuses.add(handleStatus(user, s, sub, e));
FixedSizeSortedList<FeedEntryStatus> fssl = new FixedSizeSortedList<>(capacity, comparator);
for (FeedSubscription sub : subs) {
FeedEntryStatus last = (order != null && fssl.isFull()) ? fssl.last() : null;
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId,
maxEntryId);
List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
for (Tuple tuple : tuples) {
Long id = tuple.get(entry.id);
Instant updated = tuple.get(entry.updated);
Long statusId = tuple.get(status.id);
FeedEntryContent content = new FeedEntryContent();
content.setTitle(tuple.get(entry.content.title));
FeedEntry entry = new FeedEntry();
entry.setId(id);
entry.setUpdated(updated);
entry.setContent(content);
FeedEntryStatus status = new FeedEntryStatus();
status.setId(statusId);
status.setEntryUpdated(updated);
status.setEntry(entry);
status.setSubscription(sub);
fssl.add(status);
} }
} }
List<FeedEntryStatus> placeholders = fssl.asList(); if (includeContent) {
int size = placeholders.size(); fetchTags(user, statuses);
if (size < offset) {
return new ArrayList<>();
} }
placeholders = placeholders.subList(Math.max(offset, 0), size);
List<FeedEntryStatus> statuses;
if (onlyIds) {
statuses = placeholders;
} else {
statuses = new ArrayList<>();
for (FeedEntryStatus placeholder : placeholders) {
Long statusId = placeholder.getId();
FeedEntry entry = feedEntryDAO.findById(placeholder.getEntry().getId());
FeedEntryStatus status = handleStatus(user, statusId == null ? null : findById(statusId), placeholder.getSubscription(),
entry);
status = fetchTags(user, status);
statuses.add(status);
}
statuses = lazyLoadContent(includeContent, statuses);
}
return statuses; return statuses;
} }
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) { public UnreadCount getUnreadCount(FeedSubscription sub) {
UnreadCount uc = null; JPAQuery<Tuple> query = query().select(ENTRY.count(), ENTRY.updated.max())
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null, null, null); .from(ENTRY)
List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch(); .leftJoin(ENTRY.statuses, STATUS)
for (Tuple tuple : tuples) { .on(STATUS.subscription.eq(sub))
Long count = tuple.get(entry.count()); .where(ENTRY.feed.eq(sub.getFeed()))
Instant updated = tuple.get(entry.updated.max()); .where(buildUnreadPredicate());
uc = new UnreadCount(subscription.getId(), count == null ? 0 : count, updated);
} Tuple tuple = query.fetchOne();
return uc; Long count = tuple.get(ENTRY.count());
Instant updated = tuple.get(ENTRY.updated.max());
return new UnreadCount(sub.getId(), count == null ? 0 : count, updated);
} }
private List<FeedEntryStatus> lazyLoadContent(boolean includeContent, List<FeedEntryStatus> results) { private BooleanBuilder buildUnreadPredicate() {
if (includeContent) { BooleanBuilder or = new BooleanBuilder();
for (FeedEntryStatus status : results) { or.or(STATUS.read.isNull());
Models.initialize(status.getSubscription().getFeed()); or.or(STATUS.read.isFalse());
Models.initialize(status.getEntry().getContent());
} Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
if (unreadThreshold != null) {
return or.and(ENTRY.updated.goe(unreadThreshold));
} else {
return or;
} }
return results;
} }
public long deleteOldStatuses(Instant olderThan, int limit) { public long deleteOldStatuses(Instant olderThan, int limit) {
List<Long> ids = query().select(status.id) List<Long> ids = query().select(STATUS.id)
.from(status) .from(STATUS)
.where(status.entryInserted.lt(olderThan), status.starred.isFalse()) .where(STATUS.entryInserted.lt(olderThan), STATUS.starred.isFalse())
.limit(limit) .limit(limit)
.fetch(); .fetch();
return deleteQuery(status).where(status.id.in(ids)).execute(); return deleteQuery(STATUS).where(STATUS.id.in(ids)).execute();
} }
} }

View File

@@ -1,6 +1,8 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
@@ -15,7 +17,7 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> { public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
private final QFeedEntryTag tag = QFeedEntryTag.feedEntryTag; private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
@Inject @Inject
public FeedEntryTagDAO(SessionFactory sessionFactory) { public FeedEntryTagDAO(SessionFactory sessionFactory) {
@@ -23,10 +25,18 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
} }
public List<String> findByUser(User user) { public List<String> findByUser(User user) {
return query().selectDistinct(tag.name).from(tag).where(tag.user.eq(user)).fetch(); return query().selectDistinct(TAG.name).from(TAG).where(TAG.user.eq(user)).fetch();
} }
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) { public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
return query().selectFrom(tag).where(tag.user.eq(user), tag.entry.eq(entry)).fetch(); return query().selectFrom(TAG).where(TAG.user.eq(user), TAG.entry.eq(entry)).fetch();
}
public Map<Long, List<FeedEntryTag>> findByEntries(User user, List<FeedEntry> entries) {
return query().selectFrom(TAG)
.where(TAG.user.eq(user), TAG.entry.in(entries))
.fetch()
.stream()
.collect(Collectors.groupingBy(t -> t.getEntry().getId()));
} }
} }

View File

@@ -2,9 +2,16 @@ package com.commafeed.backend.dao;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.persister.entity.EntityPersister;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@@ -22,54 +29,79 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> { public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
private final QFeedSubscription sub = QFeedSubscription.feedSubscription; private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
private final SessionFactory sessionFactory;
@Inject @Inject
public FeedSubscriptionDAO(SessionFactory sessionFactory) { public FeedSubscriptionDAO(SessionFactory sessionFactory) {
super(sessionFactory); super(sessionFactory);
this.sessionFactory = sessionFactory;
}
public void onPostCommitInsert(Consumer<FeedSubscription> consumer) {
sessionFactory.unwrap(SessionFactoryImplementor.class)
.getServiceRegistry()
.getService(EventListenerRegistry.class)
.getEventListenerGroup(EventType.POST_COMMIT_INSERT)
.appendListener(new PostInsertEventListener() {
@Override
public void onPostInsert(PostInsertEvent event) {
if (event.getEntity() instanceof FeedSubscription s) {
consumer.accept(s);
}
}
@Override
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
});
} }
public FeedSubscription findById(User user, Long id) { public FeedSubscription findById(User user, Long id) {
List<FeedSubscription> subs = query().selectFrom(sub) List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION)
.where(sub.user.eq(user), sub.id.eq(id)) .where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.id.eq(id))
.leftJoin(sub.feed) .leftJoin(SUBSCRIPTION.feed)
.fetchJoin() .fetchJoin()
.leftJoin(sub.category) .leftJoin(SUBSCRIPTION.category)
.fetchJoin() .fetchJoin()
.fetch(); .fetch();
return initRelations(Iterables.getFirst(subs, null)); return initRelations(Iterables.getFirst(subs, null));
} }
public List<FeedSubscription> findByFeed(Feed feed) { public List<FeedSubscription> findByFeed(Feed feed) {
return query().selectFrom(sub).where(sub.feed.eq(feed)).fetch(); return query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.feed.eq(feed)).fetch();
} }
public FeedSubscription findByFeed(User user, Feed feed) { public FeedSubscription findByFeed(User user, Feed feed) {
List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.feed.eq(feed)).fetch(); List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION)
.where(SUBSCRIPTION.user.eq(user), SUBSCRIPTION.feed.eq(feed))
.fetch();
return initRelations(Iterables.getFirst(subs, null)); return initRelations(Iterables.getFirst(subs, null));
} }
public List<FeedSubscription> findAll(User user) { public List<FeedSubscription> findAll(User user) {
List<FeedSubscription> subs = query().selectFrom(sub) List<FeedSubscription> subs = query().selectFrom(SUBSCRIPTION)
.where(sub.user.eq(user)) .where(SUBSCRIPTION.user.eq(user))
.leftJoin(sub.feed) .leftJoin(SUBSCRIPTION.feed)
.fetchJoin() .fetchJoin()
.leftJoin(sub.category) .leftJoin(SUBSCRIPTION.category)
.fetchJoin() .fetchJoin()
.fetch(); .fetch();
return initRelations(subs); return initRelations(subs);
} }
public Long count(User user) { public Long count(User user) {
return query().select(sub.count()).from(sub).where(sub.user.eq(user)).fetchOne(); return query().select(SUBSCRIPTION.count()).from(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user)).fetchOne();
} }
public List<FeedSubscription> findByCategory(User user, FeedCategory category) { public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user)); JPQLQuery<FeedSubscription> query = query().selectFrom(SUBSCRIPTION).where(SUBSCRIPTION.user.eq(user));
if (category == null) { if (category == null) {
query.where(sub.category.isNull()); query.where(SUBSCRIPTION.category.isNull());
} else { } else {
query.where(sub.category.eq(category)); query.where(SUBSCRIPTION.category.eq(category));
} }
return initRelations(query.fetch()); return initRelations(query.fetch());
} }

View File

@@ -11,7 +11,7 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class UserDAO extends GenericDAO<User> { public class UserDAO extends GenericDAO<User> {
private final QUser user = QUser.user; private static final QUser USER = QUser.user;
@Inject @Inject
public UserDAO(SessionFactory sessionFactory) { public UserDAO(SessionFactory sessionFactory) {
@@ -19,18 +19,18 @@ public class UserDAO extends GenericDAO<User> {
} }
public User findByName(String name) { public User findByName(String name) {
return query().selectFrom(user).where(user.name.equalsIgnoreCase(name)).fetchOne(); return query().selectFrom(USER).where(USER.name.equalsIgnoreCase(name)).fetchOne();
} }
public User findByApiKey(String key) { public User findByApiKey(String key) {
return query().selectFrom(user).where(user.apiKey.equalsIgnoreCase(key)).fetchOne(); return query().selectFrom(USER).where(USER.apiKey.equalsIgnoreCase(key)).fetchOne();
} }
public User findByEmail(String email) { public User findByEmail(String email) {
return query().selectFrom(user).where(user.email.equalsIgnoreCase(email)).fetchOne(); return query().selectFrom(USER).where(USER.email.equalsIgnoreCase(email)).fetchOne();
} }
public long count() { public long count() {
return query().select(user.count()).from(user).fetchOne(); return query().select(USER.count()).from(USER).fetchOne();
} }
} }

View File

@@ -17,7 +17,7 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class UserRoleDAO extends GenericDAO<UserRole> { public class UserRoleDAO extends GenericDAO<UserRole> {
private final QUserRole role = QUserRole.userRole; private static final QUserRole ROLE = QUserRole.userRole;
@Inject @Inject
public UserRoleDAO(SessionFactory sessionFactory) { public UserRoleDAO(SessionFactory sessionFactory) {
@@ -25,11 +25,11 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
} }
public List<UserRole> findAll() { public List<UserRole> findAll() {
return query().selectFrom(role).leftJoin(role.user).fetchJoin().distinct().fetch(); return query().selectFrom(ROLE).leftJoin(ROLE.user).fetchJoin().distinct().fetch();
} }
public List<UserRole> findAll(User user) { public List<UserRole> findAll(User user) {
return query().selectFrom(role).where(role.user.eq(user)).distinct().fetch(); return query().selectFrom(ROLE).where(ROLE.user.eq(user)).distinct().fetch();
} }
public Set<Role> findRoles(User user) { public Set<Role> findRoles(User user) {

View File

@@ -12,7 +12,7 @@ import jakarta.inject.Singleton;
@Singleton @Singleton
public class UserSettingsDAO extends GenericDAO<UserSettings> { public class UserSettingsDAO extends GenericDAO<UserSettings> {
private final QUserSettings settings = QUserSettings.userSettings; private static final QUserSettings SETTINGS = QUserSettings.userSettings;
@Inject @Inject
public UserSettingsDAO(SessionFactory sessionFactory) { public UserSettingsDAO(SessionFactory sessionFactory) {
@@ -20,6 +20,6 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
} }
public UserSettings findByUser(User user) { public UserSettings findByUser(User user) {
return query().selectFrom(settings).where(settings.user.eq(user)).fetchFirst(); return query().selectFrom(SETTINGS).where(SETTINGS.user.eq(user)).fetchFirst();
} }
} }

View File

@@ -1,6 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@@ -103,9 +102,12 @@ public class FeedRefreshUpdater {
if (locked1 && locked2) { if (locked1 && locked2) {
processed = true; processed = true;
inserted = unitOfWork.call(() -> { inserted = unitOfWork.call(() -> {
Instant now = Instant.now(); boolean newEntry = false;
FeedEntry feedEntry = feedEntryService.findOrCreate(feed, entry); FeedEntry feedEntry = feedEntryService.find(feed, entry);
boolean newEntry = !feedEntry.getInserted().isBefore(now); if (feedEntry == null) {
feedEntry = feedEntryService.create(feed, entry);
newEntry = true;
}
if (newEntry) { if (newEntry) {
entryInserted.mark(); entryInserted.mark();
for (FeedSubscription sub : subscriptions) { for (FeedSubscription sub : subscriptions) {
@@ -118,10 +120,10 @@ public class FeedRefreshUpdater {
return newEntry; return newEntry;
}); });
} else { } else {
log.error("lock timeout for " + feed.getUrl() + " - " + key1); log.error("lock timeout for {} - {}", feed.getUrl(), key1);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("interrupted while waiting for lock for " + feed.getUrl() + " : " + e.getMessage(), e); log.error("interrupted while waiting for lock for {} : {}", feed.getUrl(), e.getMessage(), e);
} finally { } finally {
if (locked1) { if (locked1) {
lock1.unlock(); lock1.unlock();
@@ -168,11 +170,10 @@ public class FeedRefreshUpdater {
if (subscriptions == null) { if (subscriptions == null) {
feed.setMessage("No new entries found"); feed.setMessage("No new entries found");
} else if (inserted > 0) { } else if (inserted > 0) {
log.debug("inserted {} entries for feed {}", inserted, feed.getId());
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).toList(); List<User> users = subscriptions.stream().map(FeedSubscription::getUser).toList();
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0])); cache.invalidateUserRootCategory(users.toArray(new User[0]));
notifyOverWebsocket(unreadCountBySubscription);
} }
} }
@@ -187,6 +188,8 @@ public class FeedRefreshUpdater {
unitOfWork.run(() -> feedService.save(feed)); unitOfWork.run(() -> feedService.save(feed));
notifyOverWebsocket(unreadCountBySubscription);
return processed; return processed;
} }

View File

@@ -139,7 +139,7 @@ public class FeedUtils {
try { try {
return new URL(new URL(baseUrl), relativeUrl).toString(); return new URL(new URL(baseUrl), relativeUrl).toString();
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
log.debug("could not parse url : " + e.getMessage(), e); log.debug("could not parse url : {}", e.getMessage(), e);
return null; return null;
} }
} }

View File

@@ -35,13 +35,9 @@ public class RSSRDF10Parser extends RSS10Parser {
ok = defaultNS != null && defaultNS.equals(getRDFNamespace()); ok = defaultNS != null && defaultNS.equals(getRDFNamespace());
if (ok) { if (ok) {
if (additionalNSs == null) { ok = false;
ok = false; for (int i = 0; !ok && i < additionalNSs.size(); i++) {
} else { ok = getRSSNamespace().equals(additionalNSs.get(i));
ok = false;
for (int i = 0; !ok && i < additionalNSs.size(); i++) {
ok = getRSSNamespace().equals(additionalNSs.get(i));
}
} }
} }
return ok; return ok;

View File

@@ -35,18 +35,21 @@ public class FeedEntryService {
private final FeedEntryFilteringService feedEntryFilteringService; private final FeedEntryFilteringService feedEntryFilteringService;
private final CacheService cache; private final CacheService cache;
/** public FeedEntry find(Feed feed, Entry entry) {
* this is NOT thread-safe
*/
public FeedEntry findOrCreate(Feed feed, Entry entry) {
String guid = FeedUtils.truncate(entry.guid(), 2048);
String guidHash = Digests.sha1Hex(entry.guid()); String guidHash = Digests.sha1Hex(entry.guid());
FeedEntry existing = feedEntryDAO.findExisting(guidHash, feed); return feedEntryDAO.findExisting(guidHash, feed);
if (existing != null) { }
return existing;
} public FeedEntry create(Feed feed, Entry entry) {
FeedEntry feedEntry = new FeedEntry();
feedEntry.setGuid(FeedUtils.truncate(entry.guid(), 2048));
feedEntry.setGuidHash(Digests.sha1Hex(entry.guid()));
feedEntry.setUrl(FeedUtils.truncate(entry.url(), 2048));
feedEntry.setUpdated(entry.updated());
feedEntry.setInserted(Instant.now());
feedEntry.setFeed(feed);
feedEntry.setContent(feedEntryContentService.findOrCreate(entry.content(), feed.getLink()));
FeedEntry feedEntry = buildEntry(feed, entry, guid, guidHash);
feedEntryDAO.saveOrUpdate(feedEntry); feedEntryDAO.saveOrUpdate(feedEntry);
return feedEntry; return feedEntry;
} }
@@ -68,19 +71,6 @@ public class FeedEntryService {
return matches; return matches;
} }
private FeedEntry buildEntry(Feed feed, Entry e, String guid, String guidHash) {
FeedEntry entry = new FeedEntry();
entry.setGuid(guid);
entry.setGuidHash(guidHash);
entry.setUrl(FeedUtils.truncate(e.url(), 2048));
entry.setUpdated(e.updated());
entry.setInserted(Instant.now());
entry.setFeed(feed);
entry.setContent(feedEntryContentService.findOrCreate(e.content(), feed.getLink()));
return entry;
}
public void markEntry(User user, Long entryId, boolean read) { public void markEntry(User user, Long entryId, boolean read) {
FeedEntry entry = feedEntryDAO.findById(entryId); FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) { if (entry == null) {
@@ -121,7 +111,7 @@ public class FeedEntryService {
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Instant olderThan, Instant insertedBefore, public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Instant olderThan, Instant insertedBefore,
List<FeedEntryKeyword> keywords) { List<FeedEntryKeyword> keywords) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null, List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
false, false, null, null, null); false, null, null, null);
markList(statuses, olderThan, insertedBefore); markList(statuses, olderThan, insertedBefore);
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(user); cache.invalidateUserRootCategory(user);
@@ -133,7 +123,7 @@ public class FeedEntryService {
} }
private void markList(List<FeedEntryStatus> statuses, Instant olderThan, Instant insertedBefore) { private void markList(List<FeedEntryStatus> statuses, Instant olderThan, Instant insertedBefore) {
List<FeedEntryStatus> statusesToMark = statuses.stream().filter(s -> { List<FeedEntryStatus> statusesToMark = statuses.stream().filter(FeedEntryStatus::isMarkable).filter(s -> {
Instant entryDate = s.getEntry().getUpdated(); Instant entryDate = s.getEntry().getUpdated();
return olderThan == null || entryDate == null || entryDate.isBefore(olderThan); return olderThan == null || entryDate == null || entryDate.isBefore(olderThan);
}).filter(s -> { }).filter(s -> {

View File

@@ -23,11 +23,9 @@ import com.commafeed.frontend.model.UnreadCount;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
public class FeedSubscriptionService { public class FeedSubscriptionService {
@@ -39,6 +37,22 @@ public class FeedSubscriptionService {
private final CacheService cache; private final CacheService cache;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
@Inject
public FeedSubscriptionService(FeedDAO feedDAO, FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO,
FeedService feedService, FeedRefreshEngine feedRefreshEngine, CacheService cache, 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
// we need to use this hook because the feed needs to have been persisted because the queue processing is asynchronous
feedSubscriptionDAO.onPostCommitInsert(sub -> feedRefreshEngine.refreshImmediately(sub.getFeed()));
}
public long subscribe(User user, String url, String title) { public long subscribe(User user, String url, String title) {
return subscribe(user, url, title, null, 0); return subscribe(user, url, title, null, 0);
} }
@@ -83,7 +97,6 @@ public class FeedSubscriptionService {
sub.setTitle(FeedUtils.truncate(title, 128)); sub.setTitle(FeedUtils.truncate(title, 128));
feedSubscriptionDAO.saveOrUpdate(sub); feedSubscriptionDAO.saveOrUpdate(sub);
feedRefreshEngine.refreshImmediately(feed);
cache.invalidateUserRootCategory(user); cache.invalidateUserRootCategory(user);
return sub.getId(); return sub.getId();
} }
@@ -119,14 +132,14 @@ public class FeedSubscriptionService {
} }
public Map<Long, UnreadCount> getUnreadCount(User user) { public Map<Long, UnreadCount> getUnreadCount(User user) {
return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, s -> getUnreadCount(user, s))); return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, this::getUnreadCount));
} }
private UnreadCount getUnreadCount(User user, FeedSubscription sub) { private UnreadCount getUnreadCount(FeedSubscription sub) {
UnreadCount count = cache.getUnreadCount(sub); UnreadCount count = cache.getUnreadCount(sub);
if (count == null) { if (count == null) {
log.debug("unread count cache miss for {}", Models.getId(sub)); log.debug("unread count cache miss for {}", Models.getId(sub));
count = feedEntryStatusDAO.getUnreadCount(user, sub); count = feedEntryStatusDAO.getUnreadCount(sub);
cache.setUnreadCount(sub, count); cache.setUnreadCount(sub, count);
} }
return count; return count;

View File

@@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class H2MigrationService { public class H2MigrationService {
private static final String H2_FILE_SUFFIX = ".mv.db";
public void migrateIfNeeded(Path path, String user, String password) { public void migrateIfNeeded(Path path, String user, String password) {
if (Files.notExists(path)) { if (Files.notExists(path)) {
return; return;
@@ -54,9 +56,10 @@ public class H2MigrationService {
private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception { 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); log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion);
Path scriptPath = path.resolveSibling("script-" + System.currentTimeMillis() + ".sql"); Path scriptPath = path.resolveSibling("script-%d.sql".formatted(System.currentTimeMillis()));
Path newVersionPath = path.resolveSibling(path.getFileName() + "." + getPatchVersion(toVersion) + ".mv.db"); Path newVersionPath = path.resolveSibling("%s.%s%s".formatted(StringUtils.removeEnd(path.getFileName().toString(), H2_FILE_SUFFIX),
Path oldVersionBackupPath = path.resolveSibling(path.getFileName() + "." + getPatchVersion(fromVersion) + ".backup"); getPatchVersion(toVersion), H2_FILE_SUFFIX));
Path oldVersionBackupPath = path.resolveSibling("%s.%s.backup".formatted(path.getFileName(), getPatchVersion(fromVersion)));
Files.deleteIfExists(scriptPath); Files.deleteIfExists(scriptPath);
Files.deleteIfExists(newVersionPath); Files.deleteIfExists(newVersionPath);

View File

@@ -3,11 +3,9 @@ package com.commafeed.backend.service.internal;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedSubscriptionService;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Singleton; import jakarta.inject.Singleton;
@@ -18,9 +16,7 @@ import lombok.RequiredArgsConstructor;
public class PostLoginActivities { public class PostLoginActivities {
private final UserDAO userDAO; private final UserDAO userDAO;
private final FeedSubscriptionService feedSubscriptionService;
private final UnitOfWork unitOfWork; private final UnitOfWork unitOfWork;
private final CommaFeedConfiguration config;
public void executeFor(User user) { public void executeFor(User user) {
// only update lastLogin every once in a while in order to avoid invalidating the cache every time someone logs in // only update lastLogin every once in a while in order to avoid invalidating the cache every time someone logs in
@@ -28,19 +24,6 @@ public class PostLoginActivities {
Instant lastLogin = user.getLastLogin(); Instant lastLogin = user.getLastLogin();
if (lastLogin == null || ChronoUnit.MINUTES.between(lastLogin, now) >= 30) { if (lastLogin == null || ChronoUnit.MINUTES.between(lastLogin, now) >= 30) {
user.setLastLogin(now); user.setLastLogin(now);
boolean heavyLoad = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad());
if (heavyLoad) {
// the amount of feeds in the database that are up for refresh might be very large since we're in heavy load mode
// the feed refresh engine might not be able to catch up quickly enough
// put feeds from online users that are up for refresh at the top of the queue
feedSubscriptionService.refreshAllUpForRefresh(user);
}
// Post login activites are susceptible to run for any webservice call.
// We update the user in a new transaction to update the user immediately.
// If we didn't and the webservice call takes time, subsequent webservice calls would have to wait for the first call to
// finish even if they didn't use the same database tables, because they updated the user too.
unitOfWork.run(() -> userDAO.saveOrUpdate(user)); unitOfWork.run(() -> userDAO.saveOrUpdate(user));
} }
} }

View File

@@ -109,7 +109,6 @@ public class CategoryREST {
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@Parameter( @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, description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@Parameter(description = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,
@Parameter( @Parameter(
description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, 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) { @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
@@ -143,7 +142,7 @@ public class CategoryREST {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, excludedIds); removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag, null, null); offset, limit + 1, order, true, tag, null, null);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
@@ -151,7 +150,7 @@ public class CategoryREST {
} else if (STARRED.equals(id)) { } else if (STARRED.equals(id)) {
entries.setName("Starred"); entries.setName("Starred");
List<FeedEntryStatus> starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, !onlyIds); List<FeedEntryStatus> starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, true);
for (FeedEntryStatus status : starred) { for (FeedEntryStatus status : starred) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
} }
@@ -162,7 +161,7 @@ public class CategoryREST {
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories); List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(user, categories);
removeExcludedSubscriptions(subs, excludedIds); removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag, null, null); offset, limit + 1, order, true, tag, null, null);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
@@ -202,13 +201,11 @@ public class CategoryREST {
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@Parameter( @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, description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@Parameter(description = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,
@Parameter( @Parameter(
description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds, 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) { @Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
Response response = getCategoryEntries(user, id, readType, newerThan, offset, limit, order, keywords, onlyIds, Response response = getCategoryEntries(user, id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
excludedSubscriptionIds, tag);
if (response.getStatus() != Status.OK.getStatusCode()) { if (response.getStatus() != Status.OK.getStatusCode()) {
return response; return response;
} }

View File

@@ -143,10 +143,8 @@ public class FeedREST {
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
@Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(description = "ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(
@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) {
description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@Parameter(description = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds) {
Preconditions.checkNotNull(id); Preconditions.checkNotNull(id);
Preconditions.checkNotNull(readType); Preconditions.checkNotNull(readType);
@@ -174,7 +172,7 @@ public class FeedREST {
entries.setFeedLink(subscription.getFeed().getLink()); entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly,
entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null, null, null); entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled())); entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
@@ -209,12 +207,10 @@ public class FeedREST {
@Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @Parameter(description = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @Parameter(description = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @Parameter(description = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(
@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) {
description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@Parameter(description = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds) {
Response response = getFeedEntries(user, id, readType, newerThan, offset, limit, order, keywords, onlyIds); Response response = getFeedEntries(user, id, readType, newerThan, offset, limit, order, keywords);
if (response.getStatus() != Status.OK.getStatusCode()) { if (response.getStatus() != Status.OK.getStatusCode()) {
return response; return response;
} }

View File

@@ -210,7 +210,7 @@ public class FeverREST {
.map(Feed::getLastUpdated) .map(Feed::getLastUpdated)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.max(Comparator.naturalOrder()) .max(Comparator.naturalOrder())
.map(d -> d.getEpochSecond()) .map(Instant::getEpochSecond)
.orElse(0L); .orElse(0L);
} }
@@ -253,7 +253,7 @@ public class FeverREST {
private List<Long> buildUnreadItemIds(User user, List<FeedSubscription> subscriptions) { private List<Long> buildUnreadItemIds(User user, List<FeedSubscription> subscriptions) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0,
UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false, true, null, null, null); UNREAD_ITEM_IDS_BATCH_SIZE, ReadingOrder.desc, false, null, null, null);
return statuses.stream().map(s -> s.getEntry().getId()).toList(); return statuses.stream().map(s -> s.getEntry().getId()).toList();
} }
@@ -281,7 +281,7 @@ public class FeverREST {
private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) { private List<FeverItem> buildItems(User user, List<FeedSubscription> subscriptions, Long sinceId, Long maxId) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE, List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, false, null, null, 0, ITEMS_BATCH_SIZE,
ReadingOrder.desc, false, false, null, sinceId, maxId); ReadingOrder.desc, false, null, sinceId, maxId);
return statuses.stream().map(this::mapStatus).toList(); return statuses.stream().map(this::mapStatus).toList();
} }

View File

@@ -68,7 +68,7 @@ public class NextUnreadServlet extends HttpServlet {
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get()); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get());
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order, List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order,
true, false, null, null, null); true, null, null, null);
s = Iterables.getFirst(statuses, null); s = Iterables.getFirst(statuses, null);
} else { } else {
FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId)); FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId));
@@ -76,7 +76,7 @@ public class NextUnreadServlet extends HttpServlet {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user.get(), category); List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user.get(), category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children);
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0, List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0,
1, order, true, false, null, null, null); 1, order, true, null, null, null);
s = Iterables.getFirst(statuses, null); s = Iterables.getFirst(statuses, null);
} }
} }

View File

@@ -39,7 +39,7 @@ public class WebSocketSessions {
public void sendMessage(User user, String text) { public void sendMessage(User user, String text) {
Set<Session> userSessions = sessions.get(user.getId()); Set<Session> userSessions = sessions.get(user.getId());
if (userSessions != null && !userSessions.isEmpty()) { if (userSessions != null && !userSessions.isEmpty()) {
log.debug("sending '{}' to {} users via websocket", text, userSessions.size()); log.debug("sending '{}' to user {} via websocket ({} sessions)", text, user.getId(), userSessions.size());
for (Session userSession : userSessions) { for (Session userSession : userSessions) {
if (userSession.isOpen()) { if (userSession.isOpen()) {
userSession.getAsyncRemote().sendText(text); userSession.getAsyncRemote().sendText(text);

View File

@@ -1,23 +1,100 @@
package com.commafeed; package com.commafeed;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; 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 javax.sql.DataSource;
import org.mockserver.socket.PortFactory; 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.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration.CacheType;
import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.ConfigOverride;
import io.dropwizard.testing.ResourceHelpers; import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit5.DropwizardAppExtension; import io.dropwizard.testing.junit5.DropwizardAppExtension;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension<CommaFeedConfiguration> { public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension<CommaFeedConfiguration> {
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<String> DROP_ALL_STATEMENTS;
static {
List<ConfigOverride> 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() { public CommaFeedDropwizardAppExtension() {
super(CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"), super(CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"), CONFIG_OVERRIDES);
ConfigOverride.config("server.applicationConnectors[0].port", String.valueOf(PortFactory.findFreePort()))); }
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 @Override
@@ -27,10 +104,22 @@ public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension<Comm
// clean database after each test // clean database after each test
DataSource dataSource = getConfiguration().getDataSourceFactory().build(new MetricRegistry(), "cleanup"); DataSource dataSource = getConfiguration().getDataSourceFactory().build(new MetricRegistry(), "cleanup");
try (Connection connection = dataSource.getConnection()) { try (Connection connection = dataSource.getConnection()) {
connection.prepareStatement("DROP ALL OBJECTS").executeUpdate(); for (String statement : DROP_ALL_STATEMENTS) {
connection.prepareStatement(statement).executeUpdate();
}
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException("could not cleanup database", 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<String> dropAllStatements) {
} }
} }

View File

@@ -1,90 +0,0 @@
package com.commafeed.backend;
import java.util.Comparator;
import org.apache.commons.lang3.ObjectUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class FixedSizeSortedListTest {
private static final Comparator<String> COMP = ObjectUtils::compare;
private FixedSizeSortedList<String> list;
@BeforeEach
public void init() {
list = new FixedSizeSortedList<>(3, COMP);
}
@Test
void testSimpleAdd() {
list.add("0");
list.add("1");
list.add("2");
Assertions.assertEquals("0", list.asList().get(0));
Assertions.assertEquals("1", list.asList().get(1));
Assertions.assertEquals("2", list.asList().get(2));
}
@Test
void testIsFull() {
list.add("0");
list.add("1");
Assertions.assertFalse(list.isFull());
list.add("2");
Assertions.assertTrue(list.isFull());
}
@Test
void testOrder() {
list.add("2");
list.add("1");
list.add("0");
Assertions.assertEquals("0", list.asList().get(0));
Assertions.assertEquals("1", list.asList().get(1));
Assertions.assertEquals("2", list.asList().get(2));
}
@Test
void testEviction() {
list.add("7");
list.add("8");
list.add("9");
list.add("0");
list.add("1");
list.add("2");
Assertions.assertEquals("0", list.asList().get(0));
Assertions.assertEquals("1", list.asList().get(1));
Assertions.assertEquals("2", list.asList().get(2));
}
@Test
void testCapacity() {
list.add("0");
list.add("1");
list.add("2");
list.add("3");
Assertions.assertEquals(3, list.asList().size());
}
@Test
void testLast() {
list.add("0");
list.add("1");
list.add("2");
Assertions.assertEquals("2", list.last());
list.add("3");
Assertions.assertEquals("2", list.last());
}
}

View File

@@ -54,6 +54,7 @@ public abstract class PlaywrightTestBase {
} }
protected void customizeNewContextOptions(NewContextOptions options) { protected void customizeNewContextOptions(NewContextOptions options) {
// override in subclasses to customize the browser context
} }
protected static class SaveArtifactsOnTestFailed implements TestWatcher, BeforeEachCallback { protected static class SaveArtifactsOnTestFailed implements TestWatcher, BeforeEachCallback {

View File

@@ -10,7 +10,6 @@ import org.apache.commons.io.IOUtils;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
@@ -40,7 +39,12 @@ public abstract class BaseIT {
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/"); private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
private final CommaFeedDropwizardAppExtension extension = buildExtension(); private final CommaFeedDropwizardAppExtension extension = new CommaFeedDropwizardAppExtension() {
@Override
protected JerseyClientBuilder clientBuilder() {
return configureClientBuilder(super.clientBuilder().register(MultiPartFeature.class));
}
};
private Client client; private Client client;
@@ -54,13 +58,8 @@ public abstract class BaseIT {
private MockServerClient mockServerClient; private MockServerClient mockServerClient;
protected CommaFeedDropwizardAppExtension buildExtension() { protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return new CommaFeedDropwizardAppExtension() { return base;
@Override
protected JerseyClientBuilder clientBuilder() {
return super.clientBuilder().register(HttpAuthenticationFeature.basic("admin", "admin")).register(MultiPartFeature.class);
}
};
} }
@BeforeEach @BeforeEach

View File

@@ -6,7 +6,6 @@ import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import com.commafeed.CommaFeedDropwizardAppExtension;
import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.ProfileModificationRequest; import com.commafeed.frontend.model.request.ProfileModificationRequest;
@@ -18,12 +17,6 @@ import jakarta.ws.rs.core.Response;
class SecurityIT extends BaseIT { class SecurityIT extends BaseIT {
@Override
protected CommaFeedDropwizardAppExtension buildExtension() {
// override so we don't add http basic auth
return new CommaFeedDropwizardAppExtension();
}
@Test @Test
void notLoggedIn() { void notLoggedIn() {
try (Response response = getClient().target(getApiBaseUrl() + "user/profile").request().get()) { try (Response response = getClient().target(getApiBaseUrl() + "user/profile").request().get()) {

View File

@@ -10,6 +10,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.awaitility.Awaitility; 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.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -28,6 +30,11 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
class WebSocketIT extends BaseIT { class WebSocketIT extends BaseIT {
@Override
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
@Test @Test
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException { void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
AtomicBoolean connected = new AtomicBoolean(); AtomicBoolean connected = new AtomicBoolean();

View File

@@ -3,6 +3,8 @@ package com.commafeed.integration.rest;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -17,6 +19,11 @@ import jakarta.ws.rs.client.Entity;
class AdminIT extends BaseIT { class AdminIT extends BaseIT {
@Override
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
@Test @Test
void getApplicationSettings() { void getApplicationSettings() {
ApplicationSettings settings = getClient().target(getApiBaseUrl() + "admin/settings").request().get(ApplicationSettings.class); ApplicationSettings settings = getClient().target(getApiBaseUrl() + "admin/settings").request().get(ApplicationSettings.class);

View File

@@ -13,6 +13,8 @@ import org.apache.commons.lang3.StringUtils;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.glassfish.jersey.client.ClientProperties; 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.MultiPart;
import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
@@ -34,6 +36,11 @@ import jakarta.ws.rs.core.Response;
class FeedIT extends BaseIT { class FeedIT extends BaseIT {
@Override
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
@Nested @Nested
class Fetch { class Fetch {
@Test @Test
@@ -105,10 +112,11 @@ class FeedIT extends BaseIT {
@Test @Test
void markInsertedBeforeBeforeSubscription() { void markInsertedBeforeBeforeSubscription() {
Instant insertedBefore = Instant.now(); // mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, null, insertedBefore); markFeedEntries(subscriptionId, null, threshold);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().noneMatch(Entry::isRead)); Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().noneMatch(Entry::isRead));
} }
@@ -116,9 +124,10 @@ class FeedIT extends BaseIT {
void markInsertedBeforeAfterSubscription() { void markInsertedBeforeAfterSubscription() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Instant insertedBefore = Instant.now(); // mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().plus(Duration.ofSeconds(1));
markFeedEntries(subscriptionId, null, insertedBefore); markFeedEntries(subscriptionId, null, threshold);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead)); Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
} }
@@ -137,26 +146,29 @@ class FeedIT extends BaseIT {
void refresh() { void refresh() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Instant now = Instant.now(); // mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
IDRequest request = new IDRequest(); IDRequest request = new IDRequest();
request.setId(subscriptionId); request.setId(subscriptionId);
getClient().target(getApiBaseUrl() + "feed/refresh").request().post(Entity.json(request), Void.TYPE); getClient().target(getApiBaseUrl() + "feed/refresh").request().post(Entity.json(request), Void.TYPE);
Awaitility.await() Awaitility.await()
.atMost(Duration.ofSeconds(15)) .atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(now)); .until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
} }
@Test @Test
void refreshAll() { void refreshAll() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl()); Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Instant now = Instant.now(); // mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
getClient().target(getApiBaseUrl() + "feed/refreshAll").request().get(Void.TYPE); getClient().target(getApiBaseUrl() + "feed/refreshAll").request().get(Void.TYPE);
Awaitility.await() Awaitility.await()
.atMost(Duration.ofSeconds(15)) .atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(now)); .until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
} }
} }

View File

@@ -1,5 +1,7 @@
package com.commafeed.integration.rest; package com.commafeed.integration.rest;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -18,6 +20,11 @@ class FeverIT extends BaseIT {
private Long userId; private Long userId;
private String apiKey; private String apiKey;
@Override
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
@BeforeEach @BeforeEach
void init() { void init() {
// create api key // create api key

View File

@@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.ServerInfo; import com.commafeed.frontend.model.ServerInfo;
import com.commafeed.integration.BaseIT; import com.commafeed.integration.BaseIT;
public class ServerIT extends BaseIT { class ServerIT extends BaseIT {
@Test @Test
void getServerInfos() { void getServerInfos() {

View File

@@ -1,5 +1,7 @@
package com.commafeed.integration.servlet; package com.commafeed.integration.servlet;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -12,6 +14,11 @@ import jakarta.ws.rs.core.Response;
class CustomCodeIT extends BaseIT { class CustomCodeIT extends BaseIT {
@Override
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
@Test @Test
void test() { void test() {
// get settings // get settings

View File

@@ -5,7 +5,6 @@ import org.glassfish.jersey.client.ClientProperties;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import com.commafeed.CommaFeedDropwizardAppExtension;
import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.UserModel;
import com.commafeed.integration.BaseIT; import com.commafeed.integration.BaseIT;
@@ -16,12 +15,6 @@ import jakarta.ws.rs.core.Response;
class LogoutIT extends BaseIT { class LogoutIT extends BaseIT {
@Override
protected CommaFeedDropwizardAppExtension buildExtension() {
// override so we don't add http basic auth
return new CommaFeedDropwizardAppExtension();
}
@Test @Test
void test() { void test() {
String cookie = login(); String cookie = login();

View File

@@ -2,6 +2,8 @@ package com.commafeed.integration.servlet;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.glassfish.jersey.client.ClientProperties; 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.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -12,6 +14,11 @@ import jakarta.ws.rs.core.Response;
class NextUnreadIT extends BaseIT { class NextUnreadIT extends BaseIT {
@Override
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
@Test @Test
void test() { void test() {
subscribeAndWaitForEntries(getFeedUrl()); subscribeAndWaitForEntries(getFeedUrl());

View File

@@ -104,10 +104,6 @@ app:
# for PostgreSQL # for PostgreSQL
# driverClass is org.postgresql.Driver # driverClass is org.postgresql.Driver
# url is jdbc:postgresql://localhost:5432/commafeed # url is jdbc:postgresql://localhost:5432/commafeed
#
# for Microsoft SQL Server
# driverClass is net.sourceforge.jtds.jdbc.Driver
# url is jdbc:jtds:sqlserver://localhost:1433/commafeed;instance=<instanceName, remove if not needed>
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver
@@ -117,6 +113,9 @@ database:
properties: properties:
charSet: UTF-8 charSet: UTF-8
validationQuery: "/* CommaFeed Health Check */ SELECT 1" validationQuery: "/* CommaFeed Health Check */ SELECT 1"
minSize: 1
maxSize: 5
initialSize: 1
server: server:
applicationConnectors: applicationConnectors:

View File

@@ -0,0 +1,4 @@
postgresql=postgres:${postgresql.image.version}
mysql=mysql:${mysql.image.version}
mariadb=mariadb:${mariadb.image.version}
redis=redis:${redis.image.version}

13
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>4.4.1</version> <version>4.5.0</version>
<name>CommaFeed</name> <name>CommaFeed</name>
<packaging>pom</packaging> <packaging>pom</packaging>
@@ -22,6 +22,17 @@
<version>3.13.0</version> <version>3.13.0</version>
<configuration> <configuration>
<parameters>true</parameters> <parameters>true</parameters>
<!-- treat warnings as errors -->
<!-- https://stackoverflow.com/a/33823355/ -->
<showWarnings>true</showWarnings>
<compilerArgs>
<!-- disable the "processing" linter because we have annotations that are processed at runtime -->
<!-- https://stackoverflow.com/a/76126981/ -->
<!-- disable the "classfile" linter because it generates "file missing" warnings about annotations with the "provided" scope -->
<arg>-Xlint:all,-processing,-classfile</arg>
<arg>-Werror</arg>
</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>

View File

@@ -2,7 +2,12 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"config:recommended", "config:recommended",
"customManagers:biomeVersions" "customManagers:mavenPropertyVersions",
"customManagers:biomeVersions",
":automergePatch",
":automergeBranch",
":automergeRequireAllStatusChecks",
":maintainLockFilesWeekly"
], ],
"packageRules": [ "packageRules": [
{ {