forked from Archives/Athou_commafeed
@@ -1,7 +1 @@
|
||||
# ignore everything
|
||||
*
|
||||
|
||||
# allow only what we need
|
||||
!commafeed-server/target/commafeed.jar
|
||||
!commafeed-server/config.yml.example
|
||||
|
||||
commafeed-client
|
||||
121
.github/workflows/build.yml
vendored
121
.github/workflows/build.yml
vendored
@@ -1,121 +0,0 @@
|
||||
name: Java CI
|
||||
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ "17", "21" ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: "temurin"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install
|
||||
env:
|
||||
TEST_DATABASE: h2
|
||||
|
||||
- name: Run integration tests on PostgreSQL
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: postgresql
|
||||
|
||||
- name: Run integration tests on MySQL
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: mysql
|
||||
|
||||
- name: Run integration tests on MariaDB
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: mariadb
|
||||
|
||||
- name: Run integration tests with Redis cache enabled
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: h2
|
||||
REDIS: true
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.java == '17' }}
|
||||
with:
|
||||
name: commafeed.jar
|
||||
path: commafeed-server/target/commafeed.jar
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-artifacts
|
||||
path: |
|
||||
**/target/playwright-artifacts/
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker build and push tag
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest
|
||||
athou/commafeed:${{ github.ref_name }}
|
||||
|
||||
- name: Docker build and push master
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:master
|
||||
|
||||
# Create GitHub release after Docker image has been published
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
commafeed-server/target/commafeed.jar
|
||||
commafeed-server/config.yml.example
|
||||
197
.github/workflows/ci.yml
vendored
Normal file
197
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
name: ci
|
||||
|
||||
on: [ push ]
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 21
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }}
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload cross-platform app
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-jvm
|
||||
path: commafeed-server/target/commafeed-*.zip
|
||||
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' || github.ref_name == 'quarkus' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
## tags
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}-jvm
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
|
||||
|
||||
## master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:master-${{ matrix.database }}-jvm
|
||||
|
||||
## quarkus branch - remove when merged into master, also remove the condition in the login step
|
||||
- name: Docker build and push quarkus - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'quarkus' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: athou/commafeed:quarkus-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push quarkus - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'quarkus' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:quarkus-${{ matrix.database }}-jvm
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Configure git to checkout as-is
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.database != 'h2' }}
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner.exe
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-linux
|
||||
- build-windows
|
||||
if: github.ref_type == 'tag'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: commafeed-*
|
||||
path: ./artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: ./artifacts/*
|
||||
18
.github/workflows/dockerhub.yml
vendored
Normal file
18
.github/workflows/dockerhub.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Update Docker Hub Description
|
||||
|
||||
on: release
|
||||
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: athou/commafeed
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
readme-filepath: commafeed-server/src/main/docker/README.md
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## [5.0.0]
|
||||
|
||||
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
|
||||
the [announcement](https://github.com/Athou/commafeed/discussions/1517).
|
||||
The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
|
||||
0.3s) and very low memory footprint (< 50M).
|
||||
|
||||
- CommaFeed now has a different package for each supported database.
|
||||
- If you are deploying CommaFeed with a precompiled package, please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package).
|
||||
- If you are building CommaFeed from sources, please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources).
|
||||
- If you are using the Docker image, please read the instructions on
|
||||
the [Docker Hub page](https://hub.docker.com/r/athou/commafeed).
|
||||
- Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone).
|
||||
Please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration).
|
||||
Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature.
|
||||
- Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB.
|
||||
- Use a different icon for filtering unread entries and marking an entry as read (#1506)
|
||||
- Added various HTML attributes to ease custom JS/CSS customization (#1507)
|
||||
- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer
|
||||
needed, even for instances with a large number of feeds.
|
||||
- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using
|
||||
the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0.
|
||||
|
||||
## [4.6.0]
|
||||
|
||||
- switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50%
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,19 +0,0 @@
|
||||
FROM ibm-semeru-runtimes:open-21.0.4_7-jre
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/config.yml.example config.yml
|
||||
COPY commafeed-server/target/commafeed.jar .
|
||||
|
||||
CMD ["java", \
|
||||
"-Djava.net.preferIPv4Stack=true", \
|
||||
"-Xtune:virtualized", \
|
||||
"-Xminf0.05", \
|
||||
"-Xmaxf0.1", \
|
||||
"-jar", \
|
||||
"commafeed.jar", \
|
||||
"server", \
|
||||
"config.yml"]
|
||||
93
README.md
93
README.md
@@ -1,6 +1,6 @@
|
||||
# CommaFeed
|
||||
|
||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
|
||||
Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript.
|
||||
|
||||

|
||||
|
||||
@@ -8,14 +8,22 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
|
||||
|
||||
- 4 different layouts
|
||||
- Light/Dark theme
|
||||
- Fully responsive
|
||||
- Fully responsive, works great on both mobile and desktop
|
||||
- Keyboard shortcuts for almost everything
|
||||
- Support for right-to-left feeds
|
||||
- Translated in 25+ languages
|
||||
- Supports thousands of users and millions of feeds
|
||||
- OPML import/export
|
||||
- REST API and a Fever-compatible API for native mobile apps
|
||||
- REST API
|
||||
- Fever-compatible API for native mobile apps
|
||||
- Can automatically mark articles as read based on user-defined rules
|
||||
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
||||
- Compiles to native code for blazing fast startup and low memory usage
|
||||
- Supports 4 databases
|
||||
- H2 (embedded database)
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- MariaDB
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -33,28 +41,75 @@ PikaPods shares 20% of the revenue back to CommaFeed.
|
||||
|
||||
[](https://www.pikapods.com/pods?run=commafeed)
|
||||
|
||||
### Download precompiled package
|
||||
### Download a precompiled package
|
||||
|
||||
mkdir commafeed && cd commafeed
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
||||
Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating
|
||||
system and database of choice.
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
There are two types of packages:
|
||||
|
||||
- The `linux-x86_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run
|
||||
directly.
|
||||
- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all
|
||||
platforms and is started with `java -jar quarkus-run.jar`.
|
||||
|
||||
If available for your operating system, the native package is recommended because it has a faster startup time and lower
|
||||
memory usage.
|
||||
|
||||
### Build from sources
|
||||
|
||||
git clone https://github.com/Athou/commafeed.git
|
||||
cd commafeed
|
||||
./mvnw clean package
|
||||
cp commafeed-server/config.yml.example config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
||||
./mvnw clean package [-P<database>] [-Pnative] [-DskipTests]
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
- `<database>` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`.
|
||||
- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment
|
||||
variable pointing to a GraalVM installation).
|
||||
- `-DskipTests` to speed up the build process by skipping tests.
|
||||
|
||||
### Memory management
|
||||
When the build is complete:
|
||||
|
||||
- a zip containing all jars required to run the application is located at
|
||||
`commafeed-server/target/commafeed-<version>-<database>-jvm.zip`. Extract it and run the application with
|
||||
`java -jar quarkus-run.jar`
|
||||
- if you used the native profile, the executable is located at
|
||||
`commafeed-server/target/commafeed-<version>-<database>-<platform>-<arch>-runner[.exe]`
|
||||
|
||||
## Configuration
|
||||
|
||||
CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in
|
||||
the `data` directory of the current directory.
|
||||
|
||||
To use a different database, you will need to configure the following properties:
|
||||
|
||||
- `quarkus.datasource.jdbc-url`
|
||||
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
|
||||
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
|
||||
- e.g. for MySQL:
|
||||
`jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- e.g. for MariaDB:
|
||||
`jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- `quarkus.datasource.username`
|
||||
- `quarkus.datasource.password`
|
||||
|
||||
There are multiple ways to configure CommaFeed:
|
||||
|
||||
- a [properties](https://en.wikipedia.org/wiki/.properties) file in `config/application.properties` (keys in kebab-case)
|
||||
- Command line arguments prefixed with `-D` (keys in kebab-case)
|
||||
- Environment variables (keys in UPPER_CASE)
|
||||
- an .env file in the working directory (keys in UPPER_CASE)
|
||||
|
||||
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
|
||||
|
||||
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
|
||||
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
|
||||
|
||||
All [CommaFeed settings](commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java)
|
||||
are optional and have sensible default values.
|
||||
|
||||
When started, the server will listen on http://localhost:8082.
|
||||
The default user is `admin` and the default password is `admin`.
|
||||
|
||||
### Memory management (`jvm` package only)
|
||||
|
||||
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
|
||||
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
|
||||
@@ -108,7 +163,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
|
||||
|
||||
- Open `commafeed-server` in your preferred Java IDE.
|
||||
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
||||
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
||||
- run `./mvnw quarkus:dev`
|
||||
|
||||
### Frontend
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.6.0</version>
|
||||
<version>5.0.0-beta</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
@@ -80,7 +80,7 @@
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory>
|
||||
<outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>dist</directory>
|
||||
|
||||
@@ -81,7 +81,17 @@ export const client = {
|
||||
},
|
||||
},
|
||||
user: {
|
||||
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
|
||||
login: async (req: LoginRequest) => {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append("j_username", req.name)
|
||||
formData.append("j_password", req.password)
|
||||
return await axiosInstance.post("j_security_check", formData, {
|
||||
baseURL: ".",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
},
|
||||
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
|
||||
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
|
||||
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { Accordion, Box, Tabs } from "@mantine/core"
|
||||
import { Accordion, Box } from "@mantine/core"
|
||||
import { client } from "app/client"
|
||||
import { Loader } from "components/Loader"
|
||||
import { Gauge } from "components/metrics/Gauge"
|
||||
import { Meter } from "components/metrics/Meter"
|
||||
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
|
||||
import { Timer } from "components/metrics/Timer"
|
||||
import { useEffect } from "react"
|
||||
import { useAsync } from "react-async-hook"
|
||||
import { TbChartAreaLine, TbClock } from "react-icons/tb"
|
||||
|
||||
const shownMeters: Record<string, string> = {
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryInserted": "Entries inserted",
|
||||
"com.commafeed.backend.service.db.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
}
|
||||
|
||||
@@ -42,46 +39,25 @@ export function MetricsPage() {
|
||||
}, [query.execute])
|
||||
|
||||
if (!query.result) return <Loader />
|
||||
const { meters, gauges, timers } = query.result.data
|
||||
const { meters, gauges } = query.result.data
|
||||
return (
|
||||
<Tabs defaultValue="stats">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="stats" leftSection={<TbChartAreaLine size={14} />}>
|
||||
Stats
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="timers" leftSection={<TbClock size={14} />}>
|
||||
Timers
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<>
|
||||
<Accordion variant="contained" chevronPosition="left">
|
||||
{Object.keys(shownMeters).map(m => (
|
||||
<MetricAccordionItem key={m} metricKey={m} name={shownMeters[m]} headerValue={meters[m].count}>
|
||||
<Meter meter={meters[m]} />
|
||||
</MetricAccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<Tabs.Panel value="stats" pt="xs">
|
||||
<Accordion variant="contained" chevronPosition="left">
|
||||
{Object.keys(shownMeters).map(m => (
|
||||
<MetricAccordionItem key={m} metricKey={m} name={shownMeters[m]} headerValue={meters[m].count}>
|
||||
<Meter meter={meters[m]} />
|
||||
</MetricAccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<Box pt="xs">
|
||||
{Object.keys(shownGauges).map(g => (
|
||||
<Box key={g}>
|
||||
<span>{shownGauges[g]}: </span>
|
||||
<Gauge gauge={gauges[g]} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="timers" pt="xs">
|
||||
<Accordion variant="contained" chevronPosition="left">
|
||||
{Object.keys(timers).map(key => (
|
||||
<MetricAccordionItem key={key} metricKey={key} name={key} headerValue={timers[key].count}>
|
||||
<Timer timer={timers[key]} />
|
||||
</MetricAccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Box pt="xs">
|
||||
{Object.keys(shownGauges).map(g => (
|
||||
<Box key={g}>
|
||||
<span>{shownGauges[g]}: </span>
|
||||
<Gauge gauge={gauges[g]} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,12 +24,18 @@ export function RegistrationPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const register = useAsyncCallback(client.user.register, {
|
||||
const login = useAsyncCallback(client.user.login, {
|
||||
onSuccess: () => {
|
||||
dispatch(redirectToRootCategory())
|
||||
},
|
||||
})
|
||||
|
||||
const register = useAsyncCallback(client.user.register, {
|
||||
onSuccess: () => {
|
||||
login.execute(form.values)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Container size="xs">
|
||||
<PageTitle />
|
||||
@@ -50,6 +56,12 @@ export function RegistrationPage() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{login.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(login.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(register.execute)}>
|
||||
<Stack>
|
||||
<TextInput label="User Name" placeholder="User Name" {...form.getInputProps("name")} size="md" required />
|
||||
@@ -68,7 +80,7 @@ export function RegistrationPage() {
|
||||
size="md"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" loading={register.loading}>
|
||||
<Button type="submit" loading={register.loading || login.loading}>
|
||||
<Trans>Sign up</Trans>
|
||||
</Button>
|
||||
<Center>
|
||||
|
||||
@@ -32,6 +32,7 @@ export default defineConfig(env => ({
|
||||
"/openapi.json": "http://localhost:8083",
|
||||
"/custom_css.css": "http://localhost:8083",
|
||||
"/custom_js.js": "http://localhost:8083",
|
||||
"/j_security_check": "http://localhost:8083",
|
||||
"/logout": "http://localhost:8083",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
# CommaFeed settings
|
||||
# ------------------
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8082/
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: true
|
||||
|
||||
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
|
||||
strictPasswordPolicy: true
|
||||
|
||||
# create a demo account the first time the app starts
|
||||
createDemoAccount: true
|
||||
|
||||
# put your google analytics tracking code here
|
||||
googleAnalyticsTrackingCode:
|
||||
|
||||
# put your google server key (used for youtube favicon fetching)
|
||||
googleAuthKey:
|
||||
|
||||
# number of http threads
|
||||
backgroundThreads: 3
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost: localhost
|
||||
smtpPort: 25
|
||||
smtpTls: false
|
||||
smtpUserName: user
|
||||
smtpPassword: pass
|
||||
smtpFromAddress:
|
||||
|
||||
# Graphite Metric settings
|
||||
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
|
||||
graphiteEnabled: false
|
||||
graphitePrefix: "test.commafeed"
|
||||
graphiteHost: "localhost"
|
||||
graphitePort: 2003
|
||||
graphiteInterval: 60
|
||||
|
||||
# whether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
heavyLoad: false
|
||||
|
||||
# minimum amount of time commafeed will wait before refreshing the same feed
|
||||
refreshIntervalMinutes: 5
|
||||
|
||||
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
imageProxyEnabled: true
|
||||
|
||||
# database query timeout (in milliseconds), 0 to disable
|
||||
queryTimeout: 0
|
||||
|
||||
# time to keep unread statuses (in days), 0 to disable
|
||||
keepStatusDays: 0
|
||||
|
||||
# entries to keep per feed, old entries will be deleted, 0 to disable
|
||||
maxFeedCapacity: 500
|
||||
|
||||
# entries older than this will be deleted, 0 to disable
|
||||
maxEntriesAgeDays: 365
|
||||
|
||||
# limit the number of feeds a user can subscribe to, 0 to disable
|
||||
maxFeedsPerUser: 0
|
||||
|
||||
# don't parse feeds that are too large to prevent memory issues
|
||||
maxFeedResponseSize: 5M
|
||||
|
||||
# cache service to use, possible values are 'noop' and 'redis'
|
||||
cache: noop
|
||||
|
||||
# announcement string displayed on the main page
|
||||
announcement:
|
||||
|
||||
# user-agent string that will be used by the http client, leave empty for the default one
|
||||
userAgent:
|
||||
|
||||
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
|
||||
websocketEnabled: true
|
||||
|
||||
# interval at which the client will send a ping message on the websocket to keep the connection alive
|
||||
websocketPingInterval: 15m
|
||||
|
||||
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
|
||||
treeReloadInterval: 30s
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||
|
||||
database:
|
||||
driverClass: org.h2.Driver
|
||||
url: jdbc:h2:./target/commafeed
|
||||
user: sa
|
||||
password: sa
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||
|
||||
server:
|
||||
applicationConnectors:
|
||||
- type: http
|
||||
port: 8083
|
||||
adminConnectors:
|
||||
- type: http
|
||||
port: 8084
|
||||
|
||||
logging:
|
||||
level: INFO
|
||||
loggers:
|
||||
com.commafeed: DEBUG
|
||||
liquibase: INFO
|
||||
org.hibernate.SQL: INFO # or ALL for sql debugging
|
||||
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
currentLogFilename: log/commafeed.log
|
||||
threshold: ALL
|
||||
archive: true
|
||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||
archivedFileCount: 5
|
||||
timeZone: UTC
|
||||
|
||||
# Redis pool configuration
|
||||
# (only used if app.cache is 'redis')
|
||||
# -----------------------------------
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# username is only required when using ACLs
|
||||
username:
|
||||
password:
|
||||
timeout: 2000
|
||||
database: 0
|
||||
maxTotal: 500
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
# CommaFeed settings
|
||||
# ------------------
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8082/
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: false
|
||||
|
||||
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
|
||||
strictPasswordPolicy: true
|
||||
|
||||
# create a demo account the first time the app starts
|
||||
createDemoAccount: false
|
||||
|
||||
# put your google analytics tracking code here
|
||||
googleAnalyticsTrackingCode:
|
||||
|
||||
# put your google server key (used for youtube favicon fetching)
|
||||
googleAuthKey:
|
||||
|
||||
# number of http threads
|
||||
backgroundThreads: 3
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost:
|
||||
smtpPort:
|
||||
smtpTls: false
|
||||
smtpUserName:
|
||||
smtpPassword:
|
||||
smtpFromAddress:
|
||||
|
||||
# Graphite Metric settings
|
||||
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
|
||||
graphiteEnabled: false
|
||||
graphitePrefix: "test.commafeed"
|
||||
graphiteHost: "localhost"
|
||||
graphitePort: 2003
|
||||
graphiteInterval: 60
|
||||
|
||||
# whether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
heavyLoad: false
|
||||
|
||||
# minimum amount of time commafeed will wait before refreshing the same feed
|
||||
refreshIntervalMinutes: 5
|
||||
|
||||
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
imageProxyEnabled: false
|
||||
|
||||
# database query timeout (in milliseconds), 0 to disable
|
||||
queryTimeout: 0
|
||||
|
||||
# time to keep unread statuses (in days), 0 to disable
|
||||
keepStatusDays: 0
|
||||
|
||||
# entries to keep per feed, old entries will be deleted, 0 to disable
|
||||
maxFeedCapacity: 500
|
||||
|
||||
# entries older than this will be deleted, 0 to disable
|
||||
maxEntriesAgeDays: 365
|
||||
|
||||
# limit the number of feeds a user can subscribe to, 0 to disable
|
||||
maxFeedsPerUser: 0
|
||||
|
||||
# don't parse feeds that are too large to prevent memory issues
|
||||
maxFeedResponseSize: 5M
|
||||
|
||||
# cache service to use, possible values are 'noop' and 'redis'
|
||||
cache: noop
|
||||
|
||||
# announcement string displayed on the main page
|
||||
announcement:
|
||||
|
||||
# user-agent string that will be used by the http client, leave empty for the default one
|
||||
userAgent:
|
||||
|
||||
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
|
||||
websocketEnabled: true
|
||||
|
||||
# interval at which the client will send a ping message on the websocket to keep the connection alive
|
||||
websocketPingInterval: 15m
|
||||
|
||||
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
|
||||
treeReloadInterval: 30s
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||
|
||||
database:
|
||||
driverClass: org.h2.Driver
|
||||
url: jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE
|
||||
user: sa
|
||||
password: sa
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||
minSize: 1
|
||||
maxSize: 50
|
||||
maxConnectionAge: 30m
|
||||
|
||||
server:
|
||||
applicationConnectors:
|
||||
- type: http
|
||||
port: 8082
|
||||
adminConnectors: [ ]
|
||||
requestLog:
|
||||
appenders: [ ]
|
||||
|
||||
logging:
|
||||
level: ERROR
|
||||
loggers:
|
||||
com.commafeed: INFO
|
||||
liquibase: INFO
|
||||
io.dropwizard.server.ServerFactory: INFO
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
currentLogFilename: log/commafeed.log
|
||||
threshold: ALL
|
||||
archive: true
|
||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||
archivedFileCount: 5
|
||||
timeZone: UTC
|
||||
|
||||
# Redis pool configuration
|
||||
# (only used if app.cache is 'redis')
|
||||
# -----------------------------------
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# username is only required when using ACLs
|
||||
username:
|
||||
password:
|
||||
timeout: 2000
|
||||
database: 0
|
||||
maxTotal: 500
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
mysql:
|
||||
image: mariadb
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_ROOT_PASSWORD=commafeed
|
||||
- MYSQL_DATABASE=commafeed
|
||||
ports:
|
||||
- "3306:3306"
|
||||
@@ -12,8 +12,8 @@ services:
|
||||
postgresql:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=root
|
||||
- POSTGRES_PASSWORD=root
|
||||
- POSTGRES_USER=commafeed
|
||||
- POSTGRES_PASSWORD=commafeed
|
||||
- POSTGRES_DB=commafeed
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
@@ -6,34 +6,26 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.6.0</version>
|
||||
<version>5.0.0-beta</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
|
||||
<properties>
|
||||
<guice.version>7.0.0</guice.version>
|
||||
<quarkus.version>3.13.1</quarkus.version>
|
||||
<querydsl.version>6.6</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
<bouncycastle.version>1.78.1</bouncycastle.version>
|
||||
<properties-plugin.version>1.2.1</properties-plugin.version>
|
||||
|
||||
<testcontainers.version>1.20.1</testcontainers.version>
|
||||
<!-- renovate: datasource=docker depName=postgres -->
|
||||
<postgresql.image.version>16.4</postgresql.image.version>
|
||||
<!-- renovate: datasource=docker depName=mysql -->
|
||||
<mysql.image.version>9.0.1</mysql.image.version>
|
||||
<!-- renovate: datasource=docker depName=mariadb -->
|
||||
<mariadb.image.version>11.4.3</mariadb.image.version>
|
||||
<!-- renovate: datasource=docker depName=redis -->
|
||||
<redis.image.version>7.4.0</redis.image.version>
|
||||
<quarkus.datasource.db-kind>h2</quarkus.datasource.db-kind>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-dependencies</artifactId>
|
||||
<version>4.0.7</version>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-bom</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -41,26 +33,80 @@
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<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>
|
||||
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-help-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>active-profiles</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
<goal>native-image-agent</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<properties>
|
||||
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
|
||||
<quarkus.package.runner-suffix>
|
||||
-${quarkus.datasource.db-kind}-${os.detected.name}-${os.detected.arch}-runner
|
||||
</quarkus.package.runner-suffix>
|
||||
</properties>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>commafeed-${project.version}-${quarkus.datasource.db-kind}-jvm</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<descriptors>
|
||||
<descriptor>src/main/assembly/zip-quarkus-app.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
@@ -74,6 +120,13 @@
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
|
||||
</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.github.git-commit-id</groupId>
|
||||
@@ -94,63 +147,13 @@
|
||||
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.shade</groupId>
|
||||
<artifactId>maven-shade-ext-transformers</artifactId>
|
||||
<version>1.4.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>module-info.class</exclude>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.commafeed.CommaFeedApplication</mainClass>
|
||||
</transformer>
|
||||
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
|
||||
<paths>
|
||||
<path>rome.properties</path>
|
||||
</paths>
|
||||
<mergeStrategy>append</mergeStrategy>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-maven-plugin-jakarta</artifactId>
|
||||
<version>2.2.22</version>
|
||||
<?m2e ignore?>
|
||||
<configuration>
|
||||
<outputPath>${project.build.directory}/classes/assets</outputPath>
|
||||
<outputPath>${project.build.directory}/classes/META-INF/resources</outputPath>
|
||||
<outputFormat>JSONANDYAML</outputFormat>
|
||||
<resourcePackages>
|
||||
<package>com.commafeed.frontend.resource</package>
|
||||
@@ -168,18 +171,6 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
@@ -237,7 +228,7 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>4.6.0</version>
|
||||
<version>5.0.0-beta</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -247,60 +238,69 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<groupId>org.kohsuke.metainf-services</groupId>
|
||||
<artifactId>metainf-services</artifactId>
|
||||
<version>1.11</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>${guice.version}</version>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-mailer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-liquibase</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mariadb</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-unix-socket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-hibernate</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-migrations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-assets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-forms</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-graphite</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.whitfin</groupId>
|
||||
<artifactId>dropwizard-environment-substitutor</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jakarta-server</artifactId>
|
||||
<version>4.2.26</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -349,17 +349,6 @@
|
||||
<version>1.6.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>5.1.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.rometools</groupId>
|
||||
<artifactId>rome</artifactId>
|
||||
@@ -409,7 +398,15 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.3.1</version>
|
||||
</dependency>
|
||||
<!-- add brotli support for httpclient5 -->
|
||||
<dependency>
|
||||
<groupId>org.brotli</groupId>
|
||||
<artifactId>dec</artifactId>
|
||||
<version>0.1.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.hakky54</groupId>
|
||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||
@@ -423,35 +420,8 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.3.232</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.manticore-projects.tools</groupId>
|
||||
<artifactId>h2migrationtool</artifactId>
|
||||
<version>1.7</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.4.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -464,37 +434,15 @@
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mock-server</groupId>
|
||||
<artifactId>mockserver-junit-jupiter</artifactId>
|
||||
<version>5.15.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- dropwizard pulls a version of bouncycastle different than the one pulled by mockserver, causing NoSuchFieldError on BCObjectIdentifiers.sphincsPlus_shake_256 -->
|
||||
<!-- bouncycastle is required by mockserver to generate a self-signed certificate dynamically when https is used -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.icegreen</groupId>
|
||||
<artifactId>greenmail-junit5</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -508,30 +456,138 @@
|
||||
<version>1.46.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>0.10.2</version>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>native</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<quarkus.native.enabled>true</quarkus.native.enabled>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>h2</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>${properties-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>set-system-properties</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<properties>
|
||||
<property>
|
||||
<name>quarkus.datasource.db-kind</name>
|
||||
<value>h2</value>
|
||||
</property>
|
||||
</properties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>mysql</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>${properties-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>set-system-properties</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<properties>
|
||||
<property>
|
||||
<name>quarkus.datasource.db-kind</name>
|
||||
<value>mysql</value>
|
||||
</property>
|
||||
</properties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>mariadb</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>${properties-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>set-system-properties</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<properties>
|
||||
<property>
|
||||
<name>quarkus.datasource.db-kind</name>
|
||||
<value>mariadb</value>
|
||||
</property>
|
||||
</properties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>postgresql</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>${properties-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>set-system-properties</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<properties>
|
||||
<property>
|
||||
<name>quarkus.datasource.db-kind</name>
|
||||
<value>postgresql</value>
|
||||
</property>
|
||||
</properties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
21
commafeed-server/src/main/assembly/zip-quarkus-app.xml
Normal file
21
commafeed-server/src/main/assembly/zip-quarkus-app.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd">
|
||||
|
||||
<id>zip-quarkus-app</id>
|
||||
|
||||
<includeBaseDirectory>true</includeBaseDirectory>
|
||||
<baseDirectory>commafeed-${project.version}-${quarkus.datasource.db-kind}</baseDirectory>
|
||||
|
||||
<formats>
|
||||
<format>zip</format>
|
||||
</formats>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}/quarkus-app</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
16
commafeed-server/src/main/docker/Dockerfile.jvm
Normal file
16
commafeed-server/src/main/docker/Dockerfile.jvm
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM ibm-semeru-runtimes:open-21.0.3_9-jre
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/target/quarkus-app/ /commafeed
|
||||
WORKDIR /commafeed
|
||||
|
||||
CMD ["java", \
|
||||
"-Xtune:virtualized", \
|
||||
"-Xminf0.05", \
|
||||
"-Xmaxf0.1", \
|
||||
"-jar", \
|
||||
"quarkus-run.jar"]
|
||||
9
commafeed-server/src/main/docker/Dockerfile.native
Normal file
9
commafeed-server/src/main/docker/Dockerfile.native
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM debian:12.6
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/target/commafeed-*-runner /commafeed/application
|
||||
WORKDIR /commafeed
|
||||
CMD ["./application"]
|
||||
86
commafeed-server/src/main/docker/README.md
Normal file
86
commafeed-server/src/main/docker/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# CommaFeed
|
||||
|
||||
Official docker images for https://github.com/Athou/commafeed/
|
||||
|
||||
## Quickstart
|
||||
|
||||
Start CommaFeed with an embedded database. Then login as `admin/admin` on http://localhost:8082/
|
||||
|
||||
### docker
|
||||
|
||||
`docker run --name commafeed --detach --publish 8082:8082 --restart unless-stopped --volume /path/to/commafeed/db:/commafeed/data --memory 256M athou/commafeed:latest-h2`
|
||||
|
||||
### docker-compose
|
||||
|
||||
```
|
||||
services:
|
||||
commafeed:
|
||||
image: athou/commafeed:latest-h2
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /path/to/commafeed/db:/commafeed/data
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
ports:
|
||||
- 8082:8082
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
While using the embedded database is perfectly fine for small instances, you may want to have more control over the
|
||||
database. Here's an example that uses postgresql (note the different docker tag):
|
||||
|
||||
```
|
||||
services:
|
||||
commafeed:
|
||||
image: athou/commafeed:latest-postgresql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql:5432/commafeed
|
||||
- QUARKUS_DATASOURCE_USERNAME=commafeed
|
||||
- QUARKUS_DATASOURCE_PASSWORD=commafeed
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
ports:
|
||||
- 8082:8082
|
||||
|
||||
postgresql:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: commafeed
|
||||
POSTGRES_PASSWORD: commafeed
|
||||
POSTGRES_DB: commafeed
|
||||
volumes:
|
||||
- /path/to/commafeed/db:/var/lib/postgresql/data
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java)
|
||||
are optional and have sensible default values.
|
||||
|
||||
Settings are overrideable with environment variables. For instance, `config.feedRefresh().intervalEmpirical()` can be
|
||||
set
|
||||
with the `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL=true` variable.
|
||||
|
||||
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
|
||||
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||
`QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY` property to a fixed value (min. 16 characters).
|
||||
|
||||
## Docker tags
|
||||
|
||||
Tags are of the form `<version>-<database>[-jvm]` where:
|
||||
|
||||
- `<version>` is either:
|
||||
- a specific CommaFeed version (e.g. `4.6.0`)
|
||||
- `latest` (always points to the latest version)
|
||||
- `master` (always points to the latest git commit)
|
||||
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
||||
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports
|
||||
the
|
||||
arm64 platform which is not yet supported by the native image.
|
||||
@@ -1,277 +1,40 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
|
||||
import com.codahale.metrics.json.MetricsModule;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.feed.FeedRefreshEngine;
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.backend.service.db.DatabaseStartupService;
|
||||
import com.commafeed.backend.service.db.H2MigrationService;
|
||||
import com.commafeed.backend.task.ScheduledTask;
|
||||
import com.commafeed.frontend.auth.PasswordConstraintValidator;
|
||||
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
|
||||
import com.commafeed.frontend.resource.AdminREST;
|
||||
import com.commafeed.frontend.resource.CategoryREST;
|
||||
import com.commafeed.frontend.resource.EntryREST;
|
||||
import com.commafeed.frontend.resource.FeedREST;
|
||||
import com.commafeed.frontend.resource.ServerREST;
|
||||
import com.commafeed.frontend.resource.UserREST;
|
||||
import com.commafeed.frontend.resource.fever.FeverREST;
|
||||
import com.commafeed.frontend.servlet.CustomCssServlet;
|
||||
import com.commafeed.frontend.servlet.CustomJsServlet;
|
||||
import com.commafeed.frontend.servlet.LogoutServlet;
|
||||
import com.commafeed.frontend.servlet.NextUnreadServlet;
|
||||
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
|
||||
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
|
||||
import com.commafeed.frontend.ws.WebSocketConfigurator;
|
||||
import com.commafeed.frontend.ws.WebSocketEndpoint;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Key;
|
||||
import com.google.inject.TypeLiteral;
|
||||
import com.commafeed.backend.task.TaskScheduler;
|
||||
import com.commafeed.security.password.PasswordConstraintValidator;
|
||||
|
||||
import io.dropwizard.assets.AssetsBundle;
|
||||
import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
|
||||
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
|
||||
import io.dropwizard.configuration.SubstitutingSourceProvider;
|
||||
import io.dropwizard.core.Application;
|
||||
import io.dropwizard.core.ConfiguredBundle;
|
||||
import io.dropwizard.core.setup.Bootstrap;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.forms.MultiPartBundle;
|
||||
import io.dropwizard.hibernate.HibernateBundle;
|
||||
import io.dropwizard.migrations.MigrationsBundle;
|
||||
import io.dropwizard.servlets.CacheBustingFilter;
|
||||
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.websocket.server.ServerEndpointConfig;
|
||||
import io.quarkus.runtime.ShutdownEvent;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
@Singleton
|
||||
@RequiredArgsConstructor
|
||||
public class CommaFeedApplication {
|
||||
|
||||
public static final String USERNAME_ADMIN = "admin";
|
||||
public static final String USERNAME_DEMO = "demo";
|
||||
|
||||
public static final Instant STARTUP_TIME = Instant.now();
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
private final FeedRefreshEngine feedRefreshEngine;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
private HibernateBundle<CommaFeedConfiguration> hibernateBundle;
|
||||
public void start(@Observes StartupEvent ev) {
|
||||
PasswordConstraintValidator.setStrict(config.users().strictPasswordPolicy());
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CommaFeed";
|
||||
databaseStartupService.populateInitialData();
|
||||
|
||||
feedRefreshEngine.start();
|
||||
taskScheduler.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
configureEnvironmentSubstitutor(bootstrap);
|
||||
configureObjectMapper(bootstrap.getObjectMapper());
|
||||
|
||||
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
|
||||
bootstrap.addBundle(new ConfiguredBundle<>() {
|
||||
@Override
|
||||
public void run(CommaFeedConfiguration config, Environment environment) {
|
||||
DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
|
||||
String url = dataSourceFactory.getUrl();
|
||||
if (isFileBasedH2(url)) {
|
||||
Path path = getFilePath(url);
|
||||
String user = dataSourceFactory.getUser();
|
||||
String password = dataSourceFactory.getPassword();
|
||||
new H2MigrationService().migrateIfNeeded(path, user, password);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFileBasedH2(String url) {
|
||||
return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:");
|
||||
}
|
||||
|
||||
private Path getFilePath(String url) {
|
||||
String name = url.substring("jdbc:h2:".length()).split(";")[0];
|
||||
return Paths.get(name + ".mv.db");
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class,
|
||||
FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class,
|
||||
UserSettings.class) {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
||||
DataSourceFactory factory = configuration.getDataSourceFactory();
|
||||
|
||||
factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
|
||||
|
||||
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
|
||||
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
|
||||
factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
|
||||
factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
|
||||
return factory;
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(new MigrationsBundle<>() {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
||||
return configuration.getDataSourceFactory();
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
|
||||
bootstrap.addBundle(new MultiPartBundle());
|
||||
public void stop(@Observes ShutdownEvent ev) {
|
||||
feedRefreshEngine.stop();
|
||||
taskScheduler.stop();
|
||||
}
|
||||
|
||||
private static void configureEnvironmentSubstitutor(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<>() {
|
||||
@Override
|
||||
protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
|
||||
// disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty
|
||||
return objectMapper
|
||||
.setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.setConfigurationSourceProvider(buildEnvironmentSubstitutor(bootstrap));
|
||||
}
|
||||
|
||||
private static void configureObjectMapper(ObjectMapper objectMapper) {
|
||||
// read and write instants as milliseconds instead of nanoseconds
|
||||
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
|
||||
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
|
||||
|
||||
// add support for serializing metrics
|
||||
objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
|
||||
}
|
||||
|
||||
private static EnvironmentSubstitutor buildEnvironmentSubstitutor(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
// enable config.yml string substitution
|
||||
// e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR
|
||||
SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
|
||||
new EnvironmentVariableSubstitutor(false));
|
||||
|
||||
// enable config.yml properties override with env variables prefixed with CF_
|
||||
// e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true
|
||||
return new EnvironmentSubstitutor("CF", substitutingSourceProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(CommaFeedConfiguration config, Environment environment) {
|
||||
PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
|
||||
|
||||
// guice init
|
||||
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
|
||||
|
||||
// session management
|
||||
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
|
||||
|
||||
// support for "@SecurityCheck User user" injection
|
||||
environment.jersey()
|
||||
.register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserDAO.class),
|
||||
injector.getInstance(UserService.class), config));
|
||||
// support for "@Context SessionHelper sessionHelper" injection
|
||||
environment.jersey().register(new SessionHelperFactoryProvider.Binder());
|
||||
|
||||
// REST resources
|
||||
environment.jersey().setUrlPattern("/rest/*");
|
||||
environment.jersey().register(injector.getInstance(AdminREST.class));
|
||||
environment.jersey().register(injector.getInstance(CategoryREST.class));
|
||||
environment.jersey().register(injector.getInstance(EntryREST.class));
|
||||
environment.jersey().register(injector.getInstance(FeedREST.class));
|
||||
environment.jersey().register(injector.getInstance(ServerREST.class));
|
||||
environment.jersey().register(injector.getInstance(UserREST.class));
|
||||
environment.jersey().register(injector.getInstance(FeverREST.class));
|
||||
|
||||
// Servlets
|
||||
environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");
|
||||
environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout");
|
||||
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
|
||||
environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
|
||||
if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
|
||||
environment.servlets()
|
||||
.addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
|
||||
.addMapping("/robots.txt");
|
||||
}
|
||||
|
||||
// WebSocket endpoint
|
||||
JakartaWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> {
|
||||
container.setDefaultMaxSessionIdleTimeout(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000);
|
||||
|
||||
container.addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
|
||||
.configurator(injector.getInstance(WebSocketConfigurator.class))
|
||||
.build());
|
||||
});
|
||||
|
||||
// Scheduled tasks
|
||||
Set<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<>() {
|
||||
}));
|
||||
ScheduledExecutorService executor = environment.lifecycle()
|
||||
.scheduledExecutorService("task-scheduler", true)
|
||||
.threads(tasks.size())
|
||||
.build();
|
||||
for (ScheduledTask task : tasks) {
|
||||
task.register(executor);
|
||||
}
|
||||
|
||||
// database init/changelogs
|
||||
environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class));
|
||||
|
||||
// start feed fetching engine
|
||||
environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class));
|
||||
|
||||
// prevent caching index.html, so that the webapp is always up to date
|
||||
environment.servlets()
|
||||
.addFilter("index-cache-busting-filter", new CacheBustingFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/");
|
||||
|
||||
// prevent caching openapi files, so that the documentation is always up to date
|
||||
environment.servlets()
|
||||
.addFilter("openapi-cache-busting-filter", new CacheBustingFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/openapi.json", "/openapi.yaml");
|
||||
|
||||
// prevent caching REST resources, except for favicons
|
||||
environment.servlets().addFilter("rest-cache-busting-filter", new CacheBustingFilter() {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
String path = ((HttpServletRequest) request).getRequestURI();
|
||||
if (path.contains("/feed/favicon")) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
super.doFilter(request, response, chain);
|
||||
}
|
||||
}
|
||||
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new CommaFeedApplication().run(args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,188 +1,275 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Properties;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.commafeed.backend.cache.RedisPoolFactory;
|
||||
import com.commafeed.frontend.session.SessionHandlerFactory;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
|
||||
|
||||
import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.dropwizard.util.Duration;
|
||||
import jakarta.validation.Valid;
|
||||
import io.quarkus.runtime.configuration.MemorySize;
|
||||
import io.smallrye.config.ConfigMapping;
|
||||
import io.smallrye.config.WithDefault;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CommaFeedConfiguration extends Configuration {
|
||||
/**
|
||||
* CommaFeed configuration
|
||||
*
|
||||
* Default values are for production, they can be overridden in application.properties for other profiles
|
||||
*/
|
||||
@ConfigMapping(prefix = "commafeed")
|
||||
public interface CommaFeedConfiguration {
|
||||
/**
|
||||
* Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean hideFromWebCrawlers();
|
||||
|
||||
public enum CacheType {
|
||||
NOOP, REDIS
|
||||
/**
|
||||
* If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser.
|
||||
*
|
||||
* This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean imageProxyEnabled();
|
||||
|
||||
/**
|
||||
* Enable password recovery via email.
|
||||
*
|
||||
* Quarkus mailer will need to be configured.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean passwordRecoveryEnabled();
|
||||
|
||||
/**
|
||||
* Message displayed in a notification at the bottom of the page.
|
||||
*/
|
||||
Optional<String> announcement();
|
||||
|
||||
/**
|
||||
* Google Analytics tracking code.
|
||||
*/
|
||||
Optional<String> googleAnalyticsTrackingCode();
|
||||
|
||||
/**
|
||||
* Google Auth key for fetching Youtube channel favicons.
|
||||
*/
|
||||
Optional<String> googleAuthKey();
|
||||
|
||||
/**
|
||||
* HTTP client configuration
|
||||
*/
|
||||
HttpClient httpClient();
|
||||
|
||||
/**
|
||||
* Feed refresh engine settings.
|
||||
*/
|
||||
FeedRefresh feedRefresh();
|
||||
|
||||
/**
|
||||
* Database settings.
|
||||
*/
|
||||
Database database();
|
||||
|
||||
/**
|
||||
* Users settings.
|
||||
*/
|
||||
Users users();
|
||||
|
||||
/**
|
||||
* Websocket settings.
|
||||
*/
|
||||
Websocket websocket();
|
||||
|
||||
interface HttpClient {
|
||||
/**
|
||||
* User-Agent string that will be used by the http client, leave empty for the default one.
|
||||
*/
|
||||
Optional<String> userAgent();
|
||||
|
||||
/**
|
||||
* Time to wait for a connection to be established.
|
||||
*/
|
||||
@WithDefault("5s")
|
||||
Duration connectTimeout();
|
||||
|
||||
/**
|
||||
* Time to wait for SSL handshake to complete.
|
||||
*/
|
||||
@WithDefault("5s")
|
||||
Duration sslHandshakeTimeout();
|
||||
|
||||
/**
|
||||
* Time to wait between two packets before timeout.
|
||||
*/
|
||||
@WithDefault("10s")
|
||||
Duration socketTimeout();
|
||||
|
||||
/**
|
||||
* Time to wait for the full response to be received.
|
||||
*/
|
||||
@WithDefault("10s")
|
||||
Duration responseTimeout();
|
||||
|
||||
/**
|
||||
* Time to live for a connection in the pool.
|
||||
*/
|
||||
@WithDefault("30s")
|
||||
Duration connectionTimeToLive();
|
||||
|
||||
/**
|
||||
* Time between eviction runs for idle connections.
|
||||
*/
|
||||
@WithDefault("1m")
|
||||
Duration idleConnectionsEvictionInterval();
|
||||
|
||||
/**
|
||||
* If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
|
||||
*/
|
||||
@WithDefault("5M")
|
||||
MemorySize maxResponseSize();
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("database")
|
||||
private final DataSourceFactory dataSourceFactory = new DataSourceFactory();
|
||||
interface FeedRefresh {
|
||||
/**
|
||||
* Amount of time CommaFeed will wait before refreshing the same feed.
|
||||
*/
|
||||
@WithDefault("5m")
|
||||
Duration interval();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("redis")
|
||||
private final RedisPoolFactory redisPoolFactory = new RedisPoolFactory();
|
||||
/**
|
||||
* If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the
|
||||
* last entry was published. The interval will be somewhere between the default refresh interval and 24h.
|
||||
*
|
||||
* See {@link FeedRefreshIntervalCalculator} for details.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean intervalEmpirical();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("session")
|
||||
private final SessionHandlerFactory sessionHandlerFactory = new SessionHandlerFactory();
|
||||
/**
|
||||
* Amount of http threads used to fetch feeds.
|
||||
*/
|
||||
@Min(1)
|
||||
@WithDefault("3")
|
||||
int httpThreads();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("app")
|
||||
private ApplicationSettings applicationSettings;
|
||||
/**
|
||||
* Amount of threads used to insert new entries in the database.
|
||||
*/
|
||||
@Min(1)
|
||||
@WithDefault("1")
|
||||
int databaseThreads();
|
||||
|
||||
private final String version;
|
||||
private final String gitCommit;
|
||||
/**
|
||||
* Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration userInactivityPeriod();
|
||||
|
||||
public CommaFeedConfiguration() {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
|
||||
if (stream != null) {
|
||||
properties.load(stream);
|
||||
/**
|
||||
* Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
|
||||
*/
|
||||
@WithDefault("500ms")
|
||||
Duration filteringExpressionEvaluationTimeout();
|
||||
}
|
||||
|
||||
interface Database {
|
||||
/**
|
||||
* Database query timeout.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration queryTimeout();
|
||||
|
||||
Cleanup cleanup();
|
||||
|
||||
interface Cleanup {
|
||||
/**
|
||||
* Maximum age of feed entries in the database. Older entries will be deleted.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("365d")
|
||||
Duration entriesMaxAge();
|
||||
|
||||
/**
|
||||
* Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration statusesMaxAge();
|
||||
|
||||
/**
|
||||
* Maximum number of entries per feed to keep in the database.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("500")
|
||||
int maxFeedCapacity();
|
||||
|
||||
/**
|
||||
* Limit the number of feeds a user can subscribe to.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
int maxFeedsPerUser();
|
||||
|
||||
/**
|
||||
* Rows to delete per query while cleaning up old entries.
|
||||
*/
|
||||
@Positive
|
||||
@WithDefault("100")
|
||||
int batchSize();
|
||||
|
||||
default Instant statusesInstantThreshold() {
|
||||
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.version = properties.getProperty("git.build.version", "unknown");
|
||||
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ApplicationSettings {
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@Valid
|
||||
private String publicUrl;
|
||||
interface Users {
|
||||
/**
|
||||
* Whether to let users create accounts for themselves.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean allowRegistrations();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean hideFromWebCrawlers = true;
|
||||
/**
|
||||
* Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean strictPasswordPolicy();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean allowRegistrations;
|
||||
/**
|
||||
* Whether to create a demo account the first time the app starts.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean createDemoAccount();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean strictPasswordPolicy = true;
|
||||
interface Websocket {
|
||||
/**
|
||||
* Enable websocket connection so the server can notify web clients that there are new entries for feeds.
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean enabled();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean createDemoAccount;
|
||||
|
||||
private String googleAnalyticsTrackingCode;
|
||||
|
||||
private String googleAuthKey;
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@Valid
|
||||
private Integer backgroundThreads;
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@Valid
|
||||
private Integer databaseUpdateThreads;
|
||||
|
||||
@NotNull
|
||||
@Positive
|
||||
@Valid
|
||||
private Integer databaseCleanupBatchSize = 100;
|
||||
|
||||
private String smtpHost;
|
||||
private int smtpPort;
|
||||
private boolean smtpTls;
|
||||
private String smtpUserName;
|
||||
private String smtpPassword;
|
||||
private String smtpFromAddress;
|
||||
|
||||
private boolean graphiteEnabled;
|
||||
private String graphitePrefix;
|
||||
private String graphiteHost;
|
||||
private int graphitePort;
|
||||
private int graphiteInterval;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean heavyLoad;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean imageProxyEnabled;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer queryTimeout;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer keepStatusDays;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer maxFeedCapacity;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer maxEntriesAgeDays = 0;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Integer maxFeedsPerUser = 0;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private DataSize maxFeedResponseSize = DataSize.megabytes(5);
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer refreshIntervalMinutes;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private CacheType cache;
|
||||
|
||||
@Valid
|
||||
private String announcement;
|
||||
|
||||
private String userAgent;
|
||||
|
||||
private Boolean websocketEnabled = true;
|
||||
|
||||
private Duration websocketPingInterval = Duration.minutes(15);
|
||||
|
||||
private Duration treeReloadInterval = Duration.seconds(30);
|
||||
|
||||
public Instant getUnreadThreshold() {
|
||||
return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null;
|
||||
}
|
||||
/**
|
||||
* Interval at which the client will send a ping message on the websocket to keep the connection alive.
|
||||
*/
|
||||
@WithDefault("15m")
|
||||
Duration pingInterval();
|
||||
|
||||
/**
|
||||
* If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
|
||||
*/
|
||||
@WithDefault("30s")
|
||||
Duration treeReloadInterval();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.codahale.metrics.MetricFilter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.graphite.Graphite;
|
||||
import com.codahale.metrics.graphite.GraphiteReporter;
|
||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
||||
import com.commafeed.CommaFeedConfiguration.CacheType;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.cache.NoopCacheService;
|
||||
import com.commafeed.backend.cache.RedisCacheService;
|
||||
import com.commafeed.backend.favicon.AbstractFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.DefaultFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.FacebookFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.YoutubeFaviconFetcher;
|
||||
import com.commafeed.backend.task.DemoAccountCleanupTask;
|
||||
import com.commafeed.backend.task.EntriesExceedingFeedCapacityCleanupTask;
|
||||
import com.commafeed.backend.task.OldEntriesCleanupTask;
|
||||
import com.commafeed.backend.task.OldStatusesCleanupTask;
|
||||
import com.commafeed.backend.task.OrphanedContentsCleanupTask;
|
||||
import com.commafeed.backend.task.OrphanedFeedsCleanupTask;
|
||||
import com.commafeed.backend.task.ScheduledTask;
|
||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||
import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider;
|
||||
import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CommaFeedModule extends AbstractModule {
|
||||
|
||||
@Getter(onMethod = @__({ @Provides }))
|
||||
private final SessionFactory sessionFactory;
|
||||
|
||||
@Getter(onMethod = @__({ @Provides }))
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Getter(onMethod = @__({ @Provides }))
|
||||
private final MetricRegistry metrics;
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
CacheService cacheService = config.getApplicationSettings().getCache() == CacheType.NOOP ? new NoopCacheService()
|
||||
: new RedisCacheService(config.getRedisPoolFactory().build());
|
||||
log.info("using cache {}", cacheService.getClass());
|
||||
bind(CacheService.class).toInstance(cacheService);
|
||||
|
||||
Multibinder<AbstractFaviconFetcher> faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
|
||||
faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
|
||||
faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
|
||||
faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
|
||||
|
||||
Multibinder<FeedURLProvider> urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class);
|
||||
urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class);
|
||||
urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class);
|
||||
|
||||
Multibinder<ScheduledTask> taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class);
|
||||
taskMultibinder.addBinding().to(OldStatusesCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(EntriesExceedingFeedCapacityCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(OldEntriesCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(DemoAccountCleanupTask.class);
|
||||
|
||||
ApplicationSettings settings = config.getApplicationSettings();
|
||||
|
||||
if (settings.isGraphiteEnabled()) {
|
||||
final String graphitePrefix = settings.getGraphitePrefix();
|
||||
final String graphiteHost = settings.getGraphiteHost();
|
||||
final int graphitePort = settings.getGraphitePort();
|
||||
final int graphiteInterval = settings.getGraphiteInterval();
|
||||
|
||||
log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort,
|
||||
graphitePrefix, graphiteInterval);
|
||||
|
||||
final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort));
|
||||
final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics)
|
||||
.prefixedWith(graphitePrefix)
|
||||
.convertRatesTo(TimeUnit.SECONDS)
|
||||
.convertDurationsTo(TimeUnit.MILLISECONDS)
|
||||
.filter(MetricFilter.ALL)
|
||||
.build(graphite);
|
||||
reporter.start(graphiteInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.commafeed;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class CommaFeedProducers {
|
||||
|
||||
@Produces
|
||||
@Singleton
|
||||
public MetricRegistry metricRegistry() {
|
||||
return new MetricRegistry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.Getter;
|
||||
|
||||
@Singleton
|
||||
@Getter
|
||||
public class CommaFeedVersion {
|
||||
|
||||
private final String version;
|
||||
private final String gitCommit;
|
||||
|
||||
public CommaFeedVersion() {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
|
||||
if (stream != null) {
|
||||
properties.load(stream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.version = properties.getProperty("git.build.version", "unknown");
|
||||
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.commafeed;
|
||||
|
||||
import org.jboss.resteasy.reactive.RestResponse;
|
||||
import org.jboss.resteasy.reactive.RestResponse.Status;
|
||||
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.security.AuthenticationFailedException;
|
||||
import io.quarkus.security.UnauthorizedException;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.validation.ValidationException;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Provider
|
||||
@Priority(1)
|
||||
public class ExceptionMappers {
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@ServerExceptionMapper(UnauthorizedException.class)
|
||||
public RestResponse<UnauthorizedResponse> unauthorized(UnauthorizedException e) {
|
||||
return RestResponse.status(RestResponse.Status.UNAUTHORIZED,
|
||||
new UnauthorizedResponse(e.getMessage(), config.users().allowRegistrations()));
|
||||
}
|
||||
|
||||
@ServerExceptionMapper(AuthenticationFailedException.class)
|
||||
public RestResponse<AuthenticationFailed> authenticationFailed(AuthenticationFailedException e) {
|
||||
return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage()));
|
||||
}
|
||||
|
||||
@ServerExceptionMapper(ValidationException.class)
|
||||
public RestResponse<ValidationFailed> validationFailed(ValidationException e) {
|
||||
return RestResponse.status(Status.BAD_REQUEST, new ValidationFailed(e.getMessage()));
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record UnauthorizedResponse(String message, boolean allowRegistrations) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record AuthenticationFailed(String message) {
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record ValidationFailed(String message) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.codahale.metrics.json.MetricsModule;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class JacksonCustomizer implements ObjectMapperCustomizer {
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// read and write instants as milliseconds instead of nanoseconds
|
||||
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
|
||||
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
|
||||
|
||||
// add support for serializing metrics
|
||||
objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.commafeed;
|
||||
|
||||
import com.codahale.metrics.Counter;
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.Timer;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = {
|
||||
// metrics
|
||||
MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class,
|
||||
|
||||
// rome
|
||||
java.util.Date.class, com.rometools.opml.feed.synd.impl.TreeCategoryImpl.class,
|
||||
com.rometools.rome.feed.synd.SyndFeedImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class,
|
||||
com.rometools.rome.feed.synd.SyndEntryImpl.class, com.rometools.modules.psc.types.SimpleChapter.class,
|
||||
com.rometools.rome.feed.synd.SyndCategoryImpl.class, com.rometools.rome.feed.synd.SyndImageImpl.class,
|
||||
com.rometools.rome.feed.synd.SyndContentImpl.class, com.rometools.rome.feed.synd.SyndEnclosureImpl.class,
|
||||
|
||||
// rome cloneable
|
||||
com.rometools.modules.activitystreams.types.Article.class, com.rometools.modules.activitystreams.types.Audio.class,
|
||||
com.rometools.modules.activitystreams.types.Bookmark.class, com.rometools.modules.activitystreams.types.Comment.class,
|
||||
com.rometools.modules.activitystreams.types.Event.class, com.rometools.modules.activitystreams.types.File.class,
|
||||
com.rometools.modules.activitystreams.types.Folder.class, com.rometools.modules.activitystreams.types.List.class,
|
||||
com.rometools.modules.activitystreams.types.Note.class, com.rometools.modules.activitystreams.types.Person.class,
|
||||
com.rometools.modules.activitystreams.types.Photo.class, com.rometools.modules.activitystreams.types.PhotoAlbum.class,
|
||||
com.rometools.modules.activitystreams.types.Place.class, com.rometools.modules.activitystreams.types.Playlist.class,
|
||||
com.rometools.modules.activitystreams.types.Product.class, com.rometools.modules.activitystreams.types.Review.class,
|
||||
com.rometools.modules.activitystreams.types.Service.class, com.rometools.modules.activitystreams.types.Song.class,
|
||||
com.rometools.modules.activitystreams.types.Status.class, com.rometools.modules.base.types.DateTimeRange.class,
|
||||
com.rometools.modules.base.types.FloatUnit.class, com.rometools.modules.base.types.GenderEnumeration.class,
|
||||
com.rometools.modules.base.types.IntUnit.class, com.rometools.modules.base.types.PriceTypeEnumeration.class,
|
||||
com.rometools.modules.base.types.ShippingType.class, com.rometools.modules.base.types.ShortDate.class,
|
||||
com.rometools.modules.base.types.Size.class, com.rometools.modules.base.types.YearType.class,
|
||||
com.rometools.modules.content.ContentItem.class, com.rometools.modules.georss.GeoRSSPoint.class,
|
||||
com.rometools.modules.georss.geometries.Envelope.class, com.rometools.modules.georss.geometries.LineString.class,
|
||||
com.rometools.modules.georss.geometries.LinearRing.class, com.rometools.modules.georss.geometries.Point.class,
|
||||
com.rometools.modules.georss.geometries.Polygon.class, com.rometools.modules.georss.geometries.Position.class,
|
||||
com.rometools.modules.georss.geometries.PositionList.class, com.rometools.modules.mediarss.types.MediaGroup.class,
|
||||
com.rometools.modules.mediarss.types.Metadata.class, com.rometools.modules.mediarss.types.Thumbnail.class,
|
||||
com.rometools.modules.opensearch.entity.OSQuery.class, com.rometools.modules.photocast.types.PhotoDate.class,
|
||||
com.rometools.modules.sle.types.DateValue.class, com.rometools.modules.sle.types.Group.class,
|
||||
com.rometools.modules.sle.types.NumberValue.class, com.rometools.modules.sle.types.Sort.class,
|
||||
com.rometools.modules.sle.types.StringValue.class, com.rometools.modules.yahooweather.types.Astronomy.class,
|
||||
com.rometools.modules.yahooweather.types.Atmosphere.class, com.rometools.modules.yahooweather.types.Condition.class,
|
||||
com.rometools.modules.yahooweather.types.Forecast.class, com.rometools.modules.yahooweather.types.Location.class,
|
||||
com.rometools.modules.yahooweather.types.Units.class, com.rometools.modules.yahooweather.types.Wind.class,
|
||||
com.rometools.opml.feed.opml.Attribute.class, com.rometools.opml.feed.opml.Opml.class,
|
||||
com.rometools.opml.feed.opml.Outline.class, com.rometools.rome.feed.atom.Category.class,
|
||||
com.rometools.rome.feed.atom.Content.class, com.rometools.rome.feed.atom.Entry.class,
|
||||
com.rometools.rome.feed.atom.Feed.class, com.rometools.rome.feed.atom.Generator.class,
|
||||
com.rometools.rome.feed.atom.Link.class, com.rometools.rome.feed.atom.Person.class,
|
||||
com.rometools.rome.feed.rss.Category.class, com.rometools.rome.feed.rss.Channel.class,
|
||||
com.rometools.rome.feed.rss.Cloud.class, com.rometools.rome.feed.rss.Content.class,
|
||||
com.rometools.rome.feed.rss.Description.class, com.rometools.rome.feed.rss.Enclosure.class,
|
||||
com.rometools.rome.feed.rss.Guid.class, com.rometools.rome.feed.rss.Image.class, com.rometools.rome.feed.rss.Item.class,
|
||||
com.rometools.rome.feed.rss.Source.class, com.rometools.rome.feed.rss.TextInput.class,
|
||||
com.rometools.rome.feed.synd.SyndLinkImpl.class, com.rometools.rome.feed.synd.SyndPersonImpl.class,
|
||||
java.util.ArrayList.class,
|
||||
|
||||
// rome modules
|
||||
com.rometools.modules.sse.modules.Conflict.class, com.rometools.modules.sse.modules.Conflicts.class,
|
||||
com.rometools.modules.cc.CreativeCommonsImpl.class, com.rometools.modules.feedpress.modules.FeedpressModuleImpl.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleImpl.class, com.rometools.modules.sse.modules.Sharing.class,
|
||||
com.rometools.modules.georss.SimpleModuleImpl.class, com.rometools.modules.atom.modules.AtomLinkModuleImpl.class,
|
||||
com.rometools.modules.itunes.EntryInformationImpl.class, com.rometools.modules.sse.modules.Update.class,
|
||||
com.rometools.modules.photocast.PhotocastModuleImpl.class, com.rometools.modules.itunes.FeedInformationImpl.class,
|
||||
com.rometools.modules.yahooweather.YWeatherModuleImpl.class, com.rometools.modules.feedburner.FeedBurnerImpl.class,
|
||||
com.rometools.modules.sse.modules.Related.class, com.rometools.modules.fyyd.modules.FyydModuleImpl.class,
|
||||
com.rometools.modules.psc.modules.PodloveSimpleChapterModuleImpl.class, com.rometools.modules.thr.ThreadingModuleImpl.class,
|
||||
com.rometools.modules.sse.modules.Sync.class, com.rometools.modules.sle.SimpleListExtensionImpl.class,
|
||||
com.rometools.modules.slash.SlashImpl.class, com.rometools.modules.sse.modules.History.class,
|
||||
com.rometools.modules.georss.GMLModuleImpl.class, com.rometools.modules.base.CustomTagsImpl.class,
|
||||
com.rometools.modules.base.GoogleBaseImpl.class, com.rometools.modules.sle.SleEntryImpl.class,
|
||||
com.rometools.modules.mediarss.MediaEntryModuleImpl.class, com.rometools.modules.content.ContentModuleImpl.class,
|
||||
com.rometools.modules.georss.W3CGeoModuleImpl.class, com.rometools.rome.feed.module.DCModuleImpl.class,
|
||||
com.rometools.modules.mediarss.MediaModuleImpl.class, com.rometools.rome.feed.module.SyModuleImpl.class,
|
||||
|
||||
// extracted from all 3 rome.properties files of rome library
|
||||
com.rometools.rome.io.impl.RSS090Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class,
|
||||
com.rometools.rome.io.impl.RSS091UserlandParser.class, com.rometools.rome.io.impl.RSS092Parser.class,
|
||||
com.rometools.rome.io.impl.RSS093Parser.class, com.rometools.rome.io.impl.RSS094Parser.class,
|
||||
com.rometools.rome.io.impl.RSS10Parser.class, com.rometools.rome.io.impl.RSS20wNSParser.class,
|
||||
com.rometools.rome.io.impl.RSS20Parser.class, com.rometools.rome.io.impl.Atom10Parser.class,
|
||||
com.rometools.rome.io.impl.Atom03Parser.class,
|
||||
|
||||
com.rometools.rome.io.impl.SyModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class,
|
||||
|
||||
com.rometools.rome.io.impl.RSS090Generator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class,
|
||||
com.rometools.rome.io.impl.RSS091UserlandGenerator.class, com.rometools.rome.io.impl.RSS092Generator.class,
|
||||
com.rometools.rome.io.impl.RSS093Generator.class, com.rometools.rome.io.impl.RSS094Generator.class,
|
||||
com.rometools.rome.io.impl.RSS10Generator.class, com.rometools.rome.io.impl.RSS20Generator.class,
|
||||
com.rometools.rome.io.impl.Atom10Generator.class, com.rometools.rome.io.impl.Atom03Generator.class,
|
||||
|
||||
com.rometools.rome.feed.synd.impl.ConverterForAtom10.class, com.rometools.rome.feed.synd.impl.ConverterForAtom03.class,
|
||||
com.rometools.rome.feed.synd.impl.ConverterForRSS090.class,
|
||||
com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class,
|
||||
com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class,
|
||||
com.rometools.rome.feed.synd.impl.ConverterForRSS092.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class,
|
||||
com.rometools.rome.feed.synd.impl.ConverterForRSS094.class, com.rometools.rome.feed.synd.impl.ConverterForRSS10.class,
|
||||
com.rometools.rome.feed.synd.impl.ConverterForRSS20.class,
|
||||
|
||||
com.rometools.modules.mediarss.io.RSS20YahooParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
|
||||
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
|
||||
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
|
||||
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
|
||||
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
|
||||
com.rometools.modules.fyyd.io.FyydParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
|
||||
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
|
||||
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
|
||||
com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
|
||||
com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
|
||||
com.rometools.modules.fyyd.io.FyydParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.content.io.ContentModuleParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
|
||||
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
|
||||
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
|
||||
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
|
||||
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
|
||||
com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.fyyd.io.FyydParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
|
||||
com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
|
||||
com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
|
||||
com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
|
||||
com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ItemParser.class,
|
||||
com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
|
||||
com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.base.io.GoogleBaseParser.class,
|
||||
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
|
||||
com.rometools.modules.slash.io.SlashModuleParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
|
||||
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
|
||||
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
|
||||
com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
|
||||
com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
|
||||
com.rometools.modules.thr.io.ThreadingModuleParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
|
||||
|
||||
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
|
||||
com.rometools.modules.itunes.io.ITunesGenerator.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
|
||||
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
|
||||
com.rometools.modules.sle.io.ModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
|
||||
com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
|
||||
|
||||
com.rometools.modules.content.io.ContentModuleGenerator.class,
|
||||
|
||||
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
|
||||
com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
|
||||
com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
|
||||
|
||||
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
|
||||
com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
|
||||
com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
|
||||
com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
|
||||
|
||||
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
|
||||
com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
|
||||
com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.itunes.io.ITunesGenerator.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
|
||||
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
|
||||
com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
|
||||
com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
|
||||
|
||||
com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
|
||||
com.rometools.modules.slash.io.SlashModuleGenerator.class,
|
||||
|
||||
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
|
||||
com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
|
||||
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
|
||||
|
||||
com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class,
|
||||
com.rometools.modules.slash.io.SlashModuleGenerator.class,
|
||||
com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
|
||||
com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
|
||||
com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.thr.io.ThreadingModuleGenerator.class,
|
||||
com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
|
||||
|
||||
com.rometools.modules.mediarss.io.MediaModuleParser.class,
|
||||
|
||||
com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
|
||||
|
||||
com.rometools.opml.io.impl.OPML10Generator.class, com.rometools.opml.io.impl.OPML20Generator.class,
|
||||
|
||||
com.rometools.opml.io.impl.OPML10Parser.class, com.rometools.opml.io.impl.OPML20Parser.class,
|
||||
|
||||
com.rometools.opml.feed.synd.impl.ConverterForOPML10.class, com.rometools.opml.feed.synd.impl.ConverterForOPML20.class, })
|
||||
|
||||
public class NativeImageClasses {
|
||||
}
|
||||
@@ -3,14 +3,15 @@ package com.commafeed.backend;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.config.TlsConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
@@ -21,21 +22,20 @@ import org.apache.hc.client5.http.protocol.RedirectLocations;
|
||||
import org.apache.hc.core5.http.ClassicHttpRequest;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpEntity;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.apache.hc.core5.http.NameValuePair;
|
||||
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
|
||||
import org.apache.hc.core5.http.message.BasicHeader;
|
||||
import org.apache.hc.core5.util.TimeValue;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedVersion;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import io.dropwizard.util.DataSize;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -50,16 +50,18 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
||||
@Slf4j
|
||||
public class HttpGetter {
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
private final CloseableHttpClient client;
|
||||
private final DataSize maxResponseSize;
|
||||
|
||||
@Inject
|
||||
public HttpGetter(CommaFeedConfiguration config, MetricRegistry metrics) {
|
||||
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.getApplicationSettings().getBackgroundThreads());
|
||||
String userAgent = Optional.ofNullable(config.getApplicationSettings().getUserAgent())
|
||||
.orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion()));
|
||||
this.client = newClient(connectionManager, userAgent);
|
||||
this.maxResponseSize = config.getApplicationSettings().getMaxFeedResponseSize();
|
||||
public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) {
|
||||
this.config = config;
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config);
|
||||
String userAgent = config.httpClient()
|
||||
.userAgent()
|
||||
.orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
|
||||
|
||||
this.client = newClient(connectionManager, userAgent, config.httpClient().idleConnectionsEvictionInterval());
|
||||
|
||||
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax());
|
||||
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"),
|
||||
@@ -68,8 +70,8 @@ public class HttpGetter {
|
||||
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "pending"), () -> connectionManager.getTotalStats().getPending());
|
||||
}
|
||||
|
||||
public HttpResult getBinary(String url, int timeout) throws IOException, NotModifiedException {
|
||||
return getBinary(url, null, null, timeout);
|
||||
public HttpResult getBinary(String url) throws IOException, NotModifiedException {
|
||||
return getBinary(url, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,10 +84,9 @@ public class HttpGetter {
|
||||
* @throws NotModifiedException
|
||||
* if the url hasn't changed since we asked for it last time
|
||||
*/
|
||||
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException {
|
||||
public HttpResult getBinary(String url, String lastModified, String eTag) throws IOException, NotModifiedException {
|
||||
log.debug("fetching {}", url);
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
ClassicHttpRequest request = ClassicRequestBuilder.get(url).build();
|
||||
if (lastModified != null) {
|
||||
request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
|
||||
@@ -95,10 +96,10 @@ public class HttpGetter {
|
||||
}
|
||||
|
||||
HttpClientContext context = HttpClientContext.create();
|
||||
context.setRequestConfig(RequestConfig.custom().setResponseTimeout(timeout, TimeUnit.MILLISECONDS).build());
|
||||
|
||||
context.setRequestConfig(RequestConfig.custom().setResponseTimeout(Timeout.of(config.httpClient().responseTimeout())).build());
|
||||
HttpResponse response = client.execute(request, context, resp -> {
|
||||
byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.toBytes());
|
||||
byte[] content = resp.getEntity() == null ? null
|
||||
: toByteArray(resp.getEntity(), config.httpClient().maxResponseSize().asLongValue());
|
||||
int code = resp.getCode();
|
||||
String lastModifiedHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED))
|
||||
.map(NameValuePair::getValue)
|
||||
@@ -120,7 +121,7 @@ public class HttpGetter {
|
||||
});
|
||||
|
||||
int code = response.getCode();
|
||||
if (code == HttpStatus.NOT_MODIFIED_304) {
|
||||
if (code == HttpStatus.SC_NOT_MODIFIED) {
|
||||
throw new NotModifiedException("'304 - not modified' http code received");
|
||||
} else if (code >= 300) {
|
||||
throw new HttpResponseException(code, "Server returned HTTP error code " + code);
|
||||
@@ -136,8 +137,7 @@ public class HttpGetter {
|
||||
throw new NotModifiedException("eTagHeader is the same");
|
||||
}
|
||||
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader, duration,
|
||||
return new HttpResult(response.getContent(), response.getContentType(), lastModifiedHeader, eTagHeader,
|
||||
response.getUrlAfterRedirect());
|
||||
}
|
||||
|
||||
@@ -160,20 +160,26 @@ public class HttpGetter {
|
||||
}
|
||||
}
|
||||
|
||||
private static PoolingHttpClientConnectionManager newConnectionManager(int poolSize) {
|
||||
private static PoolingHttpClientConnectionManager newConnectionManager(CommaFeedConfiguration config) {
|
||||
SSLFactory sslFactory = SSLFactory.builder().withUnsafeTrustMaterial().withUnsafeHostnameVerifier().build();
|
||||
|
||||
int poolSize = config.feedRefresh().httpThreads();
|
||||
return PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setSSLSocketFactory(Apache5SslUtils.toSocketFactory(sslFactory))
|
||||
.setDefaultConnectionConfig(
|
||||
ConnectionConfig.custom().setConnectTimeout(Timeout.ofSeconds(5)).setTimeToLive(TimeValue.ofSeconds(30)).build())
|
||||
.setDefaultConnectionConfig(ConnectionConfig.custom()
|
||||
.setConnectTimeout(Timeout.of(config.httpClient().connectTimeout()))
|
||||
.setSocketTimeout(Timeout.of(config.httpClient().socketTimeout()))
|
||||
.setTimeToLive(Timeout.of(config.httpClient().connectionTimeToLive()))
|
||||
.build())
|
||||
.setDefaultTlsConfig(TlsConfig.custom().setHandshakeTimeout(Timeout.of(config.httpClient().sslHandshakeTimeout())).build())
|
||||
.setMaxConnPerRoute(poolSize)
|
||||
.setMaxConnTotal(poolSize)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
private static CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent) {
|
||||
private static CloseableHttpClient newClient(HttpClientConnectionManager connectionManager, String userAgent,
|
||||
Duration idleConnectionsEvictionInterval) {
|
||||
List<Header> headers = new ArrayList<>();
|
||||
headers.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en"));
|
||||
headers.add(new BasicHeader(HttpHeaders.PRAGMA, "No-cache"));
|
||||
@@ -187,7 +193,7 @@ public class HttpGetter {
|
||||
.setDefaultHeaders(headers)
|
||||
.setConnectionManager(connectionManager)
|
||||
.evictExpiredConnections()
|
||||
.evictIdleConnections(TimeValue.ofMinutes(1))
|
||||
.evictIdleConnections(TimeValue.of(idleConnectionsEvictionInterval))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -247,7 +253,6 @@ public class HttpGetter {
|
||||
private final String contentType;
|
||||
private final String lastModifiedSince;
|
||||
private final String eTag;
|
||||
private final long duration;
|
||||
private final String urlAfterRedirect;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.commafeed.backend.cache;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.commafeed.backend.Digests;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.model.Category;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
|
||||
public abstract class CacheService {
|
||||
|
||||
// feed entries for faster refresh
|
||||
public abstract Set<String> getLastEntries(Feed feed);
|
||||
|
||||
public abstract void setLastEntries(Feed feed, List<String> entries);
|
||||
|
||||
public String buildUniqueEntryKey(Entry entry) {
|
||||
return Digests.sha1Hex(entry.guid() + entry.url());
|
||||
}
|
||||
|
||||
// user categories
|
||||
public abstract Category getUserRootCategory(User user);
|
||||
|
||||
public abstract void setUserRootCategory(User user, Category category);
|
||||
|
||||
public abstract void invalidateUserRootCategory(User... users);
|
||||
|
||||
// unread count
|
||||
public abstract UnreadCount getUnreadCount(FeedSubscription sub);
|
||||
|
||||
public abstract void setUnreadCount(FeedSubscription sub, UnreadCount count);
|
||||
|
||||
public abstract void invalidateUnreadCount(FeedSubscription... subs);
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.commafeed.backend.cache;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.model.Category;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
|
||||
public class NoopCacheService extends CacheService {
|
||||
|
||||
@Override
|
||||
public Set<String> getLastEntries(Feed feed) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastEntries(Feed feed, List<String> entries) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnreadCount getUnreadCount(FeedSubscription sub) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUnreadCount(FeedSubscription sub, UnreadCount count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateUnreadCount(FeedSubscription... subs) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Category getUserRootCategory(User user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserRootCategory(User user, Category category) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateUserRootCategory(User... users) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package com.commafeed.backend.cache;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.model.Category;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.Pipeline;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RedisCacheService extends CacheService {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
private final JedisPool pool;
|
||||
|
||||
@Override
|
||||
public Set<String> getLastEntries(Feed feed) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisEntryKey(feed);
|
||||
return jedis.smembers(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastEntries(Feed feed, List<String> entries) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisEntryKey(feed);
|
||||
|
||||
Pipeline pipe = jedis.pipelined();
|
||||
pipe.del(key);
|
||||
for (String entry : entries) {
|
||||
pipe.sadd(key, entry);
|
||||
}
|
||||
pipe.expire(key, (int) TimeUnit.DAYS.toSeconds(7));
|
||||
pipe.sync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Category getUserRootCategory(User user) {
|
||||
Category cat = null;
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisUserRootCategoryKey(user);
|
||||
String json = jedis.get(key);
|
||||
if (json != null) {
|
||||
cat = MAPPER.readValue(json, Category.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return cat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserRootCategory(User user, Category category) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisUserRootCategoryKey(user);
|
||||
|
||||
Pipeline pipe = jedis.pipelined();
|
||||
pipe.del(key);
|
||||
pipe.set(key, MAPPER.writeValueAsString(category));
|
||||
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
|
||||
pipe.sync();
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnreadCount getUnreadCount(FeedSubscription sub) {
|
||||
UnreadCount count = null;
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisUnreadCountKey(sub);
|
||||
String json = jedis.get(key);
|
||||
if (json != null) {
|
||||
count = MAPPER.readValue(json, UnreadCount.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUnreadCount(FeedSubscription sub, UnreadCount count) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisUnreadCountKey(sub);
|
||||
|
||||
Pipeline pipe = jedis.pipelined();
|
||||
pipe.del(key);
|
||||
pipe.set(key, MAPPER.writeValueAsString(count));
|
||||
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
|
||||
pipe.sync();
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateUserRootCategory(User... users) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
Pipeline pipe = jedis.pipelined();
|
||||
if (users != null) {
|
||||
for (User user : users) {
|
||||
String key = buildRedisUserRootCategoryKey(user);
|
||||
pipe.del(key);
|
||||
}
|
||||
}
|
||||
pipe.sync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateUnreadCount(FeedSubscription... subs) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
Pipeline pipe = jedis.pipelined();
|
||||
if (subs != null) {
|
||||
for (FeedSubscription sub : subs) {
|
||||
String key = buildRedisUnreadCountKey(sub);
|
||||
pipe.del(key);
|
||||
}
|
||||
}
|
||||
pipe.sync();
|
||||
}
|
||||
}
|
||||
|
||||
private String buildRedisEntryKey(Feed feed) {
|
||||
return "f:" + Models.getId(feed);
|
||||
}
|
||||
|
||||
private String buildRedisUserRootCategoryKey(User user) {
|
||||
return "c:" + Models.getId(user);
|
||||
}
|
||||
|
||||
private String buildRedisUnreadCountKey(FeedSubscription sub) {
|
||||
return "u:" + Models.getId(sub);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.commafeed.backend.cache;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Getter;
|
||||
import redis.clients.jedis.DefaultJedisClientConfig;
|
||||
import redis.clients.jedis.HostAndPort;
|
||||
import redis.clients.jedis.JedisClientConfig;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.Protocol;
|
||||
|
||||
@Getter
|
||||
public class RedisPoolFactory {
|
||||
|
||||
@JsonProperty
|
||||
private String host = "localhost";
|
||||
|
||||
@JsonProperty
|
||||
private int port = Protocol.DEFAULT_PORT;
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
@JsonProperty
|
||||
private int timeout = Protocol.DEFAULT_TIMEOUT;
|
||||
|
||||
@JsonProperty
|
||||
private int database = Protocol.DEFAULT_DATABASE;
|
||||
|
||||
@JsonProperty
|
||||
private int maxTotal = 500;
|
||||
|
||||
public JedisPool build() {
|
||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||
poolConfig.setMaxTotal(maxTotal);
|
||||
|
||||
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
|
||||
.user(username)
|
||||
.password(password)
|
||||
.timeoutMillis(timeout)
|
||||
.database(database)
|
||||
.build();
|
||||
|
||||
return new JedisPool(poolConfig, new HostAndPort(host, port), clientConfig);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,25 +3,22 @@ package com.commafeed.backend.dao;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.QFeedCategory;
|
||||
import com.commafeed.backend.model.QUser;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
||||
|
||||
private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
|
||||
|
||||
@Inject
|
||||
public FeedCategoryDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public FeedCategoryDAO(EntityManager entityManager) {
|
||||
super(entityManager, FeedCategory.class);
|
||||
}
|
||||
|
||||
public List<FeedCategory> findAll(User user) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.QFeed;
|
||||
@@ -12,8 +11,8 @@ import com.commafeed.backend.model.QFeedSubscription;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class FeedDAO extends GenericDAO<Feed> {
|
||||
@@ -21,9 +20,12 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
private static final QFeed FEED = QFeed.feed;
|
||||
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
|
||||
|
||||
@Inject
|
||||
public FeedDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public FeedDAO(EntityManager entityManager) {
|
||||
super(entityManager, Feed.class);
|
||||
}
|
||||
|
||||
public List<Feed> findByIds(List<Long> id) {
|
||||
return query().selectFrom(FEED).where(FEED.id.in(id)).fetch();
|
||||
}
|
||||
|
||||
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {
|
||||
|
||||
@@ -2,16 +2,12 @@ package com.commafeed.backend.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.commafeed.backend.model.QFeedEntry;
|
||||
import com.commafeed.backend.model.QFeedEntryContent;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
import com.querydsl.jpa.JPQLSubQuery;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||
@@ -19,9 +15,8 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
|
||||
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||
|
||||
@Inject
|
||||
public FeedEntryContentDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public FeedEntryContentDAO(EntityManager entityManager) {
|
||||
super(entityManager, FeedEntryContent.class);
|
||||
}
|
||||
|
||||
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
|
||||
@@ -29,9 +24,13 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||
}
|
||||
|
||||
public long deleteWithoutEntries(int max) {
|
||||
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)
|
||||
.leftJoin(ENTRY)
|
||||
.on(ENTRY.content.id.eq(CONTENT.id))
|
||||
.where(ENTRY.id.isNull())
|
||||
.limit(max)
|
||||
.fetch();
|
||||
return deleteQuery(CONTENT).where(CONTENT.id.in(ids)).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@ package com.commafeed.backend.dao;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.QFeedEntry;
|
||||
import com.querydsl.core.Tuple;
|
||||
import com.querydsl.core.types.dsl.NumberExpression;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -21,9 +19,8 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
|
||||
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||
|
||||
@Inject
|
||||
public FeedEntryDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public FeedEntryDAO(EntityManager entityManager) {
|
||||
super(entityManager, FeedEntry.class);
|
||||
}
|
||||
|
||||
public FeedEntry findExisting(String guidHash, Feed feed) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||
@@ -28,8 +27,8 @@ import com.querydsl.core.BooleanBuilder;
|
||||
import com.querydsl.core.Tuple;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
@@ -42,9 +41,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Inject
|
||||
public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
|
||||
super(sessionFactory);
|
||||
public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
|
||||
super(entityManager, FeedEntryStatus.class);
|
||||
this.feedEntryTagDAO = feedEntryTagDAO;
|
||||
this.config = config;
|
||||
}
|
||||
@@ -60,8 +58,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
*/
|
||||
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||
if (status == null) {
|
||||
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
boolean read = unreadThreshold != null && entry.getPublished().isBefore(unreadThreshold);
|
||||
Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
|
||||
boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold);
|
||||
status = new FeedEntryStatus(user, sub, entry);
|
||||
status.setRead(read);
|
||||
status.setMarkable(!read);
|
||||
@@ -84,6 +82,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
boolean includeContent) {
|
||||
JPAQuery<FeedEntryStatus> query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
|
||||
if (includeContent) {
|
||||
query.join(STATUS.entry).fetchJoin();
|
||||
query.join(STATUS.entry.content).fetchJoin();
|
||||
}
|
||||
|
||||
@@ -105,7 +104,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
query.limit(limit);
|
||||
}
|
||||
|
||||
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
|
||||
setTimeout(query, config.database().queryTimeout());
|
||||
|
||||
List<FeedEntryStatus> statuses = query.fetch();
|
||||
statuses.forEach(s -> s.setMarkable(true));
|
||||
@@ -179,7 +178,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
query.limit(limit);
|
||||
}
|
||||
|
||||
setTimeout(query, config.getApplicationSettings().getQueryTimeout());
|
||||
setTimeout(query, config.database().queryTimeout());
|
||||
|
||||
List<FeedEntryStatus> statuses = new ArrayList<>();
|
||||
List<Tuple> tuples = query.fetch();
|
||||
@@ -217,9 +216,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
or.or(STATUS.read.isNull());
|
||||
or.or(STATUS.read.isFalse());
|
||||
|
||||
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
if (unreadThreshold != null) {
|
||||
return or.and(ENTRY.published.goe(unreadThreshold));
|
||||
Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
|
||||
if (statusesInstantThreshold != null) {
|
||||
return or.and(ENTRY.published.goe(statusesInstantThreshold));
|
||||
} else {
|
||||
return or;
|
||||
}
|
||||
|
||||
@@ -4,24 +4,21 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.QFeedEntryTag;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
||||
|
||||
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
|
||||
|
||||
@Inject
|
||||
public FeedEntryTagDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public FeedEntryTagDAO(EntityManager entityManager) {
|
||||
super(entityManager, FeedEntryTag.class);
|
||||
}
|
||||
|
||||
public List<String> findByUser(User user) {
|
||||
|
||||
@@ -5,12 +5,11 @@ import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||
import org.hibernate.event.service.spi.EventListenerRegistry;
|
||||
import org.hibernate.event.spi.EventType;
|
||||
import org.hibernate.event.spi.PostCommitInsertEventListener;
|
||||
import org.hibernate.event.spi.PostInsertEvent;
|
||||
import org.hibernate.event.spi.PostInsertEventListener;
|
||||
import org.hibernate.persister.entity.EntityPersister;
|
||||
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
@@ -23,28 +22,28 @@ import com.commafeed.backend.model.User;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.querydsl.jpa.JPQLQuery;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
||||
|
||||
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
|
||||
|
||||
private final SessionFactory sessionFactory;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
@Inject
|
||||
public FeedSubscriptionDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
this.sessionFactory = sessionFactory;
|
||||
public FeedSubscriptionDAO(EntityManager entityManager) {
|
||||
super(entityManager, FeedSubscription.class);
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
public void onPostCommitInsert(Consumer<FeedSubscription> consumer) {
|
||||
sessionFactory.unwrap(SessionFactoryImplementor.class)
|
||||
entityManager.unwrap(SharedSessionContractImplementor.class)
|
||||
.getFactory()
|
||||
.getServiceRegistry()
|
||||
.getService(EventListenerRegistry.class)
|
||||
.getEventListenerGroup(EventType.POST_COMMIT_INSERT)
|
||||
.appendListener(new PostInsertEventListener() {
|
||||
.appendListener(new PostCommitInsertEventListener() {
|
||||
@Override
|
||||
public void onPostInsert(PostInsertEvent event) {
|
||||
if (event.getEntity() instanceof FeedSubscription s) {
|
||||
@@ -56,6 +55,11 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
||||
public boolean requiresPostCommitHandling(EntityPersister persister) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostInsertCommitFailed(PostInsertEvent event) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.jpa.SpecHints;
|
||||
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
@@ -12,45 +13,51 @@ import com.querydsl.jpa.impl.JPAQuery;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import com.querydsl.jpa.impl.JPAUpdateClause;
|
||||
|
||||
import io.dropwizard.hibernate.AbstractDAO;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
|
||||
@RequiredArgsConstructor
|
||||
public abstract class GenericDAO<T extends AbstractModel> {
|
||||
|
||||
protected GenericDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
}
|
||||
private final EntityManager entityManager;
|
||||
private final Class<T> entityClass;
|
||||
|
||||
protected JPAQueryFactory query() {
|
||||
return new JPAQueryFactory(currentSession());
|
||||
return new JPAQueryFactory(entityManager);
|
||||
}
|
||||
|
||||
protected JPAUpdateClause updateQuery(EntityPath<T> entityPath) {
|
||||
return new JPAUpdateClause(currentSession(), entityPath);
|
||||
return new JPAUpdateClause(entityManager, entityPath);
|
||||
}
|
||||
|
||||
protected JPADeleteClause deleteQuery(EntityPath<T> entityPath) {
|
||||
return new JPADeleteClause(currentSession(), entityPath);
|
||||
return new JPADeleteClause(entityManager, entityPath);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public void saveOrUpdate(T model) {
|
||||
persist(model);
|
||||
entityManager.unwrap(Session.class).saveOrUpdate(model);
|
||||
}
|
||||
|
||||
public void saveOrUpdate(Collection<T> models) {
|
||||
models.forEach(this::persist);
|
||||
models.forEach(this::saveOrUpdate);
|
||||
}
|
||||
|
||||
public void update(T model) {
|
||||
currentSession().merge(model);
|
||||
public void persist(T model) {
|
||||
entityManager.persist(model);
|
||||
}
|
||||
|
||||
public T merge(T model) {
|
||||
return entityManager.merge(model);
|
||||
}
|
||||
|
||||
public T findById(Long id) {
|
||||
return get(id);
|
||||
return entityManager.find(entityClass, id);
|
||||
}
|
||||
|
||||
public void delete(T object) {
|
||||
if (object != null) {
|
||||
currentSession().remove(object);
|
||||
entityManager.remove(object);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +66,9 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
|
||||
return objects.size();
|
||||
}
|
||||
|
||||
protected void setTimeout(JPAQuery<?> query, int timeoutMs) {
|
||||
if (timeoutMs > 0) {
|
||||
query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, timeoutMs);
|
||||
protected void setTimeout(JPAQuery<?> query, Duration timeout) {
|
||||
if (!timeout.isZero()) {
|
||||
query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,20 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.Transaction;
|
||||
import org.hibernate.context.internal.ManagedSessionContext;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import io.quarkus.narayana.jta.QuarkusTransaction;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Singleton
|
||||
public class UnitOfWork {
|
||||
|
||||
private final SessionFactory sessionFactory;
|
||||
|
||||
public void run(SessionRunner sessionRunner) {
|
||||
public void run(SessionRunner runner) {
|
||||
call(() -> {
|
||||
sessionRunner.runInSession();
|
||||
runner.runInSession();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public <T> T call(SessionRunnerReturningValue<T> sessionRunner) {
|
||||
T t = null;
|
||||
|
||||
boolean sessionAlreadyBound = ManagedSessionContext.hasBind(sessionFactory);
|
||||
try (Session session = sessionFactory.openSession()) {
|
||||
if (!sessionAlreadyBound) {
|
||||
ManagedSessionContext.bind(session);
|
||||
}
|
||||
|
||||
Transaction tx = session.beginTransaction();
|
||||
try {
|
||||
t = sessionRunner.runInSession();
|
||||
commitTransaction(tx);
|
||||
} catch (Exception e) {
|
||||
rollbackTransaction(tx);
|
||||
UnitOfWork.rethrow(e);
|
||||
}
|
||||
} finally {
|
||||
if (!sessionAlreadyBound) {
|
||||
ManagedSessionContext.unbind(sessionFactory);
|
||||
}
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
private static void rollbackTransaction(Transaction tx) {
|
||||
if (tx != null && tx.isActive()) {
|
||||
tx.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
private static void commitTransaction(Transaction tx) {
|
||||
if (tx != null && tx.isActive()) {
|
||||
tx.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <E extends Exception> void rethrow(Exception e) throws E {
|
||||
throw (E) e;
|
||||
public <T> T call(SessionRunnerReturningValue<T> runner) {
|
||||
return QuarkusTransaction.joiningExisting().call(runner::runInSession);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.QUser;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class UserDAO extends GenericDAO<User> {
|
||||
|
||||
private static final QUser USER = QUser.user;
|
||||
|
||||
@Inject
|
||||
public UserDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public UserDAO(EntityManager entityManager) {
|
||||
super(entityManager, User.class);
|
||||
}
|
||||
|
||||
public User findByName(String name) {
|
||||
|
||||
@@ -4,24 +4,21 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.QUserRole;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class UserRoleDAO extends GenericDAO<UserRole> {
|
||||
|
||||
private static final QUserRole ROLE = QUserRole.userRole;
|
||||
|
||||
@Inject
|
||||
public UserRoleDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public UserRoleDAO(EntityManager entityManager) {
|
||||
super(entityManager, UserRole.class);
|
||||
}
|
||||
|
||||
public List<UserRole> findAll() {
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.QUserSettings;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
@Singleton
|
||||
public class UserSettingsDAO extends GenericDAO<UserSettings> {
|
||||
|
||||
private static final QUserSettings SETTINGS = QUserSettings.userSettings;
|
||||
|
||||
@Inject
|
||||
public UserSettingsDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
public UserSettingsDAO(EntityManager entityManager) {
|
||||
super(entityManager, UserSettings.class);
|
||||
}
|
||||
|
||||
public UserSettings findByUser(User user) {
|
||||
|
||||
@@ -12,8 +12,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public abstract class AbstractFaviconFetcher {
|
||||
|
||||
protected static final int TIMEOUT = 4000;
|
||||
|
||||
private static final List<String> ICON_MIMETYPE_BLACKLIST = Arrays.asList("application/xml", "text/html");
|
||||
private static final long MIN_ICON_LENGTH = 100;
|
||||
private static final long MAX_ICON_LENGTH = 100000;
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,8 +20,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Priority(Integer.MIN_VALUE)
|
||||
public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
||||
|
||||
private final HttpGetter getter;
|
||||
@@ -68,7 +69,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
||||
try {
|
||||
url = FeedUtils.removeTrailingSlash(url) + "/favicon.ico";
|
||||
log.debug("getting root icon at {}", url);
|
||||
HttpResult result = getter.getBinary(url, TIMEOUT);
|
||||
HttpResult result = getter.getBinary(url);
|
||||
bytes = result.getContent();
|
||||
contentType = result.getContentType();
|
||||
} catch (Exception e) {
|
||||
@@ -86,7 +87,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
||||
|
||||
Document doc;
|
||||
try {
|
||||
HttpResult result = getter.getBinary(url, TIMEOUT);
|
||||
HttpResult result = getter.getBinary(url);
|
||||
doc = Jsoup.parse(new String(result.getContent()), url);
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to retrieve page to find icon");
|
||||
@@ -112,7 +113,7 @@ public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
|
||||
byte[] bytes;
|
||||
String contentType;
|
||||
try {
|
||||
HttpResult result = getter.getBinary(href, TIMEOUT);
|
||||
HttpResult result = getter.getBinary(href);
|
||||
bytes = result.getContent();
|
||||
contentType = result.getContentType();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -11,13 +11,12 @@ import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FacebookFaviconFetcher extends AbstractFaviconFetcher {
|
||||
|
||||
@@ -44,7 +43,7 @@ public class FacebookFaviconFetcher extends AbstractFaviconFetcher {
|
||||
try {
|
||||
log.debug("Getting Facebook user's icon, {}", url);
|
||||
|
||||
HttpResult iconResult = getter.getBinary(iconUrl, TIMEOUT);
|
||||
HttpResult iconResult = getter.getBinary(iconUrl);
|
||||
bytes = iconResult.getContent();
|
||||
contentType = iconResult.getContentType();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -22,13 +22,12 @@ import com.google.api.services.youtube.model.ChannelListResponse;
|
||||
import com.google.api.services.youtube.model.PlaylistListResponse;
|
||||
import com.google.api.services.youtube.model.Thumbnail;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
|
||||
@@ -43,8 +42,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey();
|
||||
if (googleAuthKey == null) {
|
||||
Optional<String> googleAuthKey = config.googleAuthKey();
|
||||
if (googleAuthKey.isEmpty()) {
|
||||
log.debug("no google auth key configured");
|
||||
return null;
|
||||
}
|
||||
@@ -63,13 +62,13 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
ChannelListResponse response = null;
|
||||
if (userId.isPresent()) {
|
||||
log.debug("contacting youtube api for user {}", userId.get().getValue());
|
||||
response = fetchForUser(youtube, googleAuthKey, userId.get().getValue());
|
||||
response = fetchForUser(youtube, googleAuthKey.get(), userId.get().getValue());
|
||||
} else if (channelId.isPresent()) {
|
||||
log.debug("contacting youtube api for channel {}", channelId.get().getValue());
|
||||
response = fetchForChannel(youtube, googleAuthKey, channelId.get().getValue());
|
||||
response = fetchForChannel(youtube, googleAuthKey.get(), channelId.get().getValue());
|
||||
} else if (playlistId.isPresent()) {
|
||||
log.debug("contacting youtube api for playlist {}", playlistId.get().getValue());
|
||||
response = fetchForPlaylist(youtube, googleAuthKey, playlistId.get().getValue());
|
||||
response = fetchForPlaylist(youtube, googleAuthKey.get(), playlistId.get().getValue());
|
||||
}
|
||||
|
||||
if (response == null || response.isEmpty() || CollectionUtils.isEmpty(response.getItems())) {
|
||||
@@ -81,7 +80,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
|
||||
Thumbnail thumbnail = channel.getSnippet().getThumbnails().getDefault();
|
||||
|
||||
log.debug("fetching favicon");
|
||||
HttpResult iconResult = getter.getBinary(thumbnail.getUrl(), TIMEOUT);
|
||||
HttpResult iconResult = getter.getBinary(thumbnail.getUrl());
|
||||
bytes = iconResult.getContent();
|
||||
contentType = iconResult.getContentType();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.backend.feed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
|
||||
@@ -15,30 +15,32 @@ import com.commafeed.backend.feed.parser.FeedParserResult;
|
||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import io.quarkus.arc.All;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Fetches a feed then parses it
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Singleton
|
||||
public class FeedFetcher {
|
||||
|
||||
private final FeedParser parser;
|
||||
private final HttpGetter getter;
|
||||
private final Set<FeedURLProvider> urlProviders;
|
||||
private final List<FeedURLProvider> urlProviders;
|
||||
|
||||
public FeedFetcher(FeedParser parser, HttpGetter getter, @All List<FeedURLProvider> urlProviders) {
|
||||
this.parser = parser;
|
||||
this.getter = getter;
|
||||
this.urlProviders = urlProviders;
|
||||
}
|
||||
|
||||
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
|
||||
Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException {
|
||||
log.debug("Fetching feed {}", feedUrl);
|
||||
|
||||
int timeout = 20000;
|
||||
|
||||
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout);
|
||||
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag);
|
||||
byte[] content = result.getContent();
|
||||
|
||||
FeedParserResult parserResult;
|
||||
@@ -50,7 +52,7 @@ public class FeedFetcher {
|
||||
if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) {
|
||||
feedUrl = extractedUrl;
|
||||
|
||||
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
|
||||
result = getter.getBinary(extractedUrl, lastModified, eTag);
|
||||
content = result.getContent();
|
||||
parserResult = parser.parse(result.getUrlAfterRedirect(), content);
|
||||
} else {
|
||||
@@ -83,11 +85,10 @@ public class FeedFetcher {
|
||||
etagHeaderValueChanged ? result.getETag() : null);
|
||||
}
|
||||
|
||||
return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash,
|
||||
result.getDuration());
|
||||
return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash);
|
||||
}
|
||||
|
||||
private static String extractFeedUrl(Set<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||
private static String extractFeedUrl(List<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||
for (FeedURLProvider urlProvider : urlProviders) {
|
||||
String feedUrl = urlProvider.get(url, urlContent);
|
||||
if (feedUrl != null) {
|
||||
@@ -99,7 +100,7 @@ public class FeedFetcher {
|
||||
}
|
||||
|
||||
public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader,
|
||||
String contentHash, long fetchDuration) {
|
||||
String contentHash) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingDeque;
|
||||
@@ -21,14 +20,12 @@ import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class FeedRefreshEngine implements Managed {
|
||||
public class FeedRefreshEngine {
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final FeedDAO feedDAO;
|
||||
@@ -45,7 +42,6 @@ public class FeedRefreshEngine implements Managed {
|
||||
private final ThreadPoolExecutor workerExecutor;
|
||||
private final ThreadPoolExecutor databaseUpdaterExecutor;
|
||||
|
||||
@Inject
|
||||
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
|
||||
CommaFeedConfiguration config, MetricRegistry metrics) {
|
||||
this.unitOfWork = unitOfWork;
|
||||
@@ -60,15 +56,14 @@ public class FeedRefreshEngine implements Managed {
|
||||
this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
|
||||
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
|
||||
this.refillExecutor = newDiscardingSingleThreadExecutorService();
|
||||
this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads());
|
||||
this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads());
|
||||
this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
|
||||
this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
|
||||
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
|
||||
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
startFeedProcessingLoop();
|
||||
startRefillLoop();
|
||||
@@ -165,22 +160,20 @@ public class FeedRefreshEngine implements Managed {
|
||||
|
||||
private List<Feed> getNextUpdatableFeeds(int max) {
|
||||
return unitOfWork.call(() -> {
|
||||
Instant lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
|
||||
? Instant.now().minus(Duration.ofDays(30))
|
||||
: null;
|
||||
Instant lastLoginThreshold = config.feedRefresh().userInactivityPeriod().isZero() ? null
|
||||
: Instant.now().minus(config.feedRefresh().userInactivityPeriod());
|
||||
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
|
||||
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
|
||||
Instant nextUpdateDate = Instant.now().plus(Duration.ofMinutes(config.getApplicationSettings().getRefreshIntervalMinutes()));
|
||||
Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval());
|
||||
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
|
||||
return feeds;
|
||||
});
|
||||
}
|
||||
|
||||
private int getBatchSize() {
|
||||
return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
|
||||
return Math.min(100, 3 * config.feedRefresh().httpThreads());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.feedProcessingLoopExecutor.shutdownNow();
|
||||
this.refillLoopExecutor.shutdownNow();
|
||||
|
||||
@@ -6,24 +6,22 @@ import java.time.temporal.ChronoUnit;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class FeedRefreshIntervalCalculator {
|
||||
|
||||
private final boolean heavyLoad;
|
||||
private final int refreshIntervalMinutes;
|
||||
private final Duration refreshInterval;
|
||||
private final boolean empiricalInterval;
|
||||
|
||||
@Inject
|
||||
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {
|
||||
this.heavyLoad = config.getApplicationSettings().getHeavyLoad();
|
||||
this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes();
|
||||
this.refreshInterval = config.feedRefresh().interval();
|
||||
this.empiricalInterval = config.feedRefresh().intervalEmpirical();
|
||||
}
|
||||
|
||||
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) {
|
||||
Instant defaultRefreshInterval = getDefaultRefreshInterval();
|
||||
return heavyLoad ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval)
|
||||
return empiricalInterval ? computeEmpiricalRefreshInterval(publishedDate, averageEntryInterval, defaultRefreshInterval)
|
||||
: defaultRefreshInterval;
|
||||
}
|
||||
|
||||
@@ -33,7 +31,7 @@ public class FeedRefreshIntervalCalculator {
|
||||
|
||||
public Instant onFetchError(int errorCount) {
|
||||
int retriesBeforeDisable = 3;
|
||||
if (errorCount < retriesBeforeDisable || !heavyLoad) {
|
||||
if (errorCount < retriesBeforeDisable || !empiricalInterval) {
|
||||
return getDefaultRefreshInterval();
|
||||
}
|
||||
|
||||
@@ -42,10 +40,10 @@ public class FeedRefreshIntervalCalculator {
|
||||
}
|
||||
|
||||
private Instant getDefaultRefreshInterval() {
|
||||
return Instant.now().plus(Duration.ofMinutes(refreshIntervalMinutes));
|
||||
return Instant.now().plus(refreshInterval);
|
||||
}
|
||||
|
||||
private Instant computeRefreshIntervalForHeavyLoad(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) {
|
||||
private Instant computeEmpiricalRefreshInterval(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) {
|
||||
Instant now = Instant.now();
|
||||
|
||||
if (publishedDate == null) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -16,7 +15,6 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.Digests;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Content;
|
||||
@@ -25,14 +23,12 @@ import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.FeedService;
|
||||
import com.commafeed.frontend.ws.WebSocketMessageBuilder;
|
||||
import com.commafeed.frontend.ws.WebSocketSessions;
|
||||
import com.google.common.util.concurrent.Striped;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -48,30 +44,23 @@ public class FeedRefreshUpdater {
|
||||
private final FeedService feedService;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final CacheService cache;
|
||||
private final WebSocketSessions webSocketSessions;
|
||||
|
||||
private final Striped<Lock> locks;
|
||||
|
||||
private final Meter entryCacheMiss;
|
||||
private final Meter entryCacheHit;
|
||||
private final Meter feedUpdated;
|
||||
private final Meter entryInserted;
|
||||
|
||||
@Inject
|
||||
public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics,
|
||||
FeedSubscriptionDAO feedSubscriptionDAO, CacheService cache, WebSocketSessions webSocketSessions) {
|
||||
FeedSubscriptionDAO feedSubscriptionDAO, WebSocketSessions webSocketSessions) {
|
||||
this.unitOfWork = unitOfWork;
|
||||
this.feedService = feedService;
|
||||
this.feedEntryService = feedEntryService;
|
||||
this.feedSubscriptionDAO = feedSubscriptionDAO;
|
||||
this.cache = cache;
|
||||
this.webSocketSessions = webSocketSessions;
|
||||
|
||||
locks = Striped.lazyWeakLock(100000);
|
||||
|
||||
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
|
||||
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
|
||||
feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
|
||||
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
||||
}
|
||||
@@ -141,39 +130,21 @@ public class FeedRefreshUpdater {
|
||||
Map<FeedSubscription, Long> unreadCountBySubscription = new HashMap<>();
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
Set<String> lastEntries = cache.getLastEntries(feed);
|
||||
List<String> currentEntries = new ArrayList<>();
|
||||
|
||||
List<FeedSubscription> subscriptions = null;
|
||||
for (Entry entry : entries) {
|
||||
String cacheKey = cache.buildUniqueEntryKey(entry);
|
||||
if (!lastEntries.contains(cacheKey)) {
|
||||
log.debug("cache miss for {}", entry.url());
|
||||
if (subscriptions == null) {
|
||||
subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
|
||||
}
|
||||
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
||||
processed &= addEntryResult.processed;
|
||||
inserted += addEntryResult.inserted ? 1 : 0;
|
||||
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum));
|
||||
|
||||
entryCacheMiss.mark();
|
||||
} else {
|
||||
log.debug("cache hit for {}", entry.url());
|
||||
entryCacheHit.mark();
|
||||
if (subscriptions == null) {
|
||||
subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
|
||||
}
|
||||
|
||||
currentEntries.add(cacheKey);
|
||||
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
||||
processed &= addEntryResult.processed;
|
||||
inserted += addEntryResult.inserted ? 1 : 0;
|
||||
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum));
|
||||
}
|
||||
cache.setLastEntries(feed, currentEntries);
|
||||
|
||||
if (subscriptions == null) {
|
||||
if (inserted == 0) {
|
||||
feed.setMessage("No new entries found");
|
||||
} else if (inserted > 0) {
|
||||
log.debug("inserted {} entries for feed {}", inserted, feed.getId());
|
||||
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).toList();
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
||||
feed.setMessage("Found %s new entries".formatted(inserted));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +157,7 @@ public class FeedRefreshUpdater {
|
||||
feedUpdated.mark();
|
||||
}
|
||||
|
||||
unitOfWork.run(() -> feedService.save(feed));
|
||||
unitOfWork.run(() -> feedService.update(feed));
|
||||
|
||||
notifyOverWebsocket(unreadCountBySubscription);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -32,7 +31,6 @@ public class FeedRefreshWorker {
|
||||
private final CommaFeedConfiguration config;
|
||||
private final Meter feedFetched;
|
||||
|
||||
@Inject
|
||||
public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config,
|
||||
MetricRegistry metrics) {
|
||||
this.refreshIntervalCalculator = refreshIntervalCalculator;
|
||||
@@ -51,14 +49,14 @@ public class FeedRefreshWorker {
|
||||
|
||||
List<Entry> entries = result.feed().entries();
|
||||
|
||||
Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
|
||||
int maxFeedCapacity = config.database().cleanup().maxFeedCapacity();
|
||||
if (maxFeedCapacity > 0) {
|
||||
entries = entries.stream().limit(maxFeedCapacity).toList();
|
||||
}
|
||||
|
||||
Integer maxEntriesAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays();
|
||||
if (maxEntriesAgeDays > 0) {
|
||||
Instant threshold = Instant.now().minus(Duration.ofDays(maxEntriesAgeDays));
|
||||
Duration maxEntriesAgeDays = config.database().cleanup().entriesMaxAge();
|
||||
if (!maxEntriesAgeDays.isZero()) {
|
||||
Instant threshold = Instant.now().minus(maxEntriesAgeDays);
|
||||
entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,14 +38,13 @@ import com.rometools.rome.feed.synd.SyndLinkImpl;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
import com.rometools.rome.io.SyndFeedInput;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Parses raw xml into a FeedParserResult object
|
||||
*/
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FeedParser {
|
||||
|
||||
|
||||
@@ -15,11 +15,10 @@ import com.rometools.opml.feed.opml.Attribute;
|
||||
import com.rometools.opml.feed.opml.Opml;
|
||||
import com.rometools.opml.feed.opml.Outline;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class OPMLExporter {
|
||||
|
||||
@@ -28,7 +27,7 @@ public class OPMLExporter {
|
||||
|
||||
public Opml export(User user) {
|
||||
Opml opml = new Opml();
|
||||
opml.setFeedType("opml_1.1");
|
||||
opml.setFeedType("opml_1.0");
|
||||
opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName()));
|
||||
opml.setCreated(new Date());
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.util.List;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
@@ -17,19 +16,17 @@ import com.rometools.opml.feed.opml.Outline;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
import com.rometools.rome.io.WireFeedInput;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class OPMLImporter {
|
||||
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final CacheService cache;
|
||||
|
||||
public void importOpml(User user, String xml) throws IllegalArgumentException, FeedException {
|
||||
xml = xml.substring(xml.indexOf('<'));
|
||||
@@ -80,6 +77,5 @@ public class OPMLImporter {
|
||||
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
|
||||
}
|
||||
}
|
||||
cache.invalidateUserRootCategory(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import org.jdom2.Element;
|
||||
|
||||
import com.rometools.opml.feed.opml.Opml;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
/**
|
||||
* Add missing title to the generated OPML
|
||||
*
|
||||
*/
|
||||
@RegisterForReflection
|
||||
public class OPML11Generator extends com.rometools.opml.io.impl.OPML10Generator {
|
||||
|
||||
public OPML11Generator() {
|
||||
|
||||
@@ -9,10 +9,13 @@ import com.rometools.opml.io.impl.OPML10Parser;
|
||||
import com.rometools.rome.feed.WireFeed;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
/**
|
||||
* Support for OPML 1.1 parsing
|
||||
*
|
||||
*/
|
||||
@RegisterForReflection
|
||||
public class OPML11Parser extends OPML10Parser {
|
||||
|
||||
public OPML11Parser() {
|
||||
|
||||
@@ -6,10 +6,13 @@ import com.rometools.rome.feed.synd.SyndContentImpl;
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.impl.ConverterForRSS090;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
/**
|
||||
* Support description tag for RSS09
|
||||
*
|
||||
*/
|
||||
@RegisterForReflection
|
||||
public class RSS090DescriptionConverter extends ConverterForRSS090 {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,10 +8,13 @@ import com.rometools.rome.feed.rss.Description;
|
||||
import com.rometools.rome.feed.rss.Item;
|
||||
import com.rometools.rome.io.impl.RSS090Parser;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
/**
|
||||
* Support description tag for RSS09
|
||||
*
|
||||
*/
|
||||
@RegisterForReflection
|
||||
public class RSS090DescriptionParser extends RSS090Parser {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,9 @@ import org.jdom2.Namespace;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.rometools.rome.io.impl.RSS10Parser;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
public class RSSRDF10Parser extends RSS10Parser {
|
||||
|
||||
private static final String RSS_URI = "http://purl.org/rss/1.0/";
|
||||
|
||||
@@ -20,13 +20,13 @@ import org.w3c.css.sac.InputSource;
|
||||
import org.w3c.dom.css.CSSStyleDeclaration;
|
||||
|
||||
import com.steadystate.css.parser.CSSOMParser;
|
||||
import com.steadystate.css.parser.SACParserCSS21;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class FeedEntryContentCleaningService {
|
||||
@@ -150,7 +150,7 @@ public class FeedEntryContentCleaningService {
|
||||
}
|
||||
|
||||
private CSSOMParser buildCssParser() {
|
||||
CSSOMParser parser = new CSSOMParser();
|
||||
CSSOMParser parser = new CSSOMParser(new SACParserCSS21());
|
||||
|
||||
parser.setErrorHandler(new ErrorHandler() {
|
||||
@Override
|
||||
|
||||
@@ -12,11 +12,10 @@ import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Media;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FeedEntryContentService {
|
||||
|
||||
|
||||
@@ -23,19 +23,20 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FeedEntryFilteringService {
|
||||
|
||||
private static final JexlEngine ENGINE = initEngine();
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
private static JexlEngine initEngine() {
|
||||
// classloader that prevents object creation
|
||||
@@ -97,8 +98,9 @@ public class FeedEntryFilteringService {
|
||||
Future<Object> future = executor.submit(callable);
|
||||
Object result;
|
||||
try {
|
||||
result = future.get(500, TimeUnit.MILLISECONDS);
|
||||
result = future.get(config.feedRefresh().filteringExpressionEvaluationTimeout().toMillis(), TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new FeedEntryFilterException("interrupted while evaluating expression " + filter, e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new FeedEntryFilterException("Exception while evaluating expression " + filter, e);
|
||||
|
||||
@@ -4,7 +4,6 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import com.commafeed.backend.Digests;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
@@ -18,13 +17,12 @@ import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FeedEntryService {
|
||||
|
||||
@@ -33,7 +31,6 @@ public class FeedEntryService {
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedEntryContentService feedEntryContentService;
|
||||
private final FeedEntryFilteringService feedEntryFilteringService;
|
||||
private final CacheService cache;
|
||||
|
||||
public FeedEntry find(Feed feed, Entry entry) {
|
||||
String guidHash = Digests.sha1Hex(entry.guid());
|
||||
@@ -86,8 +83,6 @@ public class FeedEntryService {
|
||||
if (status.isMarkable()) {
|
||||
status.setRead(read);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
cache.invalidateUnreadCount(sub);
|
||||
cache.invalidateUserRootCategory(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +108,6 @@ public class FeedEntryService {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
|
||||
false, null, null, null);
|
||||
markList(statuses, olderThan, insertedBefore);
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(user);
|
||||
}
|
||||
|
||||
public void markStarredEntries(User user, Instant olderThan, Instant insertedBefore) {
|
||||
|
||||
@@ -10,11 +10,10 @@ import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class FeedEntryTagService {
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.commafeed.backend.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import com.commafeed.backend.Digests;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
@@ -14,19 +14,18 @@ import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.google.common.io.Resources;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import io.quarkus.arc.All;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class FeedService {
|
||||
|
||||
private final FeedDAO feedDAO;
|
||||
private final Set<AbstractFaviconFetcher> faviconFetchers;
|
||||
private final List<AbstractFaviconFetcher> faviconFetchers;
|
||||
|
||||
private final Favicon defaultFavicon;
|
||||
|
||||
@Inject
|
||||
public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) {
|
||||
public FeedService(FeedDAO feedDAO, @All List<AbstractFaviconFetcher> faviconFetchers) {
|
||||
this.feedDAO = feedDAO;
|
||||
this.faviconFetchers = faviconFetchers;
|
||||
|
||||
@@ -48,18 +47,18 @@ public class FeedService {
|
||||
feed.setNormalizedUrl(normalizedUrl);
|
||||
feed.setNormalizedUrlHash(normalizedUrlHash);
|
||||
feed.setDisabledUntil(Models.MINIMUM_INSTANT);
|
||||
feedDAO.saveOrUpdate(feed);
|
||||
feedDAO.persist(feed);
|
||||
}
|
||||
return feed;
|
||||
}
|
||||
|
||||
public void save(Feed feed) {
|
||||
public void update(Feed feed) {
|
||||
String normalized = FeedUtils.normalizeURL(feed.getUrl());
|
||||
feed.setNormalizedUrl(normalized);
|
||||
feed.setNormalizedUrlHash(Digests.sha1Hex(normalized));
|
||||
feed.setLastUpdated(Instant.now());
|
||||
feed.setEtagHeader(FeedUtils.truncate(feed.getEtagHeader(), 255));
|
||||
feedDAO.saveOrUpdate(feed);
|
||||
feedDAO.merge(feed);
|
||||
}
|
||||
|
||||
public Favicon fetchFavicon(Feed feed) {
|
||||
|
||||
@@ -5,10 +5,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
@@ -17,11 +14,9 @@ import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -34,18 +29,15 @@ public class FeedSubscriptionService {
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedService feedService;
|
||||
private final FeedRefreshEngine feedRefreshEngine;
|
||||
private final CacheService cache;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Inject
|
||||
public FeedSubscriptionService(FeedDAO feedDAO, FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO,
|
||||
FeedService feedService, FeedRefreshEngine feedRefreshEngine, CacheService cache, CommaFeedConfiguration config) {
|
||||
FeedService feedService, FeedRefreshEngine feedRefreshEngine, CommaFeedConfiguration config) {
|
||||
this.feedDAO = feedDAO;
|
||||
this.feedEntryStatusDAO = feedEntryStatusDAO;
|
||||
this.feedSubscriptionDAO = feedSubscriptionDAO;
|
||||
this.feedService = feedService;
|
||||
this.feedRefreshEngine = feedRefreshEngine;
|
||||
this.cache = cache;
|
||||
this.config = config;
|
||||
|
||||
// automatically refresh feeds after they are subscribed to
|
||||
@@ -62,16 +54,7 @@ public class FeedSubscriptionService {
|
||||
}
|
||||
|
||||
public long subscribe(User user, String url, String title, FeedCategory category, int position) {
|
||||
|
||||
final String pubUrl = config.getApplicationSettings().getPublicUrl();
|
||||
if (StringUtils.isBlank(pubUrl)) {
|
||||
throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set");
|
||||
}
|
||||
if (url.startsWith(pubUrl)) {
|
||||
throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance");
|
||||
}
|
||||
|
||||
Integer maxFeedsPerUser = config.getApplicationSettings().getMaxFeedsPerUser();
|
||||
Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser();
|
||||
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) {
|
||||
String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)",
|
||||
maxFeedsPerUser);
|
||||
@@ -97,7 +80,6 @@ public class FeedSubscriptionService {
|
||||
sub.setTitle(FeedUtils.truncate(title, 128));
|
||||
feedSubscriptionDAO.saveOrUpdate(sub);
|
||||
|
||||
cache.invalidateUserRootCategory(user);
|
||||
return sub.getId();
|
||||
}
|
||||
|
||||
@@ -105,7 +87,6 @@ public class FeedSubscriptionService {
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, subId);
|
||||
if (sub != null) {
|
||||
feedSubscriptionDAO.delete(sub);
|
||||
cache.invalidateUserRootCategory(user);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -132,17 +113,9 @@ public class FeedSubscriptionService {
|
||||
}
|
||||
|
||||
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
||||
return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, this::getUnreadCount));
|
||||
}
|
||||
|
||||
private UnreadCount getUnreadCount(FeedSubscription sub) {
|
||||
UnreadCount count = cache.getUnreadCount(sub);
|
||||
if (count == null) {
|
||||
log.debug("unread count cache miss for {}", Models.getId(sub));
|
||||
count = feedEntryStatusDAO.getUnreadCount(sub);
|
||||
cache.setUnreadCount(sub, count);
|
||||
}
|
||||
return count;
|
||||
return feedSubscriptionDAO.findAll(user)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount));
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
|
||||
@@ -1,63 +1,20 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import io.quarkus.mailer.Mail;
|
||||
import io.quarkus.mailer.Mailer;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.mail.Authenticator;
|
||||
import jakarta.mail.Message;
|
||||
import jakarta.mail.PasswordAuthentication;
|
||||
import jakarta.mail.Session;
|
||||
import jakarta.mail.Transport;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Mailing service
|
||||
*
|
||||
*/
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class MailService {
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public void sendMail(User user, String subject, String content) throws Exception {
|
||||
|
||||
ApplicationSettings settings = config.getApplicationSettings();
|
||||
|
||||
final String username = settings.getSmtpUserName();
|
||||
final String password = settings.getSmtpPassword();
|
||||
final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName());
|
||||
|
||||
String dest = user.getEmail();
|
||||
|
||||
Properties props = new Properties();
|
||||
props.put("mail.smtp.auth", "true");
|
||||
props.put("mail.smtp.starttls.enable", String.valueOf(settings.isSmtpTls()));
|
||||
props.put("mail.smtp.host", settings.getSmtpHost());
|
||||
props.put("mail.smtp.port", String.valueOf(settings.getSmtpPort()));
|
||||
|
||||
Session session = Session.getInstance(props, new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
|
||||
Message message = new MimeMessage(session);
|
||||
message.setFrom(new InternetAddress(fromAddress, "CommaFeed"));
|
||||
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
|
||||
message.setSubject("CommaFeed - " + subject);
|
||||
message.setContent(content, "text/html; charset=utf-8");
|
||||
|
||||
Transport.send(message);
|
||||
private final Mailer mailer;
|
||||
|
||||
public void sendMail(User user, String subject, String content) {
|
||||
Mail mail = Mail.withHtml(user.getEmail(), "CommaFeed - " + subject, content);
|
||||
mailer.send(mail);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,7 +19,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
// taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
|
||||
@SuppressWarnings("serial")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class PasswordEncryptionService implements Serializable {
|
||||
|
||||
|
||||
@@ -24,11 +24,10 @@ import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.internal.PostLoginActivities;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class UserService {
|
||||
|
||||
@@ -117,8 +116,7 @@ public class UserService {
|
||||
public User register(String name, String password, String email, Collection<Role> roles, boolean forceRegistration) {
|
||||
|
||||
if (!forceRegistration) {
|
||||
Preconditions.checkState(config.getApplicationSettings().getAllowRegistrations(),
|
||||
"Registrations are closed on this CommaFeed instance");
|
||||
Preconditions.checkState(config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance");
|
||||
}
|
||||
|
||||
Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
|
||||
|
||||
@@ -12,9 +12,9 @@ import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -35,7 +35,6 @@ public class DatabaseCleaningService {
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final Meter entriesDeletedMeter;
|
||||
|
||||
@Inject
|
||||
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
|
||||
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) {
|
||||
this.unitOfWork = unitOfWork;
|
||||
@@ -43,7 +42,7 @@ public class DatabaseCleaningService {
|
||||
this.feedEntryDAO = feedEntryDAO;
|
||||
this.feedEntryContentDAO = feedEntryContentDAO;
|
||||
this.feedEntryStatusDAO = feedEntryStatusDAO;
|
||||
this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize();
|
||||
this.batchSize = config.database().cleanup().batchSize();
|
||||
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
|
||||
}
|
||||
|
||||
@@ -63,7 +62,7 @@ public class DatabaseCleaningService {
|
||||
log.info("removed {} entries for feeds without subscriptions", entriesTotal);
|
||||
} while (entriesDeleted > 0);
|
||||
}
|
||||
deleted = unitOfWork.call(() -> feedDAO.delete(feeds));
|
||||
deleted = unitOfWork.call(() -> feedDAO.delete(feedDAO.findByIds(feeds.stream().map(AbstractModel::getId).toList())));
|
||||
total += deleted;
|
||||
log.info("removed {} feeds without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
|
||||
@@ -1,109 +1,41 @@
|
||||
package com.commafeed.backend.service.db;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.kohsuke.MetaInfServices;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import liquibase.Scope;
|
||||
import liquibase.UpdateSummaryEnum;
|
||||
import liquibase.changelog.ChangeLogParameters;
|
||||
import liquibase.command.CommandScope;
|
||||
import liquibase.command.core.UpdateCommandStep;
|
||||
import liquibase.command.core.helpers.DatabaseChangelogCommandStep;
|
||||
import liquibase.command.core.helpers.DbUrlConnectionArgumentsCommandStep;
|
||||
import liquibase.command.core.helpers.ShowSummaryArgument;
|
||||
import liquibase.database.Database;
|
||||
import liquibase.database.DatabaseFactory;
|
||||
import liquibase.database.core.PostgresDatabase;
|
||||
import liquibase.database.jvm.JdbcConnection;
|
||||
import liquibase.exception.DatabaseException;
|
||||
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||
import liquibase.structure.DatabaseObject;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class DatabaseStartupService implements Managed {
|
||||
public class DatabaseStartupService {
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final SessionFactory sessionFactory;
|
||||
private final UserDAO userDAO;
|
||||
private final UserService userService;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
updateSchema();
|
||||
public void populateInitialData() {
|
||||
long count = unitOfWork.call(userDAO::count);
|
||||
if (count == 0) {
|
||||
unitOfWork.run(this::initialData);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSchema() {
|
||||
log.info("checking if database schema needs updating");
|
||||
|
||||
try (Session session = sessionFactory.openSession()) {
|
||||
session.doWork(connection -> {
|
||||
try {
|
||||
JdbcConnection jdbcConnection = new JdbcConnection(connection);
|
||||
Database database = getDatabase(jdbcConnection);
|
||||
|
||||
Map<String, Object> scopeObjects = new HashMap<>();
|
||||
scopeObjects.put(Scope.Attr.database.name(), database);
|
||||
scopeObjects.put(Scope.Attr.resourceAccessor.name(),
|
||||
new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader()));
|
||||
|
||||
Scope.child(scopeObjects, () -> {
|
||||
CommandScope command = new CommandScope(UpdateCommandStep.COMMAND_NAME);
|
||||
command.addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, database);
|
||||
command.addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, "migrations.xml");
|
||||
command.addArgumentValue(DatabaseChangelogCommandStep.CHANGELOG_PARAMETERS, new ChangeLogParameters(database));
|
||||
command.addArgumentValue(ShowSummaryArgument.SHOW_SUMMARY, UpdateSummaryEnum.OFF);
|
||||
command.execute();
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log.info("database schema is up to date");
|
||||
}
|
||||
|
||||
private Database getDatabase(JdbcConnection connection) throws DatabaseException {
|
||||
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);
|
||||
if (database instanceof PostgresDatabase) {
|
||||
database = new PostgresDatabase() {
|
||||
@Override
|
||||
public String escapeObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
|
||||
return objectName;
|
||||
}
|
||||
};
|
||||
database.setConnection(connection);
|
||||
}
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
private void initialData() {
|
||||
log.info("populating database with default values");
|
||||
try {
|
||||
userService.createAdminUser();
|
||||
if (config.getApplicationSettings().getCreateDemoAccount()) {
|
||||
if (config.users().createDemoAccount()) {
|
||||
userService.createDemoUser();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -111,4 +43,20 @@ public class DatabaseStartupService implements Managed {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a postgresql database in liquibase that doesn't escape columns, so that we can use lower case columns
|
||||
*/
|
||||
@MetaInfServices(Database.class)
|
||||
public static class LowerCaseColumnsPostgresDatabase extends PostgresDatabase {
|
||||
@Override
|
||||
public String escapeObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
|
||||
return objectName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return super.getPriority() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.commafeed.backend.service.db;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.manticore.h2.H2MigrationTool;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class H2MigrationService {
|
||||
|
||||
private static final String H2_FILE_SUFFIX = ".mv.db";
|
||||
|
||||
public void migrateIfNeeded(Path path, String user, String password) {
|
||||
if (Files.notExists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int format;
|
||||
try {
|
||||
format = getH2FileFormat(path);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("could not detect H2 format", e);
|
||||
}
|
||||
|
||||
if (format == 2) {
|
||||
try {
|
||||
migrate(path, user, password, "2.1.214", "2.2.224");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("could not migrate H2 to format 3", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getH2FileFormat(Path path) throws IOException {
|
||||
try (BufferedReader reader = Files.newBufferedReader(path)) {
|
||||
String headers = reader.readLine();
|
||||
|
||||
return Stream.of(headers.split(","))
|
||||
.filter(h -> h.startsWith("format:"))
|
||||
.map(h -> h.split(":")[1])
|
||||
.map(Integer::parseInt)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("could not find format in H2 file headers"));
|
||||
}
|
||||
}
|
||||
|
||||
private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception {
|
||||
log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion);
|
||||
|
||||
Path scriptPath = path.resolveSibling("script-%d.sql".formatted(System.currentTimeMillis()));
|
||||
Path newVersionPath = path.resolveSibling("%s.%s%s".formatted(StringUtils.removeEnd(path.getFileName().toString(), H2_FILE_SUFFIX),
|
||||
getPatchVersion(toVersion), H2_FILE_SUFFIX));
|
||||
Path oldVersionBackupPath = path.resolveSibling("%s.%s.backup".formatted(path.getFileName(), getPatchVersion(fromVersion)));
|
||||
|
||||
Files.deleteIfExists(scriptPath);
|
||||
Files.deleteIfExists(newVersionPath);
|
||||
Files.deleteIfExists(oldVersionBackupPath);
|
||||
|
||||
H2MigrationTool.readDriverRecords();
|
||||
new H2MigrationTool().migrate(fromVersion, toVersion, path.toAbsolutePath().toString(), user, password,
|
||||
scriptPath.toAbsolutePath().toString(), "", "", false, false, "");
|
||||
if (!Files.exists(newVersionPath)) {
|
||||
throw new RuntimeException("H2 migration failed, new version file not found");
|
||||
}
|
||||
|
||||
Files.move(path, oldVersionBackupPath);
|
||||
Files.move(newVersionPath, path);
|
||||
Files.delete(oldVersionBackupPath);
|
||||
Files.delete(scriptPath);
|
||||
|
||||
log.info("migrated H2 database from format {} to format {}", fromVersion, toVersion);
|
||||
}
|
||||
|
||||
private String getPatchVersion(String version) {
|
||||
return StringUtils.substringAfterLast(version, ".");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,11 +7,10 @@ import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class PostLoginActivities {
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class DemoAccountCleanupTask extends ScheduledTask {
|
||||
@@ -26,7 +25,7 @@ public class DemoAccountCleanupTask extends ScheduledTask {
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
if (!config.getApplicationSettings().getCreateDemoAccount()) {
|
||||
if (!config.users().createDemoAccount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ import java.util.concurrent.TimeUnit;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask {
|
||||
|
||||
@@ -18,7 +17,7 @@ public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
int maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
|
||||
int maxFeedCapacity = config.database().cleanup().maxFeedCapacity();
|
||||
if (maxFeedCapacity > 0) {
|
||||
cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ import java.util.concurrent.TimeUnit;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class OldEntriesCleanupTask extends ScheduledTask {
|
||||
|
||||
@@ -20,9 +19,9 @@ public class OldEntriesCleanupTask extends ScheduledTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
int maxAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays();
|
||||
if (maxAgeDays > 0) {
|
||||
Instant threshold = Instant.now().minus(Duration.ofDays(maxAgeDays));
|
||||
Duration entriesMaxAge = config.database().cleanup().entriesMaxAge();
|
||||
if (!entriesMaxAge.isZero()) {
|
||||
Instant threshold = Instant.now().minus(entriesMaxAge);
|
||||
cleaner.cleanEntriesOlderThan(threshold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ import java.util.concurrent.TimeUnit;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class OldStatusesCleanupTask extends ScheduledTask {
|
||||
|
||||
@@ -19,7 +18,7 @@ public class OldStatusesCleanupTask extends ScheduledTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Instant threshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
Instant threshold = config.database().cleanup().statusesInstantThreshold();
|
||||
if (threshold != null) {
|
||||
cleaner.cleanStatusesOlderThan(threshold);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class OrphanedContentsCleanupTask extends ScheduledTask {
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class OrphanedFeedsCleanupTask extends ScheduledTask {
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.commafeed.backend.task;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import io.quarkus.arc.All;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class TaskScheduler {
|
||||
|
||||
private final List<ScheduledTask> tasks;
|
||||
private final ScheduledExecutorService executor;
|
||||
|
||||
public TaskScheduler(@All List<ScheduledTask> tasks) {
|
||||
this.tasks = tasks;
|
||||
this.executor = Executors.newScheduledThreadPool(tasks.size());
|
||||
}
|
||||
|
||||
public void start() {
|
||||
tasks.forEach(task -> task.register(executor));
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class InPageReferenceFeedURLProvider implements FeedURLProvider {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,12 +3,15 @@ package com.commafeed.backend.urlprovider;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Workaround for Youtube channels
|
||||
*
|
||||
* converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL
|
||||
* https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
|
||||
*/
|
||||
@Singleton
|
||||
public class YoutubeFeedURLProvider implements FeedURLProvider {
|
||||
|
||||
private static final Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
|
||||
@Inherited
|
||||
@Target({ ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SecurityCheck {
|
||||
|
||||
/**
|
||||
* Roles needed.
|
||||
*/
|
||||
Role value() default Role.USER;
|
||||
|
||||
boolean apiKeyAllowed() default false;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.session.SessionHelper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityCheckFactory implements Function<ContainerRequest, User> {
|
||||
|
||||
private static final String PREFIX = "Basic";
|
||||
|
||||
private final UserDAO userDAO;
|
||||
private final UserService userService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final HttpServletRequest request;
|
||||
private final Role role;
|
||||
private final boolean apiKeyAllowed;
|
||||
|
||||
@Override
|
||||
public User apply(ContainerRequest req) {
|
||||
Optional<User> user = apiKeyLogin();
|
||||
if (user.isEmpty()) {
|
||||
user = basicAuthenticationLogin();
|
||||
}
|
||||
if (user.isEmpty()) {
|
||||
user = cookieSessionLogin(new SessionHelper(request));
|
||||
}
|
||||
|
||||
if (user.isPresent()) {
|
||||
Set<Role> roles = userService.getRoles(user.get());
|
||||
if (roles.contains(role)) {
|
||||
return user.get();
|
||||
} else {
|
||||
throw buildWebApplicationException(Response.Status.FORBIDDEN, "You don't have the required role to access this resource.");
|
||||
}
|
||||
} else {
|
||||
throw buildWebApplicationException(Response.Status.UNAUTHORIZED, "Credentials are required to access this resource.");
|
||||
}
|
||||
}
|
||||
|
||||
Optional<User> cookieSessionLogin(SessionHelper sessionHelper) {
|
||||
Optional<User> loggedInUser = sessionHelper.getLoggedInUserId().map(userDAO::findById);
|
||||
loggedInUser.ifPresent(userService::performPostLoginActivities);
|
||||
return loggedInUser;
|
||||
}
|
||||
|
||||
private Optional<User> basicAuthenticationLogin() {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null) {
|
||||
int space = header.indexOf(' ');
|
||||
if (space > 0) {
|
||||
String method = header.substring(0, space);
|
||||
if (PREFIX.equalsIgnoreCase(method)) {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(header.substring(space + 1));
|
||||
String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1);
|
||||
int i = decoded.indexOf(':');
|
||||
if (i > 0) {
|
||||
String username = decoded.substring(0, i);
|
||||
String password = decoded.substring(i + 1);
|
||||
return userService.login(username, password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<User> apiKeyLogin() {
|
||||
String apiKey = request.getParameter("apiKey");
|
||||
if (apiKey != null && apiKeyAllowed) {
|
||||
return userService.login(apiKey);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private WebApplicationException buildWebApplicationException(Response.Status status, String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("message", message);
|
||||
response.put("allowRegistrations", config.getApplicationSettings().getAllowRegistrations());
|
||||
return new WebApplicationException(Response.status(status).entity(response).type(MediaType.APPLICATION_JSON).build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.glassfish.hk2.utilities.binding.AbstractBinder;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider;
|
||||
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
|
||||
import org.glassfish.jersey.server.model.Parameter;
|
||||
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Singleton
|
||||
public class SecurityCheckFactoryProvider extends AbstractValueParamProvider {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserDAO userDAO;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@Inject
|
||||
public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserDAO userDAO,
|
||||
UserService userService, CommaFeedConfiguration config, HttpServletRequest request) {
|
||||
super(() -> extractorProvider, Parameter.Source.UNKNOWN);
|
||||
this.userDAO = userDAO;
|
||||
this.userService = userService;
|
||||
this.config = config;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<ContainerRequest, ?> createValueProvider(Parameter parameter) {
|
||||
final Class<?> classType = parameter.getRawType();
|
||||
|
||||
SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class);
|
||||
if (securityCheck == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!classType.isAssignableFrom(User.class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SecurityCheckFactory(userDAO, userService, config, request, securityCheck.value(), securityCheck.apiKeyAllowed());
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static class Binder extends AbstractBinder {
|
||||
|
||||
private final UserDAO userDAO;
|
||||
private final UserService userService;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SecurityCheckFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class);
|
||||
bind(userDAO).to(UserDAO.class);
|
||||
bind(userService).to(UserService.class);
|
||||
bind(config).to(CommaFeedConfiguration.class);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -11,6 +12,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Entry details")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Category implements Serializable {
|
||||
|
||||
@Schema(description = "category id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -11,6 +12,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "List of entries with some metadata")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Entries implements Serializable {
|
||||
|
||||
@Schema(description = "name of the feed or the category requested", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.rometools.rome.feed.synd.SyndEnclosureImpl;
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndEntryImpl;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -26,6 +27,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Entry details")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Entry implements Serializable {
|
||||
|
||||
@Schema(description = "entry id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -9,6 +10,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Feed details")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class FeedInfo implements Serializable {
|
||||
|
||||
@Schema(description = "url", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -9,6 +10,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Server infos")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class ServerInfo implements Serializable {
|
||||
|
||||
@Schema
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -9,6 +10,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User settings")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Settings implements Serializable {
|
||||
|
||||
@Schema(description = "user's preferred language, english if none", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -15,6 +16,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User information")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Subscription implements Serializable {
|
||||
|
||||
@Schema(description = "subscription id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -3,12 +3,14 @@ package com.commafeed.frontend.model;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Unread count")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UnreadCount implements Serializable {
|
||||
|
||||
@Schema
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.commafeed.frontend.model;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -10,6 +11,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User information")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UserModel implements Serializable {
|
||||
|
||||
@Schema(description = "user id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Schema
|
||||
public class LoginRequest implements Serializable {
|
||||
|
||||
@Schema(description = "username", requiredMode = RequiredMode.REQUIRED)
|
||||
@Size(min = 3, max = 32)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "password", requiredMode = RequiredMode.REQUIRED)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String password;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.frontend.auth.ValidPassword;
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.frontend.auth.ValidPassword;
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user