diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 06d3ded3..d2ba3b56 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,115 +7,39 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- java: [ "17", "21" ]
+ database: [ "h2", "postgresql", "mysql", "mariadb" ]
steps:
+ # Checkout
+ - name: Configure git to checkout as-is
+ run: git config --global core.autocrlf false
+
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Set up Java
- uses: actions/setup-java@v4
+ - name: Set up GraalVM
+ uses: graalvm/setup-graalvm@v1
with:
- java-version: ${{ matrix.java }}
- distribution: "temurin"
+ java-version: "21"
+ distribution: "graalvm"
cache: "maven"
# Build & Test
- name: Build with Maven
- run: mvn --batch-mode --no-transfer-progress install
- env:
- TEST_DATABASE: h2
-
- - name: Run integration tests on PostgreSQL
- run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
- env:
- TEST_DATABASE: postgresql
-
- - name: Run integration tests on MySQL
- run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
- env:
- TEST_DATABASE: mysql
-
- - name: Run integration tests on MariaDB
- run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
- env:
- TEST_DATABASE: mariadb
-
- - name: Run integration tests with Redis cache enabled
- run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
- env:
- TEST_DATABASE: h2
- REDIS: true
+ run: mvn --batch-mode --no-transfer-progress install -Pnative -D"quarkus.datasource.db-kind"=${{ matrix.database }}
# Upload artifacts
- - name: Upload JAR
- uses: actions/upload-artifact@v4
- if: ${{ matrix.java == '17' }}
- with:
- name: commafeed.jar
- path: commafeed-server/target/commafeed.jar
-
- - name: Upload Playwright artifacts
- if: failure()
+ - name: Upload cross-platform app
uses: actions/upload-artifact@v4
with:
- name: playwright-artifacts
- path: |
- **/target/playwright-artifacts/
+ name: commafeed-${{ matrix.database }}-jvm
+ path: commafeed-server/target/commafeed-*.zip
- # Docker
- - name: Login to Container Registry
- uses: docker/login-action@v3
- if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
+ - name: Upload native executable
+ uses: actions/upload-artifact@v4
with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Docker build and push tag
- uses: docker/build-push-action@v6
- if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
- with:
- context: .
- push: true
- platforms: linux/amd64,linux/arm64/v8
- tags: |
- athou/commafeed:latest
- athou/commafeed:${{ github.ref_name }}
-
- - name: Docker build and push master
- uses: docker/build-push-action@v6
- if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
- with:
- context: .
- push: true
- platforms: linux/amd64,linux/arm64/v8
- tags: athou/commafeed:master
-
- # Create GitHub release after Docker image has been published
- - name: Extract Changelog Entry
- uses: mindsers/changelog-reader-action@v2
- if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
- id: changelog_reader
- with:
- version: ${{ github.ref_name }}
-
- - name: Create GitHub release
- uses: softprops/action-gh-release@v2
- if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
- with:
- name: CommaFeed ${{ github.ref_name }}
- body: ${{ steps.changelog_reader.outputs.changes }}
- draft: false
- prerelease: false
- files: |
- commafeed-server/target/commafeed.jar
- commafeed-server/config.yml.example
+ name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
+ path: commafeed-server/target/commafeed-*-runner
diff --git a/commafeed-client/pom.xml b/commafeed-client/pom.xml
index 7049bbff..c55058fb 100644
--- a/commafeed-client/pom.xml
+++ b/commafeed-client/pom.xml
@@ -80,7 +80,7 @@
copy-resources
- ${project.build.directory}/classes/assets
+ ${project.build.directory}/classes/META-INF/resources
dist
diff --git a/commafeed-client/src/app/client.ts b/commafeed-client/src/app/client.ts
index 3e9a7cd1..e34759ed 100644
--- a/commafeed-client/src/app/client.ts
+++ b/commafeed-client/src/app/client.ts
@@ -81,7 +81,17 @@ export const client = {
},
},
user: {
- login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
+ login: async (req: LoginRequest) => {
+ const formData = new URLSearchParams()
+ formData.append("j_username", req.name)
+ formData.append("j_password", req.password)
+ return await axiosInstance.post("j_security_check", formData, {
+ baseURL: ".",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ })
+ },
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
getSettings: async () => await axiosInstance.get("user/settings"),
diff --git a/commafeed-client/src/pages/auth/RegistrationPage.tsx b/commafeed-client/src/pages/auth/RegistrationPage.tsx
index a5570687..23dd8fc0 100644
--- a/commafeed-client/src/pages/auth/RegistrationPage.tsx
+++ b/commafeed-client/src/pages/auth/RegistrationPage.tsx
@@ -24,12 +24,18 @@ export function RegistrationPage() {
},
})
- const register = useAsyncCallback(client.user.register, {
+ const login = useAsyncCallback(client.user.login, {
onSuccess: () => {
dispatch(redirectToRootCategory())
},
})
+ const register = useAsyncCallback(client.user.register, {
+ onSuccess: () => {
+ login.execute(form.values)
+ },
+ })
+
return (
@@ -50,6 +56,12 @@ export function RegistrationPage() {
)}
+ {login.error && (
+
+
+
+ )}
+
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.6.0
-
-
- org.kordamp.shade
- maven-shade-ext-transformers
- 1.4.0
-
-
-
- false
-
-
- *:*
-
- module-info.class
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
-
-
-
-
-
-
- package
-
- shade
-
-
-
-
-
- com.commafeed.CommaFeedApplication
-
-
-
- rome.properties
-
- append
-
-
-
-
-
-
io.swagger.core.v3
swagger-maven-plugin-jakarta
2.2.22
- ${project.build.directory}/classes/assets
+ ${project.build.directory}/classes/META-INF/resources
JSONANDYAML
com.commafeed.frontend.resource
@@ -168,18 +158,6 @@
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.4.2
-
-
-
- true
-
-
-
-
org.apache.maven.plugins
maven-checkstyle-plugin
@@ -247,60 +225,65 @@
provided
- org.slf4j
- slf4j-api
-
-
- org.slf4j
- jcl-over-slf4j
+ org.kohsuke.metainf-services
+ metainf-services
+ 1.11
+ provided
- com.google.inject
- guice
- ${guice.version}
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-security
+
+
+ io.quarkus
+ quarkus-hibernate-validator
+
+
+ io.quarkus
+ quarkus-rest
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ io.quarkus
+ quarkus-websockets
+
+
+ io.quarkus
+ quarkus-hibernate-orm
+
+
+ io.quarkus
+ quarkus-liquibase
+
+
+ io.quarkus
+ quarkus-jdbc-h2
+
+
+ io.quarkus
+ quarkus-jdbc-mysql
+
+
+ io.quarkus
+ quarkus-jdbc-mariadb
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
-
- io.dropwizard
- dropwizard-core
-
-
- io.dropwizard
- dropwizard-unix-socket
-
-
- io.dropwizard
- dropwizard-hibernate
-
-
- io.dropwizard
- dropwizard-migrations
-
-
- io.dropwizard
- dropwizard-assets
-
-
- io.dropwizard
- dropwizard-forms
-
-
- io.dropwizard.metrics
- metrics-graphite
-
io.dropwizard.metrics
metrics-json
-
-
- io.whitfin
- dropwizard-environment-substitutor
- 1.1.1
-
-
- org.eclipse.jetty.websocket
- websocket-jakarta-server
+ 4.2.26
@@ -357,7 +340,6 @@
com.sun.mail
jakarta.mail
- 2.0.1
@@ -409,7 +391,15 @@
org.apache.httpcomponents.client5
httpclient5
+ 5.3.1
+
+
+ org.brotli
+ dec
+ 0.1.2
+
+
io.github.hakky54
sslcontext-kickstart-for-apache5
@@ -423,35 +413,8 @@
- com.h2database
- h2
- 2.3.232
-
-
- com.manticore-projects.tools
- h2migrationtool
- 1.7
-
-
-
- com.mysql
- mysql-connector-j
- 9.0.0
-
-
- org.mariadb.jdbc
- mariadb-java-client
- 3.4.1
-
-
- org.postgresql
- postgresql
- 42.7.3
-
-
-
- org.junit.jupiter
- junit-jupiter-engine
+ io.quarkus
+ quarkus-junit5
test
@@ -464,39 +427,41 @@
mockito-junit-jupiter
test
-
org.mock-server
mockserver-junit-jupiter
5.15.0
test
-
-
- org.bouncycastle
- bcprov-jdk18on
- ${bouncycastle.version}
+ org.glassfish.jersey.core
+ jersey-client
+ ${jersey.version}
test
- org.bouncycastle
- bcpkix-jdk18on
- ${bouncycastle.version}
+ org.glassfish.jersey.media
+ jersey-media-multipart
+ ${jersey.version}
+ test
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+ ${jersey.version}
+ test
+
+
+ io.rest-assured
+ rest-assured
test
-
com.icegreen
greenmail-junit5
2.0.1
test
-
- io.dropwizard
- dropwizard-testing
- test
-
org.awaitility
awaitility
@@ -508,30 +473,19 @@
1.46.0
test
-
-
- org.testcontainers
- testcontainers
- ${testcontainers.version}
- test
-
-
- org.testcontainers
- postgresql
- ${testcontainers.version}
- test
-
-
- org.testcontainers
- mysql
- ${testcontainers.version}
- test
-
-
- org.testcontainers
- mariadb
- ${testcontainers.version}
- test
-
+
+
+
+ native
+
+
+ native
+
+
+
+ true
+
+
+
diff --git a/commafeed-server/src/main/assembly/zip-quarkus-app.xml b/commafeed-server/src/main/assembly/zip-quarkus-app.xml
new file mode 100644
index 00000000..e35ab082
--- /dev/null
+++ b/commafeed-server/src/main/assembly/zip-quarkus-app.xml
@@ -0,0 +1,21 @@
+
+
+ zip-quarkus-app
+
+ true
+ commafeed-${project.version}-${quarkus.datasource.db-kind}
+
+
+ zip
+
+
+
+ ${project.build.directory}/quarkus-app
+ /
+
+ **/*
+
+
+
+
\ No newline at end of file
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java
index 4bd7183f..222b6548 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java
@@ -1,277 +1,44 @@
package com.commafeed;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.time.Instant;
-import java.util.EnumSet;
-import java.util.Set;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
-import org.hibernate.cfg.AvailableSettings;
-
-import com.codahale.metrics.json.MetricsModule;
-import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.feed.FeedRefreshEngine;
-import com.commafeed.backend.model.AbstractModel;
-import com.commafeed.backend.model.Feed;
-import com.commafeed.backend.model.FeedCategory;
-import com.commafeed.backend.model.FeedEntry;
-import com.commafeed.backend.model.FeedEntryContent;
-import com.commafeed.backend.model.FeedEntryStatus;
-import com.commafeed.backend.model.FeedEntryTag;
-import com.commafeed.backend.model.FeedSubscription;
-import com.commafeed.backend.model.User;
-import com.commafeed.backend.model.UserRole;
-import com.commafeed.backend.model.UserSettings;
-import com.commafeed.backend.service.UserService;
import com.commafeed.backend.service.db.DatabaseStartupService;
-import com.commafeed.backend.service.db.H2MigrationService;
-import com.commafeed.backend.task.ScheduledTask;
-import com.commafeed.frontend.auth.PasswordConstraintValidator;
-import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
-import com.commafeed.frontend.resource.AdminREST;
-import com.commafeed.frontend.resource.CategoryREST;
-import com.commafeed.frontend.resource.EntryREST;
-import com.commafeed.frontend.resource.FeedREST;
-import com.commafeed.frontend.resource.ServerREST;
-import com.commafeed.frontend.resource.UserREST;
-import com.commafeed.frontend.resource.fever.FeverREST;
-import com.commafeed.frontend.servlet.CustomCssServlet;
-import com.commafeed.frontend.servlet.CustomJsServlet;
-import com.commafeed.frontend.servlet.LogoutServlet;
-import com.commafeed.frontend.servlet.NextUnreadServlet;
-import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
-import com.commafeed.frontend.session.SessionHelperFactoryProvider;
-import com.commafeed.frontend.ws.WebSocketConfigurator;
-import com.commafeed.frontend.ws.WebSocketEndpoint;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.MapperFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
+import com.commafeed.backend.task.TaskScheduler;
+import com.commafeed.security.password.PasswordConstraintValidator;
-import io.dropwizard.assets.AssetsBundle;
-import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
-import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
-import io.dropwizard.configuration.SubstitutingSourceProvider;
-import io.dropwizard.core.Application;
-import io.dropwizard.core.ConfiguredBundle;
-import io.dropwizard.core.setup.Bootstrap;
-import io.dropwizard.core.setup.Environment;
-import io.dropwizard.db.DataSourceFactory;
-import io.dropwizard.forms.MultiPartBundle;
-import io.dropwizard.hibernate.HibernateBundle;
-import io.dropwizard.migrations.MigrationsBundle;
-import io.dropwizard.servlets.CacheBustingFilter;
-import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
-import jakarta.servlet.DispatcherType;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.websocket.server.ServerEndpointConfig;
+import io.quarkus.runtime.ShutdownEvent;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Singleton;
+import lombok.RequiredArgsConstructor;
-public class CommaFeedApplication extends Application {
+@Singleton
+@RequiredArgsConstructor
+public class CommaFeedApplication {
public static final String USERNAME_ADMIN = "admin";
public static final String USERNAME_DEMO = "demo";
public static final Instant STARTUP_TIME = Instant.now();
- private HibernateBundle hibernateBundle;
+ private final DatabaseStartupService databaseStartupService;
+ private final FeedRefreshEngine feedRefreshEngine;
+ private final TaskScheduler taskScheduler;
+ private final CommaFeedConfiguration config;
- @Override
- public String getName() {
- return "CommaFeed";
+ public void start(@Observes StartupEvent ev) {
+ PasswordConstraintValidator.setStrict(config.users().strictPasswordPolicy());
+
+ databaseStartupService.populateInitialData();
+
+ feedRefreshEngine.start();
+ taskScheduler.start();
}
- @Override
- public void initialize(Bootstrap bootstrap) {
- configureEnvironmentSubstitutor(bootstrap);
- configureObjectMapper(bootstrap.getObjectMapper());
-
- // run h2 migration as the first bundle because we need to migrate before hibernate is initialized
- bootstrap.addBundle(new ConfiguredBundle<>() {
- @Override
- public void run(CommaFeedConfiguration config, Environment environment) {
- DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
- String url = dataSourceFactory.getUrl();
- if (isFileBasedH2(url)) {
- Path path = getFilePath(url);
- String user = dataSourceFactory.getUser();
- String password = dataSourceFactory.getPassword();
- new H2MigrationService().migrateIfNeeded(path, user, password);
- }
- }
-
- private boolean isFileBasedH2(String url) {
- return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:");
- }
-
- private Path getFilePath(String url) {
- String name = url.substring("jdbc:h2:".length()).split(";")[0];
- return Paths.get(name + ".mv.db");
- }
- });
-
- bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class,
- FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class,
- UserSettings.class) {
- @Override
- public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
- DataSourceFactory factory = configuration.getDataSourceFactory();
-
- factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
-
- factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
- factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
- factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
- factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
- return factory;
- }
- });
-
- bootstrap.addBundle(new MigrationsBundle<>() {
- @Override
- public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
- return configuration.getDataSourceFactory();
- }
- });
-
- bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
- bootstrap.addBundle(new MultiPartBundle());
+ public void stop(@Observes ShutdownEvent ev) {
+ feedRefreshEngine.stop();
+ taskScheduler.stop();
}
- private static void configureEnvironmentSubstitutor(Bootstrap bootstrap) {
- bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<>() {
- @Override
- protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
- // disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty
- return objectMapper
- .setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
- }
- });
-
- bootstrap.setConfigurationSourceProvider(buildEnvironmentSubstitutor(bootstrap));
- }
-
- private static void configureObjectMapper(ObjectMapper objectMapper) {
- // read and write instants as milliseconds instead of nanoseconds
- objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
- .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
-
- // add support for serializing metrics
- objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
- }
-
- private static EnvironmentSubstitutor buildEnvironmentSubstitutor(Bootstrap bootstrap) {
- // enable config.yml string substitution
- // e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR
- SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
- new EnvironmentVariableSubstitutor(false));
-
- // enable config.yml properties override with env variables prefixed with CF_
- // e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true
- return new EnvironmentSubstitutor("CF", substitutingSourceProvider);
- }
-
- @Override
- public void run(CommaFeedConfiguration config, Environment environment) {
- PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
-
- // guice init
- Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
-
- // session management
- environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
-
- // support for "@SecurityCheck User user" injection
- environment.jersey()
- .register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserDAO.class),
- injector.getInstance(UserService.class), config));
- // support for "@Context SessionHelper sessionHelper" injection
- environment.jersey().register(new SessionHelperFactoryProvider.Binder());
-
- // REST resources
- environment.jersey().setUrlPattern("/rest/*");
- environment.jersey().register(injector.getInstance(AdminREST.class));
- environment.jersey().register(injector.getInstance(CategoryREST.class));
- environment.jersey().register(injector.getInstance(EntryREST.class));
- environment.jersey().register(injector.getInstance(FeedREST.class));
- environment.jersey().register(injector.getInstance(ServerREST.class));
- environment.jersey().register(injector.getInstance(UserREST.class));
- environment.jersey().register(injector.getInstance(FeverREST.class));
-
- // Servlets
- environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");
- environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout");
- environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
- environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
- if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
- environment.servlets()
- .addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
- .addMapping("/robots.txt");
- }
-
- // WebSocket endpoint
- JakartaWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> {
- container.setDefaultMaxSessionIdleTimeout(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000);
-
- container.addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
- .configurator(injector.getInstance(WebSocketConfigurator.class))
- .build());
- });
-
- // Scheduled tasks
- Set tasks = injector.getInstance(Key.get(new TypeLiteral<>() {
- }));
- ScheduledExecutorService executor = environment.lifecycle()
- .scheduledExecutorService("task-scheduler", true)
- .threads(tasks.size())
- .build();
- for (ScheduledTask task : tasks) {
- task.register(executor);
- }
-
- // database init/changelogs
- environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class));
-
- // start feed fetching engine
- environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class));
-
- // prevent caching index.html, so that the webapp is always up to date
- environment.servlets()
- .addFilter("index-cache-busting-filter", new CacheBustingFilter())
- .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/");
-
- // prevent caching openapi files, so that the documentation is always up to date
- environment.servlets()
- .addFilter("openapi-cache-busting-filter", new CacheBustingFilter())
- .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/openapi.json", "/openapi.yaml");
-
- // prevent caching REST resources, except for favicons
- environment.servlets().addFilter("rest-cache-busting-filter", new CacheBustingFilter() {
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
- String path = ((HttpServletRequest) request).getRequestURI();
- if (path.contains("/feed/favicon")) {
- chain.doFilter(request, response);
- } else {
- super.doFilter(request, response, chain);
- }
- }
- }).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
-
- }
-
- public static void main(String[] args) throws Exception {
- new CommaFeedApplication().run(args);
- }
}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
index 845c01cd..eb64073f 100644
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedConfiguration.java
@@ -1,188 +1,287 @@
package com.commafeed;
-import java.io.IOException;
-import java.io.InputStream;
+import java.time.Duration;
import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Properties;
+import java.util.Optional;
-import com.commafeed.backend.cache.RedisPoolFactory;
-import com.commafeed.frontend.session.SessionHandlerFactory;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
-import io.dropwizard.core.Configuration;
-import io.dropwizard.db.DataSourceFactory;
-import io.dropwizard.util.DataSize;
-import io.dropwizard.util.Duration;
-import jakarta.validation.Valid;
+import io.quarkus.runtime.configuration.MemorySize;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
-import lombok.Getter;
-import lombok.Setter;
+import redis.clients.jedis.DefaultJedisClientConfig;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisClientConfig;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.Protocol;
-@Getter
-@Setter
-public class CommaFeedConfiguration extends Configuration {
+/**
+ * CommaFeed configuration
+ *
+ * Default values are for production, they can be overridden in application.properties
+ */
+@ConfigMapping(prefix = "commafeed")
+public interface CommaFeedConfiguration {
- public enum CacheType {
- NOOP, REDIS
+ /**
+ * URL used to access commafeed, used for various redirects.
+ *
+ */
+ @NotBlank
+ @WithDefault("http://localhost:8082")
+ String publicUrl();
+
+ /**
+ * Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
+ */
+ @WithDefault("true")
+ boolean hideFromWebCrawlers();
+
+ /**
+ * If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser.
+ *
+ * This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
+ */
+ @WithDefault("false")
+ boolean imageProxyEnabled();
+
+ /**
+ * Message displayed in a notification at the bottom of the page.
+ */
+ Optional announcement();
+
+ /**
+ * Google Analytics tracking code.
+ */
+ Optional googleAnalyticsTrackingCode();
+
+ /**
+ * Google Auth key for fetching Youtube favicons.
+ */
+ Optional googleAuthKey();
+
+ /**
+ * Feed refresh engine settings.
+ */
+ FeedRefresh feedRefresh();
+
+ /**
+ * Database settings.
+ */
+ Database database();
+
+ /**
+ * Users settings.
+ */
+ Users users();
+
+ /**
+ * Websocket settings.
+ */
+ Websocket websocket();
+
+ /**
+ * SMTP settings for password recovery.
+ */
+ Optional smtp();
+
+ /**
+ * Redis settings to enable caching. This is only really useful on instances with a lot of users.
+ */
+ Redis redis();
+
+ interface FeedRefresh {
+ /**
+ * Amount of time CommaFeed will wait before refreshing the same feed.
+ */
+ @WithDefault("5m")
+ Duration interval();
+
+ /**
+ * If true, CommaFeed will calculate the next refresh time based on the feed's average entry interval and the time since the last
+ * entry was published. See {@link FeedRefreshIntervalCalculator} for details.
+ */
+ @WithDefault("false")
+ boolean intervalEmpirical();
+
+ /**
+ * Amount of http threads used to fetch feeds.
+ */
+ @Min(1)
+ @WithDefault("3")
+ int httpThreads();
+
+ /**
+ * Amount of threads used to insert new entries in the database.
+ */
+ @Min(1)
+ @WithDefault("1")
+ int databaseThreads();
+
+ /**
+ * If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
+ */
+ @WithDefault("5M")
+ MemorySize maxResponseSize();
+
+ /**
+ * Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again.
+ */
+ @WithDefault("0")
+ Duration userInactivityPeriod();
+
+ /**
+ * User-Agent string that will be used by the http client, leave empty for the default one.
+ */
+ Optional userAgent();
}
- @Valid
- @NotNull
- @JsonProperty("database")
- private final DataSourceFactory dataSourceFactory = new DataSourceFactory();
+ interface Database {
+ /**
+ * Database query timeout.
+ */
+ @WithDefault("0")
+ int queryTimeout();
- @Valid
- @NotNull
- @JsonProperty("redis")
- private final RedisPoolFactory redisPoolFactory = new RedisPoolFactory();
+ Cleanup cleanup();
- @Valid
- @NotNull
- @JsonProperty("session")
- private final SessionHandlerFactory sessionHandlerFactory = new SessionHandlerFactory();
+ interface Cleanup {
+ /**
+ * Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable.
+ */
+ @WithDefault("365d")
+ Duration entriesMaxAge();
- @Valid
- @NotNull
- @JsonProperty("app")
- private ApplicationSettings applicationSettings;
+ /**
+ * Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. 0 to disable.
+ */
+ @WithDefault("0")
+ Duration statusesMaxAge();
- private final String version;
- private final String gitCommit;
+ /**
+ * Maximum number of entries per feed to keep in the database. 0 to disable.
+ */
+ @WithDefault("500")
+ int maxFeedCapacity();
- public CommaFeedConfiguration() {
- Properties properties = new Properties();
- try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
- if (stream != null) {
- properties.load(stream);
+ /**
+ * Limit the number of feeds a user can subscribe to. 0 to disable.
+ */
+ @WithDefault("0")
+ int maxFeedsPerUser();
+
+ /**
+ * Rows to delete per query while cleaning up old entries.
+ */
+ @Positive
+ @WithDefault("100")
+ int batchSize();
+
+ default Instant statusesInstantThreshold() {
+ return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
}
- } catch (IOException e) {
- throw new RuntimeException(e);
}
-
- this.version = properties.getProperty("git.build.version", "unknown");
- this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
}
- @Getter
- @Setter
- public static class ApplicationSettings {
- @NotNull
- @NotBlank
- @Valid
- private String publicUrl;
+ interface Users {
+ /**
+ * Whether to let users create accounts for themselves.
+ */
+ @WithDefault("false")
+ boolean allowRegistrations();
- @NotNull
- @Valid
- private Boolean hideFromWebCrawlers = true;
+ /**
+ * Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
+ */
+ @WithDefault("true")
+ boolean strictPasswordPolicy();
- @NotNull
- @Valid
- private Boolean allowRegistrations;
+ /**
+ * Whether to create a demo account the first time the app starts.
+ */
+ @WithDefault("false")
+ boolean createDemoAccount();
+ }
- @NotNull
- @Valid
- private Boolean strictPasswordPolicy = true;
+ interface Smtp {
+ String host();
- @NotNull
- @Valid
- private Boolean createDemoAccount;
+ int port();
- private String googleAnalyticsTrackingCode;
+ boolean tls();
- private String googleAuthKey;
+ String userName();
- @NotNull
- @Min(1)
- @Valid
- private Integer backgroundThreads;
+ String password();
- @NotNull
- @Min(1)
- @Valid
- private Integer databaseUpdateThreads;
+ String fromAddress();
+ }
- @NotNull
- @Positive
- @Valid
- private Integer databaseCleanupBatchSize = 100;
+ interface Websocket {
+ /**
+ * Enable websocket connection so the server can notify the web client that there are new entries for your feeds.
+ */
+ @WithDefault("true")
+ boolean enabled();
- private String smtpHost;
- private int smtpPort;
- private boolean smtpTls;
- private String smtpUserName;
- private String smtpPassword;
- private String smtpFromAddress;
+ /**
+ * Interval at which the client will send a ping message on the websocket to keep the connection alive.
+ */
+ @WithDefault("15m")
+ Duration pingInterval();
- private boolean graphiteEnabled;
- private String graphitePrefix;
- private String graphiteHost;
- private int graphitePort;
- private int graphiteInterval;
+ /**
+ * If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
+ */
+ @WithDefault("30s")
+ Duration treeReloadInterval();
+ }
- @NotNull
- @Valid
- private Boolean heavyLoad;
+ interface Redis {
- @NotNull
- @Valid
- private Boolean imageProxyEnabled;
+ Optional host();
- @NotNull
- @Min(0)
- @Valid
- private Integer queryTimeout;
+ @WithDefault("" + Protocol.DEFAULT_PORT)
+ int port();
- @NotNull
- @Min(0)
- @Valid
- private Integer keepStatusDays;
+ /**
+ * Username is only required when using Redis ACLs
+ */
+ Optional username();
- @NotNull
- @Min(0)
- @Valid
- private Integer maxFeedCapacity;
+ Optional password();
- @NotNull
- @Min(0)
- @Valid
- private Integer maxEntriesAgeDays = 0;
+ @WithDefault("" + Protocol.DEFAULT_TIMEOUT)
+ int timeout();
- @NotNull
- @Valid
- private Integer maxFeedsPerUser = 0;
+ @WithDefault("" + Protocol.DEFAULT_DATABASE)
+ int database();
- @NotNull
- @Valid
- private DataSize maxFeedResponseSize = DataSize.megabytes(5);
+ @WithDefault("500")
+ int maxTotal();
- @NotNull
- @Min(0)
- @Valid
- private Integer refreshIntervalMinutes;
+ default JedisPool build() {
+ Optional host = host();
+ if (host.isEmpty()) {
+ throw new IllegalStateException("Redis host is required");
+ }
- @NotNull
- @Valid
- private CacheType cache;
+ JedisPoolConfig poolConfig = new JedisPoolConfig();
+ poolConfig.setMaxTotal(maxTotal());
- @Valid
- private String announcement;
+ JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
+ .user(username().orElse(null))
+ .password(password().orElse(null))
+ .timeoutMillis(timeout())
+ .database(database())
+ .build();
- private String userAgent;
-
- private Boolean websocketEnabled = true;
-
- private Duration websocketPingInterval = Duration.minutes(15);
-
- private Duration treeReloadInterval = Duration.seconds(30);
-
- public Instant getUnreadThreshold() {
- return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null;
+ return new JedisPool(poolConfig, new HostAndPort(host.get(), port()), clientConfig);
}
-
}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java
deleted file mode 100644
index 945aa5b7..00000000
--- a/commafeed-server/src/main/java/com/commafeed/CommaFeedModule.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.commafeed;
-
-import java.net.InetSocketAddress;
-import java.util.concurrent.TimeUnit;
-
-import org.hibernate.SessionFactory;
-
-import com.codahale.metrics.MetricFilter;
-import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.graphite.Graphite;
-import com.codahale.metrics.graphite.GraphiteReporter;
-import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
-import com.commafeed.CommaFeedConfiguration.CacheType;
-import com.commafeed.backend.cache.CacheService;
-import com.commafeed.backend.cache.NoopCacheService;
-import com.commafeed.backend.cache.RedisCacheService;
-import com.commafeed.backend.favicon.AbstractFaviconFetcher;
-import com.commafeed.backend.favicon.DefaultFaviconFetcher;
-import com.commafeed.backend.favicon.FacebookFaviconFetcher;
-import com.commafeed.backend.favicon.YoutubeFaviconFetcher;
-import com.commafeed.backend.task.DemoAccountCleanupTask;
-import com.commafeed.backend.task.EntriesExceedingFeedCapacityCleanupTask;
-import com.commafeed.backend.task.OldEntriesCleanupTask;
-import com.commafeed.backend.task.OldStatusesCleanupTask;
-import com.commafeed.backend.task.OrphanedContentsCleanupTask;
-import com.commafeed.backend.task.OrphanedFeedsCleanupTask;
-import com.commafeed.backend.task.ScheduledTask;
-import com.commafeed.backend.urlprovider.FeedURLProvider;
-import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider;
-import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.multibindings.Multibinder;
-
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-@RequiredArgsConstructor
-@Slf4j
-public class CommaFeedModule extends AbstractModule {
-
- @Getter(onMethod = @__({ @Provides }))
- private final SessionFactory sessionFactory;
-
- @Getter(onMethod = @__({ @Provides }))
- private final CommaFeedConfiguration config;
-
- @Getter(onMethod = @__({ @Provides }))
- private final MetricRegistry metrics;
-
- @Override
- protected void configure() {
- CacheService cacheService = config.getApplicationSettings().getCache() == CacheType.NOOP ? new NoopCacheService()
- : new RedisCacheService(config.getRedisPoolFactory().build());
- log.info("using cache {}", cacheService.getClass());
- bind(CacheService.class).toInstance(cacheService);
-
- Multibinder faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
- faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
- faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
- faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
-
- Multibinder urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class);
- urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class);
- urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class);
-
- Multibinder taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class);
- taskMultibinder.addBinding().to(OldStatusesCleanupTask.class);
- taskMultibinder.addBinding().to(EntriesExceedingFeedCapacityCleanupTask.class);
- taskMultibinder.addBinding().to(OldEntriesCleanupTask.class);
- taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class);
- taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class);
- taskMultibinder.addBinding().to(DemoAccountCleanupTask.class);
-
- ApplicationSettings settings = config.getApplicationSettings();
-
- if (settings.isGraphiteEnabled()) {
- final String graphitePrefix = settings.getGraphitePrefix();
- final String graphiteHost = settings.getGraphiteHost();
- final int graphitePort = settings.getGraphitePort();
- final int graphiteInterval = settings.getGraphiteInterval();
-
- log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort,
- graphitePrefix, graphiteInterval);
-
- final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort));
- final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics)
- .prefixedWith(graphitePrefix)
- .convertRatesTo(TimeUnit.SECONDS)
- .convertDurationsTo(TimeUnit.MILLISECONDS)
- .filter(MetricFilter.ALL)
- .build(graphite);
- reporter.start(graphiteInterval, TimeUnit.SECONDS);
- }
- }
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java
new file mode 100644
index 00000000..63df69dd
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedProducers.java
@@ -0,0 +1,31 @@
+package com.commafeed;
+
+import com.codahale.metrics.MetricRegistry;
+import com.commafeed.CommaFeedConfiguration.Redis;
+import com.commafeed.backend.cache.CacheService;
+import com.commafeed.backend.cache.NoopCacheService;
+import com.commafeed.backend.cache.RedisCacheService;
+
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class CommaFeedProducers {
+
+ @Produces
+ @Singleton
+ public CacheService cacheService(CommaFeedConfiguration config) {
+ Redis redis = config.redis();
+ if (redis.host().isEmpty()) {
+ return new NoopCacheService();
+ }
+
+ return new RedisCacheService(redis.build());
+ }
+
+ @Produces
+ @Singleton
+ public MetricRegistry metricRegistry() {
+ return new MetricRegistry();
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java
new file mode 100644
index 00000000..63188ed5
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedVersion.java
@@ -0,0 +1,31 @@
+package com.commafeed;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import jakarta.inject.Singleton;
+import lombok.Getter;
+
+@Singleton
+@Getter
+public class CommaFeedVersion {
+
+ private final String version;
+ private final String gitCommit;
+
+ public CommaFeedVersion() {
+ Properties properties = new Properties();
+ try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
+ if (stream != null) {
+ properties.load(stream);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ this.version = properties.getProperty("git.build.version", "unknown");
+ this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
+ }
+
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java
new file mode 100644
index 00000000..68618094
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/ExceptionMappers.java
@@ -0,0 +1,33 @@
+package com.commafeed;
+
+import org.jboss.resteasy.reactive.RestResponse;
+import org.jboss.resteasy.reactive.RestResponse.Status;
+import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
+
+import io.quarkus.security.AuthenticationFailedException;
+import jakarta.annotation.Priority;
+import jakarta.validation.ValidationException;
+import jakarta.ws.rs.ext.Provider;
+
+@Provider
+@Priority(1)
+public class ExceptionMappers {
+
+ // display a message when the user fails to authenticate
+ @ServerExceptionMapper(AuthenticationFailedException.class)
+ public RestResponse authenticationFailed(AuthenticationFailedException e) {
+ return RestResponse.status(RestResponse.Status.UNAUTHORIZED, new AuthenticationExceptionInfo(e.getMessage()));
+ }
+
+ // display a message for validation errors
+ @ServerExceptionMapper(ValidationException.class)
+ public RestResponse validationException(ValidationException e) {
+ return RestResponse.status(Status.BAD_REQUEST, new ValidationExceptionInfo(e.getMessage()));
+ }
+
+ public record AuthenticationExceptionInfo(String message) {
+ }
+
+ public record ValidationExceptionInfo(String message) {
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java
new file mode 100644
index 00000000..0bc56613
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/JacksonCustomizer.java
@@ -0,0 +1,27 @@
+package com.commafeed;
+
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.json.MetricsModule;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import io.quarkus.jackson.ObjectMapperCustomizer;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class JacksonCustomizer implements ObjectMapperCustomizer {
+ @Override
+ public void customize(ObjectMapper objectMapper) {
+ objectMapper.registerModule(new JavaTimeModule());
+
+ // read and write instants as milliseconds instead of nanoseconds
+ objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
+
+ // add support for serializing metrics
+ objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java
new file mode 100644
index 00000000..5f6b145b
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/NativeImageClasses.java
@@ -0,0 +1,165 @@
+package com.commafeed;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
+@RegisterForReflection(
+ targets = {
+ // metrics
+ MetricRegistry.class, Meter.class, Gauge.class, Counter.class, Timer.class, Histogram.class,
+
+ // rome
+ com.rometools.rome.feed.module.DCModuleImpl.class, com.rometools.rome.feed.module.DCSubjectImpl.class,
+ com.rometools.modules.content.ContentModuleImpl.class, com.rometools.modules.mediarss.MediaModuleImpl.class,
+ com.rometools.modules.mediarss.MediaEntryModuleImpl.class,
+
+ // extracted from all 3 rome.properties files of rome library
+ com.rometools.rome.io.impl.RSS090Parser.class, com.rometools.rome.io.impl.RSS091NetscapeParser.class,
+ com.rometools.rome.io.impl.RSS091UserlandParser.class, com.rometools.rome.io.impl.RSS092Parser.class,
+ com.rometools.rome.io.impl.RSS093Parser.class, com.rometools.rome.io.impl.RSS094Parser.class,
+ com.rometools.rome.io.impl.RSS10Parser.class, com.rometools.rome.io.impl.RSS20wNSParser.class,
+ com.rometools.rome.io.impl.RSS20Parser.class, com.rometools.rome.io.impl.Atom10Parser.class,
+ com.rometools.rome.io.impl.Atom03Parser.class,
+
+ com.rometools.rome.io.impl.SyModuleParser.class, com.rometools.rome.io.impl.DCModuleParser.class,
+
+ com.rometools.rome.io.impl.RSS090Generator.class, com.rometools.rome.io.impl.RSS091NetscapeGenerator.class,
+ com.rometools.rome.io.impl.RSS091UserlandGenerator.class, com.rometools.rome.io.impl.RSS092Generator.class,
+ com.rometools.rome.io.impl.RSS093Generator.class, com.rometools.rome.io.impl.RSS094Generator.class,
+ com.rometools.rome.io.impl.RSS10Generator.class, com.rometools.rome.io.impl.RSS20Generator.class,
+ com.rometools.rome.io.impl.Atom10Generator.class, com.rometools.rome.io.impl.Atom03Generator.class,
+
+ com.rometools.rome.feed.synd.impl.ConverterForAtom10.class, com.rometools.rome.feed.synd.impl.ConverterForAtom03.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS090.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS091Netscape.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS091Userland.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS092.class, com.rometools.rome.feed.synd.impl.ConverterForRSS093.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS094.class, com.rometools.rome.feed.synd.impl.ConverterForRSS10.class,
+ com.rometools.rome.feed.synd.impl.ConverterForRSS20.class,
+
+ com.rometools.modules.mediarss.io.RSS20YahooParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
+ com.rometools.modules.fyyd.io.FyydParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class, com.rometools.modules.atom.io.AtomModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ModuleParser.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleParser.class, com.rometools.modules.feedpress.io.FeedpressParser.class,
+ com.rometools.modules.fyyd.io.FyydParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.content.io.ContentModuleParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.feedpress.io.FeedpressParser.class, com.rometools.modules.fyyd.io.FyydParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.content.io.ContentModuleParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.itunes.io.ITunesParser.class, com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.atom.io.AtomModuleParser.class, com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class,
+ com.rometools.modules.georss.SimpleParser.class, com.rometools.modules.georss.W3CGeoParser.class,
+ com.rometools.modules.photocast.io.Parser.class, com.rometools.modules.itunes.io.ITunesParserOldNamespace.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class, com.rometools.modules.sle.io.ItemParser.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleParser.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS1.class, com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.content.io.ContentModuleParser.class,
+ com.rometools.modules.slash.io.SlashModuleParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+
+ com.rometools.modules.cc.io.ModuleParserRSS2.class, com.rometools.modules.base.io.GoogleBaseParser.class,
+ com.rometools.modules.base.io.CustomTagParser.class, com.rometools.modules.slash.io.SlashModuleParser.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleParser.class, com.rometools.modules.georss.SimpleParser.class,
+ com.rometools.modules.georss.W3CGeoParser.class, com.rometools.modules.photocast.io.Parser.class,
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+ com.rometools.modules.mediarss.io.AlternateMediaModuleParser.class,
+ com.rometools.modules.thr.io.ThreadingModuleParser.class, com.rometools.modules.psc.io.PodloveSimpleChapterParser.class,
+
+ com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.itunes.io.ITunesGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
+ com.rometools.modules.sle.io.ModuleGenerator.class, com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
+ com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
+
+ com.rometools.modules.content.io.ContentModuleGenerator.class,
+
+ com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+
+ com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class,
+ com.rometools.modules.georss.SimpleGenerator.class, com.rometools.modules.georss.W3CGeoGenerator.class,
+ com.rometools.modules.photocast.io.Generator.class, com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+ com.rometools.modules.feedpress.io.FeedpressGenerator.class, com.rometools.modules.fyyd.io.FyydGenerator.class,
+
+ com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
+ com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class, com.rometools.modules.itunes.io.ITunesGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.atom.io.AtomModuleGenerator.class,
+ com.rometools.modules.yahooweather.io.WeatherModuleGenerator.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
+
+ com.rometools.modules.base.io.GoogleBaseGenerator.class, com.rometools.modules.content.io.ContentModuleGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class,
+
+ com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.GoogleBaseGenerator.class,
+ com.rometools.modules.base.io.CustomTagGenerator.class, com.rometools.modules.slash.io.SlashModuleGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+
+ com.rometools.modules.cc.io.CCModuleGenerator.class, com.rometools.modules.base.io.CustomTagGenerator.class,
+ com.rometools.modules.slash.io.SlashModuleGenerator.class,
+ com.rometools.modules.opensearch.impl.OpenSearchModuleGenerator.class, com.rometools.modules.georss.SimpleGenerator.class,
+ com.rometools.modules.georss.W3CGeoGenerator.class, com.rometools.modules.photocast.io.Generator.class,
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class, com.rometools.modules.thr.io.ThreadingModuleGenerator.class,
+ com.rometools.modules.psc.io.PodloveSimpleChapterGenerator.class,
+
+ com.rometools.modules.mediarss.io.MediaModuleParser.class,
+
+ com.rometools.modules.mediarss.io.MediaModuleGenerator.class,
+
+ com.rometools.opml.io.impl.OPML10Generator.class, com.rometools.opml.io.impl.OPML20Generator.class,
+
+ com.rometools.opml.io.impl.OPML10Parser.class, com.rometools.opml.io.impl.OPML20Parser.class,
+
+ com.rometools.opml.feed.synd.impl.ConverterForOPML10.class, com.rometools.opml.feed.synd.impl.ConverterForOPML20.class, })
+
+public class NativeImageClasses {
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
index c7bc6155..2d5a59f0 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/HttpGetter.java
@@ -21,21 +21,21 @@ import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
-import org.eclipse.jetty.http.HttpStatus;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.CommaFeedVersion;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
-import io.dropwizard.util.DataSize;
-import jakarta.inject.Inject;
+import io.quarkus.runtime.configuration.MemorySize;
import jakarta.inject.Singleton;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -51,15 +51,15 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
public class HttpGetter {
private final CloseableHttpClient client;
- private final DataSize maxResponseSize;
+ private final MemorySize maxResponseSize;
- @Inject
- public HttpGetter(CommaFeedConfiguration config, MetricRegistry metrics) {
- PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.getApplicationSettings().getBackgroundThreads());
- String userAgent = Optional.ofNullable(config.getApplicationSettings().getUserAgent())
- .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion()));
+ public HttpGetter(CommaFeedConfiguration config, CommaFeedVersion version, MetricRegistry metrics) {
+ PoolingHttpClientConnectionManager connectionManager = newConnectionManager(config.feedRefresh().httpThreads());
+ String userAgent = config.feedRefresh()
+ .userAgent()
+ .orElseGet(() -> String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", version.getVersion()));
this.client = newClient(connectionManager, userAgent);
- this.maxResponseSize = config.getApplicationSettings().getMaxFeedResponseSize();
+ this.maxResponseSize = config.feedRefresh().maxResponseSize();
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "max"), () -> connectionManager.getTotalStats().getMax());
metrics.registerGauge(MetricRegistry.name(getClass(), "pool", "size"),
@@ -98,7 +98,7 @@ public class HttpGetter {
context.setRequestConfig(RequestConfig.custom().setResponseTimeout(timeout, TimeUnit.MILLISECONDS).build());
HttpResponse response = client.execute(request, context, resp -> {
- byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.toBytes());
+ byte[] content = resp.getEntity() == null ? null : toByteArray(resp.getEntity(), maxResponseSize.asLongValue());
int code = resp.getCode();
String lastModifiedHeader = Optional.ofNullable(resp.getFirstHeader(HttpHeaders.LAST_MODIFIED))
.map(NameValuePair::getValue)
@@ -120,7 +120,7 @@ public class HttpGetter {
});
int code = response.getCode();
- if (code == HttpStatus.NOT_MODIFIED_304) {
+ if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("'304 - not modified' http code received");
} else if (code >= 300) {
throw new HttpResponseException(code, "Server returned HTTP error code " + code);
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java b/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java
deleted file mode 100644
index b74fbf19..00000000
--- a/commafeed-server/src/main/java/com/commafeed/backend/cache/RedisPoolFactory.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.commafeed.backend.cache;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import lombok.Getter;
-import redis.clients.jedis.DefaultJedisClientConfig;
-import redis.clients.jedis.HostAndPort;
-import redis.clients.jedis.JedisClientConfig;
-import redis.clients.jedis.JedisPool;
-import redis.clients.jedis.JedisPoolConfig;
-import redis.clients.jedis.Protocol;
-
-@Getter
-public class RedisPoolFactory {
-
- @JsonProperty
- private String host = "localhost";
-
- @JsonProperty
- private int port = Protocol.DEFAULT_PORT;
-
- @JsonProperty
- private String username;
-
- @JsonProperty
- private String password;
-
- @JsonProperty
- private int timeout = Protocol.DEFAULT_TIMEOUT;
-
- @JsonProperty
- private int database = Protocol.DEFAULT_DATABASE;
-
- @JsonProperty
- private int maxTotal = 500;
-
- public JedisPool build() {
- JedisPoolConfig poolConfig = new JedisPoolConfig();
- poolConfig.setMaxTotal(maxTotal);
-
- JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
- .user(username)
- .password(password)
- .timeoutMillis(timeout)
- .database(database)
- .build();
-
- return new JedisPool(poolConfig, new HostAndPort(host, port), clientConfig);
- }
-
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java
index 16bb496b..a84449a0 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedCategoryDAO.java
@@ -3,25 +3,22 @@ package com.commafeed.backend.dao;
import java.util.List;
import java.util.Objects;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.QFeedCategory;
import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User;
import com.querydsl.core.types.Predicate;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class FeedCategoryDAO extends GenericDAO {
private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
- @Inject
- public FeedCategoryDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public FeedCategoryDAO(EntityManager entityManager) {
+ super(entityManager, FeedCategory.class);
}
public List findAll(User user) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java
index f34a923e..cdaa690e 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedDAO.java
@@ -4,7 +4,6 @@ import java.time.Instant;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
-import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.QFeed;
@@ -12,8 +11,8 @@ import com.commafeed.backend.model.QFeedSubscription;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class FeedDAO extends GenericDAO {
@@ -21,9 +20,8 @@ public class FeedDAO extends GenericDAO {
private static final QFeed FEED = QFeed.feed;
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
- @Inject
- public FeedDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public FeedDAO(EntityManager entityManager) {
+ super(entityManager, Feed.class);
}
public List findNextUpdatable(int count, Instant lastLoginThreshold) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java
index 8d65b449..0b742e64 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryContentDAO.java
@@ -2,16 +2,14 @@ package com.commafeed.backend.dao;
import java.util.List;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLSubQuery;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class FeedEntryContentDAO extends GenericDAO {
@@ -19,9 +17,8 @@ public class FeedEntryContentDAO extends GenericDAO {
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
- @Inject
- public FeedEntryContentDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public FeedEntryContentDAO(EntityManager entityManager) {
+ super(entityManager, FeedEntryContent.class);
}
public List findExisting(String contentHash, String titleHash) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java
index 2cf226ad..27d5de3f 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryDAO.java
@@ -3,16 +3,14 @@ package com.commafeed.backend.dao;
import java.time.Instant;
import java.util.List;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeedEntry;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.NumberExpression;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -21,9 +19,8 @@ public class FeedEntryDAO extends GenericDAO {
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
- @Inject
- public FeedEntryDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public FeedEntryDAO(EntityManager entityManager) {
+ super(entityManager, FeedEntry.class);
}
public FeedEntry findExisting(String guidHash, Feed feed) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java
index 358fbaa2..dee56442 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryStatusDAO.java
@@ -7,7 +7,6 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
-import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.feed.FeedEntryKeyword;
@@ -28,8 +27,8 @@ import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQuery;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class FeedEntryStatusDAO extends GenericDAO {
@@ -42,9 +41,8 @@ public class FeedEntryStatusDAO extends GenericDAO {
private final FeedEntryTagDAO feedEntryTagDAO;
private final CommaFeedConfiguration config;
- @Inject
- public FeedEntryStatusDAO(SessionFactory sessionFactory, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
- super(sessionFactory);
+ public FeedEntryStatusDAO(EntityManager entityManager, FeedEntryTagDAO feedEntryTagDAO, CommaFeedConfiguration config) {
+ super(entityManager, FeedEntryStatus.class);
this.feedEntryTagDAO = feedEntryTagDAO;
this.config = config;
}
@@ -60,8 +58,8 @@ public class FeedEntryStatusDAO extends GenericDAO {
*/
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) {
- Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
- boolean read = unreadThreshold != null && entry.getPublished().isBefore(unreadThreshold);
+ Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
+ boolean read = statusesInstantThreshold != null && entry.getPublished().isBefore(statusesInstantThreshold);
status = new FeedEntryStatus(user, sub, entry);
status.setRead(read);
status.setMarkable(!read);
@@ -84,6 +82,7 @@ public class FeedEntryStatusDAO extends GenericDAO {
boolean includeContent) {
JPAQuery query = query().selectFrom(STATUS).where(STATUS.user.eq(user), STATUS.starred.isTrue());
if (includeContent) {
+ query.join(STATUS.entry).fetchJoin();
query.join(STATUS.entry.content).fetchJoin();
}
@@ -105,7 +104,7 @@ public class FeedEntryStatusDAO extends GenericDAO {
query.limit(limit);
}
- setTimeout(query, config.getApplicationSettings().getQueryTimeout());
+ setTimeout(query, config.database().queryTimeout());
List statuses = query.fetch();
statuses.forEach(s -> s.setMarkable(true));
@@ -179,7 +178,7 @@ public class FeedEntryStatusDAO extends GenericDAO {
query.limit(limit);
}
- setTimeout(query, config.getApplicationSettings().getQueryTimeout());
+ setTimeout(query, config.database().queryTimeout());
List statuses = new ArrayList<>();
List tuples = query.fetch();
@@ -217,9 +216,9 @@ public class FeedEntryStatusDAO extends GenericDAO {
or.or(STATUS.read.isNull());
or.or(STATUS.read.isFalse());
- Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
- if (unreadThreshold != null) {
- return or.and(ENTRY.published.goe(unreadThreshold));
+ Instant statusesInstantThreshold = config.database().cleanup().statusesInstantThreshold();
+ if (statusesInstantThreshold != null) {
+ return or.and(ENTRY.published.goe(statusesInstantThreshold));
} else {
return or;
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
index 5d87d729..bb00ad12 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedEntryTagDAO.java
@@ -4,24 +4,21 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.QFeedEntryTag;
import com.commafeed.backend.model.User;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class FeedEntryTagDAO extends GenericDAO {
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
- @Inject
- public FeedEntryTagDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public FeedEntryTagDAO(EntityManager entityManager) {
+ super(entityManager, FeedEntryTag.class);
}
public List findByUser(User user) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java
index 05b1cde7..990ac21c 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/FeedSubscriptionDAO.java
@@ -5,12 +5,11 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
-import org.hibernate.SessionFactory;
-import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
+import org.hibernate.event.spi.PostCommitInsertEventListener;
import org.hibernate.event.spi.PostInsertEvent;
-import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.persister.entity.EntityPersister;
import com.commafeed.backend.model.AbstractModel;
@@ -23,28 +22,28 @@ import com.commafeed.backend.model.User;
import com.google.common.collect.Iterables;
import com.querydsl.jpa.JPQLQuery;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class FeedSubscriptionDAO extends GenericDAO {
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
- private final SessionFactory sessionFactory;
+ private final EntityManager entityManager;
- @Inject
- public FeedSubscriptionDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
- this.sessionFactory = sessionFactory;
+ public FeedSubscriptionDAO(EntityManager entityManager) {
+ super(entityManager, FeedSubscription.class);
+ this.entityManager = entityManager;
}
public void onPostCommitInsert(Consumer consumer) {
- sessionFactory.unwrap(SessionFactoryImplementor.class)
+ entityManager.unwrap(SharedSessionContractImplementor.class)
+ .getFactory()
.getServiceRegistry()
.getService(EventListenerRegistry.class)
.getEventListenerGroup(EventType.POST_COMMIT_INSERT)
- .appendListener(new PostInsertEventListener() {
+ .appendListener(new PostCommitInsertEventListener() {
@Override
public void onPostInsert(PostInsertEvent event) {
if (event.getEntity() instanceof FeedSubscription s) {
@@ -56,6 +55,11 @@ public class FeedSubscriptionDAO extends GenericDAO {
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
+
+ @Override
+ public void onPostInsertCommitFailed(PostInsertEvent event) {
+ // do nothing
+ }
});
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java
index b2a75bfb..8041295b 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/GenericDAO.java
@@ -2,7 +2,7 @@ package com.commafeed.backend.dao;
import java.util.Collection;
-import org.hibernate.SessionFactory;
+import org.hibernate.Session;
import org.hibernate.jpa.SpecHints;
import com.commafeed.backend.model.AbstractModel;
@@ -12,45 +12,43 @@ import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.impl.JPAUpdateClause;
-import io.dropwizard.hibernate.AbstractDAO;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
-public abstract class GenericDAO extends AbstractDAO {
+@RequiredArgsConstructor
+public abstract class GenericDAO {
- protected GenericDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
- }
+ private final EntityManager entityManager;
+ private final Class entityClass;
protected JPAQueryFactory query() {
- return new JPAQueryFactory(currentSession());
+ return new JPAQueryFactory(entityManager);
}
protected JPAUpdateClause updateQuery(EntityPath entityPath) {
- return new JPAUpdateClause(currentSession(), entityPath);
+ return new JPAUpdateClause(entityManager, entityPath);
}
protected JPADeleteClause deleteQuery(EntityPath entityPath) {
- return new JPADeleteClause(currentSession(), entityPath);
+ return new JPADeleteClause(entityManager, entityPath);
}
+ @SuppressWarnings("deprecation")
public void saveOrUpdate(T model) {
- persist(model);
+ entityManager.unwrap(Session.class).saveOrUpdate(model);
}
public void saveOrUpdate(Collection models) {
- models.forEach(this::persist);
- }
-
- public void update(T model) {
- currentSession().merge(model);
+ models.forEach(this::saveOrUpdate);
}
public T findById(Long id) {
- return get(id);
+ return entityManager.find(entityClass, id);
}
public void delete(T object) {
if (object != null) {
- currentSession().remove(object);
+ entityManager.remove(object);
}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java
index 87be4676..5caa1ed2 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UnitOfWork.java
@@ -1,68 +1,20 @@
package com.commafeed.backend.dao;
-import org.hibernate.Session;
-import org.hibernate.SessionFactory;
-import org.hibernate.Transaction;
-import org.hibernate.context.internal.ManagedSessionContext;
-
-import jakarta.inject.Inject;
+import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.inject.Singleton;
-import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class UnitOfWork {
- private final SessionFactory sessionFactory;
-
- public void run(SessionRunner sessionRunner) {
+ public void run(SessionRunner runner) {
call(() -> {
- sessionRunner.runInSession();
+ runner.runInSession();
return null;
});
}
- public T call(SessionRunnerReturningValue sessionRunner) {
- T t = null;
-
- boolean sessionAlreadyBound = ManagedSessionContext.hasBind(sessionFactory);
- try (Session session = sessionFactory.openSession()) {
- if (!sessionAlreadyBound) {
- ManagedSessionContext.bind(session);
- }
-
- Transaction tx = session.beginTransaction();
- try {
- t = sessionRunner.runInSession();
- commitTransaction(tx);
- } catch (Exception e) {
- rollbackTransaction(tx);
- UnitOfWork.rethrow(e);
- }
- } finally {
- if (!sessionAlreadyBound) {
- ManagedSessionContext.unbind(sessionFactory);
- }
- }
-
- return t;
- }
-
- private static void rollbackTransaction(Transaction tx) {
- if (tx != null && tx.isActive()) {
- tx.rollback();
- }
- }
-
- private static void commitTransaction(Transaction tx) {
- if (tx != null && tx.isActive()) {
- tx.commit();
- }
- }
-
- @SuppressWarnings("unchecked")
- private static void rethrow(Exception e) throws E {
- throw (E) e;
+ public T call(SessionRunnerReturningValue runner) {
+ return QuarkusTransaction.joiningExisting().call(runner::runInSession);
}
@FunctionalInterface
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java
index ae7d87f4..ea7e475c 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserDAO.java
@@ -1,21 +1,18 @@
package com.commafeed.backend.dao;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class UserDAO extends GenericDAO {
private static final QUser USER = QUser.user;
- @Inject
- public UserDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public UserDAO(EntityManager entityManager) {
+ super(entityManager, User.class);
}
public User findByName(String name) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java
index 37c23e92..81d06301 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserRoleDAO.java
@@ -4,24 +4,21 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.QUserRole;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class UserRoleDAO extends GenericDAO {
private static final QUserRole ROLE = QUserRole.userRole;
- @Inject
- public UserRoleDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public UserRoleDAO(EntityManager entityManager) {
+ super(entityManager, UserRole.class);
}
public List findAll() {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java
index a95450ad..89048624 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/dao/UserSettingsDAO.java
@@ -1,22 +1,19 @@
package com.commafeed.backend.dao;
-import org.hibernate.SessionFactory;
-
import com.commafeed.backend.model.QUserSettings;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.persistence.EntityManager;
@Singleton
public class UserSettingsDAO extends GenericDAO {
private static final QUserSettings SETTINGS = QUserSettings.userSettings;
- @Inject
- public UserSettingsDAO(SessionFactory sessionFactory) {
- super(sessionFactory);
+ public UserSettingsDAO(EntityManager entityManager) {
+ super(entityManager, UserSettings.class);
}
public UserSettings findByUser(User user) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java
index 7b814fef..32a0ee11 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/DefaultFaviconFetcher.java
@@ -10,7 +10,7 @@ import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed;
-import jakarta.inject.Inject;
+import jakarta.annotation.Priority;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -20,8 +20,9 @@ import lombok.extern.slf4j.Slf4j;
*
*/
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
+@Priority(Integer.MIN_VALUE)
public class DefaultFaviconFetcher extends AbstractFaviconFetcher {
private final HttpGetter getter;
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java
index fd9722f0..fb0f0fb9 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/FacebookFaviconFetcher.java
@@ -11,13 +11,12 @@ import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class FacebookFaviconFetcher extends AbstractFaviconFetcher {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
index def1dda5..ea219e3e 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/favicon/YoutubeFaviconFetcher.java
@@ -22,13 +22,12 @@ import com.google.api.services.youtube.model.ChannelListResponse;
import com.google.api.services.youtube.model.PlaylistListResponse;
import com.google.api.services.youtube.model.Thumbnail;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
@@ -43,8 +42,8 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
return null;
}
- String googleAuthKey = config.getApplicationSettings().getGoogleAuthKey();
- if (googleAuthKey == null) {
+ Optional googleAuthKey = config.googleAuthKey();
+ if (googleAuthKey.isEmpty()) {
log.debug("no google auth key configured");
return null;
}
@@ -63,13 +62,13 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
ChannelListResponse response = null;
if (userId.isPresent()) {
log.debug("contacting youtube api for user {}", userId.get().getValue());
- response = fetchForUser(youtube, googleAuthKey, userId.get().getValue());
+ response = fetchForUser(youtube, googleAuthKey.get(), userId.get().getValue());
} else if (channelId.isPresent()) {
log.debug("contacting youtube api for channel {}", channelId.get().getValue());
- response = fetchForChannel(youtube, googleAuthKey, channelId.get().getValue());
+ response = fetchForChannel(youtube, googleAuthKey.get(), channelId.get().getValue());
} else if (playlistId.isPresent()) {
log.debug("contacting youtube api for playlist {}", playlistId.get().getValue());
- response = fetchForPlaylist(youtube, googleAuthKey, playlistId.get().getValue());
+ response = fetchForPlaylist(youtube, googleAuthKey.get(), playlistId.get().getValue());
}
if (response == null || response.isEmpty() || CollectionUtils.isEmpty(response.getItems())) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
index 18f9f2a0..03be80a9 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedFetcher.java
@@ -2,7 +2,7 @@ package com.commafeed.backend.feed;
import java.io.IOException;
import java.time.Instant;
-import java.util.Set;
+import java.util.List;
import org.apache.commons.codec.binary.StringUtils;
@@ -15,22 +15,26 @@ import com.commafeed.backend.feed.parser.FeedParserResult;
import com.commafeed.backend.urlprovider.FeedURLProvider;
import com.rometools.rome.io.FeedException;
-import jakarta.inject.Inject;
+import io.quarkus.arc.All;
import jakarta.inject.Singleton;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Fetches a feed then parses it
*/
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class FeedFetcher {
private final FeedParser parser;
private final HttpGetter getter;
- private final Set urlProviders;
+ private final List urlProviders;
+
+ public FeedFetcher(FeedParser parser, HttpGetter getter, @All List urlProviders) {
+ this.parser = parser;
+ this.getter = getter;
+ this.urlProviders = urlProviders;
+ }
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException {
@@ -87,7 +91,7 @@ public class FeedFetcher {
result.getDuration());
}
- private static String extractFeedUrl(Set urlProviders, String url, String urlContent) {
+ private static String extractFeedUrl(List urlProviders, String url, String urlContent) {
for (FeedURLProvider urlProvider : urlProviders) {
String feedUrl = urlProvider.get(url, urlContent);
if (feedUrl != null) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
index 9f39cbf0..bd1ece18 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshEngine.java
@@ -1,6 +1,5 @@
package com.commafeed.backend.feed;
-import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.BlockingDeque;
@@ -21,14 +20,12 @@ import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.AbstractModel;
import com.commafeed.backend.model.Feed;
-import io.dropwizard.lifecycle.Managed;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
-public class FeedRefreshEngine implements Managed {
+public class FeedRefreshEngine {
private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO;
@@ -45,7 +42,6 @@ public class FeedRefreshEngine implements Managed {
private final ThreadPoolExecutor workerExecutor;
private final ThreadPoolExecutor databaseUpdaterExecutor;
- @Inject
public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
CommaFeedConfiguration config, MetricRegistry metrics) {
this.unitOfWork = unitOfWork;
@@ -60,15 +56,14 @@ public class FeedRefreshEngine implements Managed {
this.feedProcessingLoopExecutor = Executors.newSingleThreadExecutor();
this.refillLoopExecutor = Executors.newSingleThreadExecutor();
this.refillExecutor = newDiscardingSingleThreadExecutorService();
- this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads());
- this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads());
+ this.workerExecutor = newBlockingExecutorService(config.feedRefresh().httpThreads());
+ this.databaseUpdaterExecutor = newBlockingExecutorService(config.feedRefresh().databaseThreads());
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge) workerExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge) databaseUpdaterExecutor::getActiveCount);
}
- @Override
public void start() {
startFeedProcessingLoop();
startRefillLoop();
@@ -165,22 +160,20 @@ public class FeedRefreshEngine implements Managed {
private List getNextUpdatableFeeds(int max) {
return unitOfWork.call(() -> {
- Instant lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
- ? Instant.now().minus(Duration.ofDays(30))
- : null;
+ Instant lastLoginThreshold = config.feedRefresh().userInactivityPeriod().isZero() ? null
+ : Instant.now().minus(config.feedRefresh().userInactivityPeriod());
List feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
- Instant nextUpdateDate = Instant.now().plus(Duration.ofMinutes(config.getApplicationSettings().getRefreshIntervalMinutes()));
+ Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval());
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
return feeds;
});
}
private int getBatchSize() {
- return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
+ return Math.min(100, 3 * config.feedRefresh().httpThreads());
}
- @Override
public void stop() {
this.feedProcessingLoopExecutor.shutdownNow();
this.refillLoopExecutor.shutdownNow();
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java
index d4097332..8aea399e 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshIntervalCalculator.java
@@ -6,24 +6,22 @@ import java.time.temporal.ChronoUnit;
import com.commafeed.CommaFeedConfiguration;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
public class FeedRefreshIntervalCalculator {
- private final boolean heavyLoad;
- private final int refreshIntervalMinutes;
+ private final Duration refreshInterval;
+ private final boolean empiricalInterval;
- @Inject
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {
- this.heavyLoad = config.getApplicationSettings().getHeavyLoad();
- this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes();
+ this.refreshInterval = config.feedRefresh().interval();
+ this.empiricalInterval = config.feedRefresh().intervalEmpirical();
}
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) {
Instant defaultRefreshInterval = getDefaultRefreshInterval();
- return heavyLoad ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval)
+ return empiricalInterval ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval)
: defaultRefreshInterval;
}
@@ -33,7 +31,7 @@ public class FeedRefreshIntervalCalculator {
public Instant onFetchError(int errorCount) {
int retriesBeforeDisable = 3;
- if (errorCount < retriesBeforeDisable || !heavyLoad) {
+ if (errorCount < retriesBeforeDisable || !empiricalInterval) {
return getDefaultRefreshInterval();
}
@@ -42,7 +40,7 @@ public class FeedRefreshIntervalCalculator {
}
private Instant getDefaultRefreshInterval() {
- return Instant.now().plus(Duration.ofMinutes(refreshIntervalMinutes));
+ return Instant.now().plus(refreshInterval);
}
private Instant computeRefreshIntervalForHeavyLoad(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
index ea891b03..b2237929 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshUpdater.java
@@ -32,7 +32,6 @@ import com.commafeed.frontend.ws.WebSocketMessageBuilder;
import com.commafeed.frontend.ws.WebSocketSessions;
import com.google.common.util.concurrent.Striped;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -58,7 +57,6 @@ public class FeedRefreshUpdater {
private final Meter feedUpdated;
private final Meter entryInserted;
- @Inject
public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService, MetricRegistry metrics,
FeedSubscriptionDAO feedSubscriptionDAO, CacheService cache, WebSocketSessions webSocketSessions) {
this.unitOfWork = unitOfWork;
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java
index fa855f2a..11421313 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/FeedRefreshWorker.java
@@ -16,7 +16,6 @@ import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
import com.commafeed.backend.model.Feed;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
@@ -32,7 +31,6 @@ public class FeedRefreshWorker {
private final CommaFeedConfiguration config;
private final Meter feedFetched;
- @Inject
public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config,
MetricRegistry metrics) {
this.refreshIntervalCalculator = refreshIntervalCalculator;
@@ -51,14 +49,14 @@ public class FeedRefreshWorker {
List entries = result.feed().entries();
- Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
+ int maxFeedCapacity = config.database().cleanup().maxFeedCapacity();
if (maxFeedCapacity > 0) {
entries = entries.stream().limit(maxFeedCapacity).toList();
}
- Integer maxEntriesAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays();
- if (maxEntriesAgeDays > 0) {
- Instant threshold = Instant.now().minus(Duration.ofDays(maxEntriesAgeDays));
+ Duration maxEntriesAgeDays = config.database().cleanup().entriesMaxAge();
+ if (!maxEntriesAgeDays.isZero()) {
+ Instant threshold = Instant.now().minus(maxEntriesAgeDays);
entries = entries.stream().filter(entry -> entry.published().isAfter(threshold)).toList();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java
index 2f62c530..d25f7c13 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/feed/parser/FeedParser.java
@@ -38,14 +38,13 @@ import com.rometools.rome.feed.synd.SyndLinkImpl;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.SyndFeedInput;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
/**
* Parses raw xml into a FeedParserResult object
*/
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class FeedParser {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java
index 5dc3329d..f451ac93 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLExporter.java
@@ -15,11 +15,10 @@ import com.rometools.opml.feed.opml.Attribute;
import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class OPMLExporter {
@@ -28,7 +27,7 @@ public class OPMLExporter {
public Opml export(User user) {
Opml opml = new Opml();
- opml.setFeedType("opml_1.1");
+ opml.setFeedType("opml_1.0");
opml.setTitle(String.format("%s subscriptions in CommaFeed", user.getName()));
opml.setCreated(new Date());
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java
index a4a00480..f28c5247 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/opml/OPMLImporter.java
@@ -17,13 +17,12 @@ import com.rometools.opml.feed.opml.Outline;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.WireFeedInput;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class OPMLImporter {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java
index 4bfc44af..480a1ba7 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Generator.java
@@ -4,10 +4,13 @@ import org.jdom2.Element;
import com.rometools.opml.feed.opml.Opml;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
/**
* Add missing title to the generated OPML
*
*/
+@RegisterForReflection
public class OPML11Generator extends com.rometools.opml.io.impl.OPML10Generator {
public OPML11Generator() {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java
index 3342a5c9..8770667a 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/OPML11Parser.java
@@ -9,10 +9,13 @@ import com.rometools.opml.io.impl.OPML10Parser;
import com.rometools.rome.feed.WireFeed;
import com.rometools.rome.io.FeedException;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
/**
* Support for OPML 1.1 parsing
*
*/
+@RegisterForReflection
public class OPML11Parser extends OPML10Parser {
public OPML11Parser() {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java
index c2154565..404364b2 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionConverter.java
@@ -6,10 +6,13 @@ import com.rometools.rome.feed.synd.SyndContentImpl;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.impl.ConverterForRSS090;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
/**
* Support description tag for RSS09
*
*/
+@RegisterForReflection
public class RSS090DescriptionConverter extends ConverterForRSS090 {
@Override
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java
index d30f82dc..abde98ed 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSS090DescriptionParser.java
@@ -8,10 +8,13 @@ import com.rometools.rome.feed.rss.Description;
import com.rometools.rome.feed.rss.Item;
import com.rometools.rome.io.impl.RSS090Parser;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
/**
* Support description tag for RSS09
*
*/
+@RegisterForReflection
public class RSS090DescriptionParser extends RSS090Parser {
@Override
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java
index 4c101230..6c7e11c7 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/rome/RSSRDF10Parser.java
@@ -10,6 +10,9 @@ import org.jdom2.Namespace;
import com.google.common.collect.Lists;
import com.rometools.rome.io.impl.RSS10Parser;
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
+@RegisterForReflection
public class RSSRDF10Parser extends RSS10Parser {
private static final String RSS_URI = "http://purl.org/rss/1.0/";
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java
index 5886f20a..f3e90352 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentCleaningService.java
@@ -21,12 +21,11 @@ import org.w3c.dom.css.CSSStyleDeclaration;
import com.steadystate.css.parser.CSSOMParser;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Slf4j
@Singleton
public class FeedEntryContentCleaningService {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java
index 1ad16ab5..879f42f3 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryContentService.java
@@ -12,11 +12,10 @@ import com.commafeed.backend.feed.parser.FeedParserResult.Enclosure;
import com.commafeed.backend.feed.parser.FeedParserResult.Media;
import com.commafeed.backend.model.FeedEntryContent;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class FeedEntryContentService {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java
index 2ca965a2..6832c58a 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryFilteringService.java
@@ -25,11 +25,10 @@ import org.jsoup.Jsoup;
import com.commafeed.backend.model.FeedEntry;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class FeedEntryFilteringService {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java
index d75e1915..7b789042 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryService.java
@@ -18,13 +18,12 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class FeedEntryService {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java
index 70f451c9..540956ab 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedEntryTagService.java
@@ -10,11 +10,10 @@ import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.User;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class FeedEntryTagService {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java
index 69a5ecf2..49cfd653 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedService.java
@@ -2,8 +2,8 @@ package com.commafeed.backend.service;
import java.io.IOException;
import java.time.Instant;
+import java.util.List;
import java.util.Objects;
-import java.util.Set;
import com.commafeed.backend.Digests;
import com.commafeed.backend.dao.FeedDAO;
@@ -14,19 +14,18 @@ import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.Models;
import com.google.common.io.Resources;
-import jakarta.inject.Inject;
+import io.quarkus.arc.All;
import jakarta.inject.Singleton;
@Singleton
public class FeedService {
private final FeedDAO feedDAO;
- private final Set faviconFetchers;
+ private final List faviconFetchers;
private final Favicon defaultFavicon;
- @Inject
- public FeedService(FeedDAO feedDAO, Set faviconFetchers) {
+ public FeedService(FeedDAO feedDAO, @All List faviconFetchers) {
this.feedDAO = feedDAO;
this.faviconFetchers = faviconFetchers;
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java
index 444b9f34..5f48a7c1 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/FeedSubscriptionService.java
@@ -21,7 +21,6 @@ import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.UnreadCount;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
@@ -37,7 +36,6 @@ public class FeedSubscriptionService {
private final CacheService cache;
private final CommaFeedConfiguration config;
- @Inject
public FeedSubscriptionService(FeedDAO feedDAO, FeedEntryStatusDAO feedEntryStatusDAO, FeedSubscriptionDAO feedSubscriptionDAO,
FeedService feedService, FeedRefreshEngine feedRefreshEngine, CacheService cache, CommaFeedConfiguration config) {
this.feedDAO = feedDAO;
@@ -63,7 +61,7 @@ public class FeedSubscriptionService {
public long subscribe(User user, String url, String title, FeedCategory category, int position) {
- final String pubUrl = config.getApplicationSettings().getPublicUrl();
+ final String pubUrl = config.publicUrl();
if (StringUtils.isBlank(pubUrl)) {
throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set");
}
@@ -71,7 +69,7 @@ public class FeedSubscriptionService {
throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance");
}
- Integer maxFeedsPerUser = config.getApplicationSettings().getMaxFeedsPerUser();
+ Integer maxFeedsPerUser = config.database().cleanup().maxFeedsPerUser();
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) {
String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)",
maxFeedsPerUser);
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java
index e50556f0..758945aa 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/MailService.java
@@ -4,10 +4,9 @@ import java.util.Optional;
import java.util.Properties;
import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
+import com.commafeed.CommaFeedConfiguration.Smtp;
import com.commafeed.backend.model.User;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.mail.Authenticator;
import jakarta.mail.Message;
@@ -22,27 +21,29 @@ import lombok.RequiredArgsConstructor;
* Mailing service
*
*/
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class MailService {
private final CommaFeedConfiguration config;
public void sendMail(User user, String subject, String content) throws Exception {
+ Optional settings = config.smtp();
+ if (settings.isEmpty()) {
+ throw new IllegalArgumentException("SMTP settings not configured");
+ }
- ApplicationSettings settings = config.getApplicationSettings();
-
- final String username = settings.getSmtpUserName();
- final String password = settings.getSmtpPassword();
- final String fromAddress = Optional.ofNullable(settings.getSmtpFromAddress()).orElse(settings.getSmtpUserName());
+ final String username = settings.get().userName();
+ final String password = settings.get().password();
+ final String fromAddress = Optional.ofNullable(settings.get().fromAddress()).orElse(settings.get().userName());
String dest = user.getEmail();
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
- props.put("mail.smtp.starttls.enable", String.valueOf(settings.isSmtpTls()));
- props.put("mail.smtp.host", settings.getSmtpHost());
- props.put("mail.smtp.port", String.valueOf(settings.getSmtpPort()));
+ props.put("mail.smtp.starttls.enable", String.valueOf(settings.get().tls()));
+ props.put("mail.smtp.host", settings.get().host());
+ props.put("mail.smtp.port", String.valueOf(settings.get().port()));
Session session = Session.getInstance(props, new Authenticator() {
@Override
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java
index 26aa70b9..a6f7d5c6 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/PasswordEncryptionService.java
@@ -12,7 +12,6 @@ import javax.crypto.spec.PBEKeySpec;
import org.apache.commons.lang3.StringUtils;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -20,7 +19,7 @@ import lombok.extern.slf4j.Slf4j;
// taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
@SuppressWarnings("serial")
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class PasswordEncryptionService implements Serializable {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java
index 76a0df1c..b56d4c47 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/UserService.java
@@ -24,11 +24,10 @@ import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.service.internal.PostLoginActivities;
import com.google.common.base.Preconditions;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class UserService {
@@ -117,8 +116,7 @@ public class UserService {
public User register(String name, String password, String email, Collection roles, boolean forceRegistration) {
if (!forceRegistration) {
- Preconditions.checkState(config.getApplicationSettings().getAllowRegistrations(),
- "Registrations are closed on this CommaFeed instance");
+ Preconditions.checkState(config.users().allowRegistrations(), "Registrations are closed on this CommaFeed instance");
}
Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java
index a8f54d4e..d64bb25b 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java
@@ -14,7 +14,6 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
@@ -35,7 +34,6 @@ public class DatabaseCleaningService {
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final Meter entriesDeletedMeter;
- @Inject
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) {
this.unitOfWork = unitOfWork;
@@ -43,7 +41,7 @@ public class DatabaseCleaningService {
this.feedEntryDAO = feedEntryDAO;
this.feedEntryContentDAO = feedEntryContentDAO;
this.feedEntryStatusDAO = feedEntryStatusDAO;
- this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize();
+ this.batchSize = config.database().cleanup().batchSize();
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java
index 865118e6..a5bbe584 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java
@@ -1,109 +1,41 @@
package com.commafeed.backend.service.db;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.hibernate.Session;
-import org.hibernate.SessionFactory;
+import org.kohsuke.MetaInfServices;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.service.UserService;
-import io.dropwizard.lifecycle.Managed;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
-import liquibase.Scope;
-import liquibase.UpdateSummaryEnum;
-import liquibase.changelog.ChangeLogParameters;
-import liquibase.command.CommandScope;
-import liquibase.command.core.UpdateCommandStep;
-import liquibase.command.core.helpers.DatabaseChangelogCommandStep;
-import liquibase.command.core.helpers.DbUrlConnectionArgumentsCommandStep;
-import liquibase.command.core.helpers.ShowSummaryArgument;
import liquibase.database.Database;
-import liquibase.database.DatabaseFactory;
import liquibase.database.core.PostgresDatabase;
-import liquibase.database.jvm.JdbcConnection;
-import liquibase.exception.DatabaseException;
-import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.structure.DatabaseObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
-public class DatabaseStartupService implements Managed {
+public class DatabaseStartupService {
private final UnitOfWork unitOfWork;
- private final SessionFactory sessionFactory;
private final UserDAO userDAO;
private final UserService userService;
private final CommaFeedConfiguration config;
- @Override
- public void start() {
- updateSchema();
+ public void populateInitialData() {
long count = unitOfWork.call(userDAO::count);
if (count == 0) {
unitOfWork.run(this::initialData);
}
}
- private void updateSchema() {
- log.info("checking if database schema needs updating");
-
- try (Session session = sessionFactory.openSession()) {
- session.doWork(connection -> {
- try {
- JdbcConnection jdbcConnection = new JdbcConnection(connection);
- Database database = getDatabase(jdbcConnection);
-
- Map scopeObjects = new HashMap<>();
- scopeObjects.put(Scope.Attr.database.name(), database);
- scopeObjects.put(Scope.Attr.resourceAccessor.name(),
- new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader()));
-
- Scope.child(scopeObjects, () -> {
- CommandScope command = new CommandScope(UpdateCommandStep.COMMAND_NAME);
- command.addArgumentValue(DbUrlConnectionArgumentsCommandStep.DATABASE_ARG, database);
- command.addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, "migrations.xml");
- command.addArgumentValue(DatabaseChangelogCommandStep.CHANGELOG_PARAMETERS, new ChangeLogParameters(database));
- command.addArgumentValue(ShowSummaryArgument.SHOW_SUMMARY, UpdateSummaryEnum.OFF);
- command.execute();
- });
-
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
- }
-
- log.info("database schema is up to date");
- }
-
- private Database getDatabase(JdbcConnection connection) throws DatabaseException {
- Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);
- if (database instanceof PostgresDatabase) {
- database = new PostgresDatabase() {
- @Override
- public String escapeObjectName(String objectName, Class 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;
+ }
+ }
+
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java
deleted file mode 100644
index 75b839c4..00000000
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.commafeed.backend.service.db;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.stream.Stream;
-
-import org.apache.commons.lang3.StringUtils;
-
-import com.manticore.h2.H2MigrationTool;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-@RequiredArgsConstructor
-@Slf4j
-public class H2MigrationService {
-
- private static final String H2_FILE_SUFFIX = ".mv.db";
-
- public void migrateIfNeeded(Path path, String user, String password) {
- if (Files.notExists(path)) {
- return;
- }
-
- int format;
- try {
- format = getH2FileFormat(path);
- } catch (IOException e) {
- throw new RuntimeException("could not detect H2 format", e);
- }
-
- if (format == 2) {
- try {
- migrate(path, user, password, "2.1.214", "2.2.224");
- } catch (Exception e) {
- throw new RuntimeException("could not migrate H2 to format 3", e);
- }
- }
- }
-
- public int getH2FileFormat(Path path) throws IOException {
- try (BufferedReader reader = Files.newBufferedReader(path)) {
- String headers = reader.readLine();
-
- return Stream.of(headers.split(","))
- .filter(h -> h.startsWith("format:"))
- .map(h -> h.split(":")[1])
- .map(Integer::parseInt)
- .findFirst()
- .orElseThrow(() -> new RuntimeException("could not find format in H2 file headers"));
- }
- }
-
- private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception {
- log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion);
-
- Path scriptPath = path.resolveSibling("script-%d.sql".formatted(System.currentTimeMillis()));
- Path newVersionPath = path.resolveSibling("%s.%s%s".formatted(StringUtils.removeEnd(path.getFileName().toString(), H2_FILE_SUFFIX),
- getPatchVersion(toVersion), H2_FILE_SUFFIX));
- Path oldVersionBackupPath = path.resolveSibling("%s.%s.backup".formatted(path.getFileName(), getPatchVersion(fromVersion)));
-
- Files.deleteIfExists(scriptPath);
- Files.deleteIfExists(newVersionPath);
- Files.deleteIfExists(oldVersionBackupPath);
-
- H2MigrationTool.readDriverRecords();
- new H2MigrationTool().migrate(fromVersion, toVersion, path.toAbsolutePath().toString(), user, password,
- scriptPath.toAbsolutePath().toString(), "", "", false, false, "");
- if (!Files.exists(newVersionPath)) {
- throw new RuntimeException("H2 migration failed, new version file not found");
- }
-
- Files.move(path, oldVersionBackupPath);
- Files.move(newVersionPath, path);
- Files.delete(oldVersionBackupPath);
- Files.delete(scriptPath);
-
- log.info("migrated H2 database from format {} to format {}", fromVersion, toVersion);
- }
-
- private String getPatchVersion(String version) {
- return StringUtils.substringAfterLast(version, ".");
- }
-
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java b/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java
index 752ef81b..bd66f02b 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/service/internal/PostLoginActivities.java
@@ -7,11 +7,10 @@ import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class PostLoginActivities {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java
index ccd2a5d2..faa2757a 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/DemoAccountCleanupTask.java
@@ -9,12 +9,11 @@ import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.UserService;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Slf4j
public class DemoAccountCleanupTask extends ScheduledTask {
@@ -26,7 +25,7 @@ public class DemoAccountCleanupTask extends ScheduledTask {
@Override
protected void run() {
- if (!config.getApplicationSettings().getCreateDemoAccount()) {
+ if (!config.users().createDemoAccount()) {
return;
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java
index 5508cbd7..09351381 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java
@@ -5,11 +5,10 @@ import java.util.concurrent.TimeUnit;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.service.db.DatabaseCleaningService;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask {
@@ -18,7 +17,7 @@ public class EntriesExceedingFeedCapacityCleanupTask extends ScheduledTask {
@Override
public void run() {
- int maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
+ int maxFeedCapacity = config.database().cleanup().maxFeedCapacity();
if (maxFeedCapacity > 0) {
cleaner.cleanEntriesForFeedsExceedingCapacity(maxFeedCapacity);
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java
index fad42a75..4bc0cd32 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java
@@ -7,11 +7,10 @@ import java.util.concurrent.TimeUnit;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.service.db.DatabaseCleaningService;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class OldEntriesCleanupTask extends ScheduledTask {
@@ -20,9 +19,9 @@ public class OldEntriesCleanupTask extends ScheduledTask {
@Override
public void run() {
- int maxAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays();
- if (maxAgeDays > 0) {
- Instant threshold = Instant.now().minus(Duration.ofDays(maxAgeDays));
+ Duration entriesMaxAge = config.database().cleanup().entriesMaxAge();
+ if (!entriesMaxAge.isZero()) {
+ Instant threshold = Instant.now().minus(entriesMaxAge);
cleaner.cleanEntriesOlderThan(threshold);
}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java
index 0c19afe3..62e87c90 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java
@@ -6,11 +6,10 @@ import java.util.concurrent.TimeUnit;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.service.db.DatabaseCleaningService;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class OldStatusesCleanupTask extends ScheduledTask {
@@ -19,7 +18,7 @@ public class OldStatusesCleanupTask extends ScheduledTask {
@Override
public void run() {
- Instant threshold = config.getApplicationSettings().getUnreadThreshold();
+ Instant threshold = config.database().cleanup().statusesInstantThreshold();
if (threshold != null) {
cleaner.cleanStatusesOlderThan(threshold);
}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java
index 7b2afcaa..786b7708 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java
@@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit;
import com.commafeed.backend.service.db.DatabaseCleaningService;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class OrphanedContentsCleanupTask extends ScheduledTask {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java
index 4606dad3..81eb14ea 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java
@@ -4,11 +4,10 @@ import java.util.concurrent.TimeUnit;
import com.commafeed.backend.service.db.DatabaseCleaningService;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
public class OrphanedFeedsCleanupTask extends ScheduledTask {
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java
new file mode 100644
index 00000000..73a37bf0
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/backend/task/TaskScheduler.java
@@ -0,0 +1,28 @@
+package com.commafeed.backend.task;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import io.quarkus.arc.All;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class TaskScheduler {
+
+ private final List tasks;
+ private final ScheduledExecutorService executor;
+
+ public TaskScheduler(@All List tasks) {
+ this.tasks = tasks;
+ this.executor = Executors.newScheduledThreadPool(tasks.size());
+ }
+
+ public void start() {
+ tasks.forEach(task -> task.register(executor));
+ }
+
+ public void stop() {
+ executor.shutdownNow();
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java
index 36913423..b2bf773c 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/InPageReferenceFeedURLProvider.java
@@ -4,6 +4,9 @@ import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
+import jakarta.inject.Singleton;
+
+@Singleton
public class InPageReferenceFeedURLProvider implements FeedURLProvider {
@Override
diff --git a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java
index ab94a1c0..882da01f 100644
--- a/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java
+++ b/commafeed-server/src/main/java/com/commafeed/backend/urlprovider/YoutubeFeedURLProvider.java
@@ -3,12 +3,15 @@ package com.commafeed.backend.urlprovider;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import jakarta.inject.Singleton;
+
/**
* Workaround for Youtube channels
*
* converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL
* https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
*/
+@Singleton
public class YoutubeFeedURLProvider implements FeedURLProvider {
private static final Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE);
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java
deleted file mode 100644
index 0e685686..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheck.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.commafeed.frontend.auth;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-import com.commafeed.backend.model.UserRole.Role;
-
-@Inherited
-@Target({ ElementType.PARAMETER })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface SecurityCheck {
-
- /**
- * Roles needed.
- */
- Role value() default Role.USER;
-
- boolean apiKeyAllowed() default false;
-}
\ No newline at end of file
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java
deleted file mode 100644
index 2e14bd7a..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package com.commafeed.frontend.auth;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-
-import org.glassfish.jersey.server.ContainerRequest;
-
-import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.backend.dao.UserDAO;
-import com.commafeed.backend.model.User;
-import com.commafeed.backend.model.UserRole.Role;
-import com.commafeed.backend.service.UserService;
-import com.commafeed.frontend.session.SessionHelper;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.ws.rs.WebApplicationException;
-import jakarta.ws.rs.core.HttpHeaders;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-public class SecurityCheckFactory implements Function {
-
- private static final String PREFIX = "Basic";
-
- private final UserDAO userDAO;
- private final UserService userService;
- private final CommaFeedConfiguration config;
- private final HttpServletRequest request;
- private final Role role;
- private final boolean apiKeyAllowed;
-
- @Override
- public User apply(ContainerRequest req) {
- Optional user = apiKeyLogin();
- if (user.isEmpty()) {
- user = basicAuthenticationLogin();
- }
- if (user.isEmpty()) {
- user = cookieSessionLogin(new SessionHelper(request));
- }
-
- if (user.isPresent()) {
- Set roles = userService.getRoles(user.get());
- if (roles.contains(role)) {
- return user.get();
- } else {
- throw buildWebApplicationException(Response.Status.FORBIDDEN, "You don't have the required role to access this resource.");
- }
- } else {
- throw buildWebApplicationException(Response.Status.UNAUTHORIZED, "Credentials are required to access this resource.");
- }
- }
-
- Optional cookieSessionLogin(SessionHelper sessionHelper) {
- Optional loggedInUser = sessionHelper.getLoggedInUserId().map(userDAO::findById);
- loggedInUser.ifPresent(userService::performPostLoginActivities);
- return loggedInUser;
- }
-
- private Optional basicAuthenticationLogin() {
- String header = request.getHeader(HttpHeaders.AUTHORIZATION);
- if (header != null) {
- int space = header.indexOf(' ');
- if (space > 0) {
- String method = header.substring(0, space);
- if (PREFIX.equalsIgnoreCase(method)) {
- byte[] decodedBytes = Base64.getDecoder().decode(header.substring(space + 1));
- String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1);
- int i = decoded.indexOf(':');
- if (i > 0) {
- String username = decoded.substring(0, i);
- String password = decoded.substring(i + 1);
- return userService.login(username, password);
- }
- }
- }
- }
- return Optional.empty();
- }
-
- private Optional apiKeyLogin() {
- String apiKey = request.getParameter("apiKey");
- if (apiKey != null && apiKeyAllowed) {
- return userService.login(apiKey);
- }
- return Optional.empty();
- }
-
- private WebApplicationException buildWebApplicationException(Response.Status status, String message) {
- Map response = new HashMap<>();
- response.put("message", message);
- response.put("allowRegistrations", config.getApplicationSettings().getAllowRegistrations());
- return new WebApplicationException(Response.status(status).entity(response).type(MediaType.APPLICATION_JSON).build());
- }
-
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java b/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java
deleted file mode 100644
index 4e70d479..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.commafeed.frontend.auth;
-
-import java.util.function.Function;
-
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
-import org.glassfish.jersey.server.ContainerRequest;
-import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider;
-import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
-import org.glassfish.jersey.server.model.Parameter;
-import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
-
-import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.backend.dao.UserDAO;
-import com.commafeed.backend.model.User;
-import com.commafeed.backend.service.UserService;
-
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
-
-@Singleton
-public class SecurityCheckFactoryProvider extends AbstractValueParamProvider {
-
- private final UserService userService;
- private final UserDAO userDAO;
- private final CommaFeedConfiguration config;
- private final HttpServletRequest request;
-
- @Inject
- public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserDAO userDAO,
- UserService userService, CommaFeedConfiguration config, HttpServletRequest request) {
- super(() -> extractorProvider, Parameter.Source.UNKNOWN);
- this.userDAO = userDAO;
- this.userService = userService;
- this.config = config;
- this.request = request;
- }
-
- @Override
- protected Function createValueProvider(Parameter parameter) {
- final Class> classType = parameter.getRawType();
-
- SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class);
- if (securityCheck == null) {
- return null;
- }
-
- if (!classType.isAssignableFrom(User.class)) {
- return null;
- }
-
- return new SecurityCheckFactory(userDAO, userService, config, request, securityCheck.value(), securityCheck.apiKeyAllowed());
- }
-
- @RequiredArgsConstructor
- public static class Binder extends AbstractBinder {
-
- private final UserDAO userDAO;
- private final UserService userService;
- private final CommaFeedConfiguration config;
-
- @Override
- protected void configure() {
- bind(SecurityCheckFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class);
- bind(userDAO).to(UserDAO.class);
- bind(userService).to(UserService.class);
- bind(config).to(CommaFeedConfiguration.class);
- }
- }
-
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java
index c360d7cd..631e56a7 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Category.java
@@ -4,6 +4,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -11,6 +12,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "Entry details")
@Data
+@RegisterForReflection
public class Category implements Serializable {
@Schema(description = "category id", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java
index 9e7d6483..9d51ee62 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entries.java
@@ -4,6 +4,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -11,6 +12,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "List of entries with some metadata")
@Data
+@RegisterForReflection
public class Entries implements Serializable {
@Schema(description = "name of the feed or the category requested", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java
index 87624d77..b7add91f 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Entry.java
@@ -19,6 +19,7 @@ import com.rometools.rome.feed.synd.SyndEnclosureImpl;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -26,6 +27,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "Entry details")
@Data
+@RegisterForReflection
public class Entry implements Serializable {
@Schema(description = "entry id", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java
index ffc4cca3..643ad6a5 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/FeedInfo.java
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
import java.io.Serializable;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -9,6 +10,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "Feed details")
@Data
+@RegisterForReflection
public class FeedInfo implements Serializable {
@Schema(description = "url", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java
index 0eee703b..f7ffd7cf 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/ServerInfo.java
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
import java.io.Serializable;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -9,6 +10,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "Server infos")
@Data
+@RegisterForReflection
public class ServerInfo implements Serializable {
@Schema
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java
index 28850422..e28412f0 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Settings.java
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
import java.io.Serializable;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -9,6 +10,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "User settings")
@Data
+@RegisterForReflection
public class Settings implements Serializable {
@Schema(description = "user's preferred language, english if none", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java
index 1a022af2..902de115 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/Subscription.java
@@ -8,6 +8,7 @@ import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -15,6 +16,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "User information")
@Data
+@RegisterForReflection
public class Subscription implements Serializable {
@Schema(description = "subscription id", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java
index 4f946ea2..6ecd7a1a 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/UnreadCount.java
@@ -3,12 +3,14 @@ package com.commafeed.frontend.model;
import java.io.Serializable;
import java.time.Instant;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "Unread count")
@Data
+@RegisterForReflection
public class UnreadCount implements Serializable {
@Schema
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java
index 7c7d332a..43d5faea 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/UserModel.java
@@ -3,6 +3,7 @@ package com.commafeed.frontend.model;
import java.io.Serializable;
import java.time.Instant;
+import io.quarkus.runtime.annotations.RegisterForReflection;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import lombok.Data;
@@ -10,6 +11,7 @@ import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "User information")
@Data
+@RegisterForReflection
public class UserModel implements Serializable {
@Schema(description = "user id", requiredMode = RequiredMode.REQUIRED)
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java
deleted file mode 100644
index c5dc4c98..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/LoginRequest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.commafeed.frontend.model.request;
-
-import java.io.Serializable;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
-import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.Size;
-import lombok.Data;
-
-@SuppressWarnings("serial")
-@Data
-@Schema
-public class LoginRequest implements Serializable {
-
- @Schema(description = "username", requiredMode = RequiredMode.REQUIRED)
- @Size(min = 3, max = 32)
- private String name;
-
- @Schema(description = "password", requiredMode = RequiredMode.REQUIRED)
- @NotEmpty
- @Size(max = 128)
- private String password;
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java
index c066a44d..1c2e1612 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/ProfileModificationRequest.java
@@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable;
-import com.commafeed.frontend.auth.ValidPassword;
+import com.commafeed.security.password.ValidPassword;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java
index bdf84ec9..cc27052e 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/model/request/RegistrationRequest.java
@@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable;
-import com.commafeed.frontend.auth.ValidPassword;
+import com.commafeed.security.password.ValidPassword;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java
index 1eccadb7..7f4f2c27 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/AdminREST.java
@@ -7,10 +7,7 @@ import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.CommaFeedApplication;
-import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.model.User;
@@ -18,14 +15,14 @@ import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.service.PasswordEncryptionService;
import com.commafeed.backend.service.UserService;
-import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.AdminSaveUserRequest;
import com.commafeed.frontend.model.request.IDRequest;
+import com.commafeed.security.AuthenticationContext;
+import com.commafeed.security.Roles;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -33,8 +30,9 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -46,30 +44,29 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import lombok.RequiredArgsConstructor;
-@Path("/admin")
+@Path("/rest/admin")
+@RolesAllowed(Roles.ADMIN)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Tag(name = "Admin")
public class AdminREST {
+ private final AuthenticationContext authenticationContext;
private final UserDAO userDAO;
private final UserRoleDAO userRoleDAO;
private final UserService userService;
private final PasswordEncryptionService encryptionService;
- private final CommaFeedConfiguration config;
private final MetricRegistry metrics;
@Path("/user/save")
@POST
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Save or update a user",
description = "Save or update a user. If the id is not specified, a new user will be created")
- @Timed
- public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
- @Parameter(required = true) AdminSaveUserRequest req) {
+ public Response adminSaveUser(@Parameter(required = true) AdminSaveUserRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getName());
@@ -87,6 +84,7 @@ public class AdminREST {
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
}
} else {
+ User user = authenticationContext.getCurrentUser();
if (req.getId().equals(user.getId()) && !req.isEnabled()) {
return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build();
}
@@ -121,14 +119,12 @@ public class AdminREST {
@Path("/user/get/{id}")
@GET
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Get user information",
description = "Get user information",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) })
- @Timed
- public Response adminGetUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
- @Parameter(description = "user id", required = true) @PathParam("id") Long id) {
+ public Response adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
Preconditions.checkNotNull(id);
User u = userDAO.findById(id);
UserModel userModel = new UserModel();
@@ -142,13 +138,12 @@ public class AdminREST {
@Path("/user/getAll")
@GET
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Get all users",
description = "Get all users",
responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserModel.class)))) })
- @Timed
- public Response adminGetUsers(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) {
+ public Response adminGetUsers() {
Map users = new HashMap<>();
for (UserRole role : userRoleDAO.findAll()) {
User u = role.getUser();
@@ -173,11 +168,9 @@ public class AdminREST {
@Path("/user/delete")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions")
- @Timed
- public Response adminDeleteUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
- @Parameter(required = true) IDRequest req) {
+ public Response adminDeleteUser(@Parameter(required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
@@ -185,6 +178,8 @@ public class AdminREST {
if (u == null) {
return Response.status(Status.NOT_FOUND).build();
}
+
+ User user = authenticationContext.getCurrentUser();
if (user.getId().equals(u.getId())) {
return Response.status(Status.FORBIDDEN).entity("You cannot delete your own user.").build();
}
@@ -192,24 +187,11 @@ public class AdminREST {
return Response.ok().build();
}
- @Path("/settings")
- @GET
- @UnitOfWork
- @Operation(
- summary = "Retrieve application settings",
- description = "Retrieve application settings",
- responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ApplicationSettings.class))) })
- @Timed
- public Response getApplicationSettings(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) {
- return Response.ok(config.getApplicationSettings()).build();
- }
-
@Path("/metrics")
@GET
- @UnitOfWork
+ @Transactional
@Operation(summary = "Retrieve server metrics")
- @Timed
- public Response getMetrics(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) {
+ public Response getMetrics() {
return Response.ok(metrics).build();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java
index 374f03a4..3a6fd362 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/CategoryREST.java
@@ -13,7 +13,6 @@ import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
@@ -29,7 +28,6 @@ import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedSubscriptionService;
-import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
@@ -40,13 +38,14 @@ import com.commafeed.frontend.model.request.CategoryModificationRequest;
import com.commafeed.frontend.model.request.CollapseRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
+import com.commafeed.security.AuthenticationContext;
+import com.commafeed.security.Roles;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.rometools.rome.feed.synd.SyndFeed;
import com.rometools.rome.feed.synd.SyndFeedImpl;
import com.rometools.rome.io.SyndFeedOutput;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -54,8 +53,9 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
@@ -70,11 +70,12 @@ import jakarta.ws.rs.core.Response.Status;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-@Path("/category")
+@Path("/rest/category")
+@RolesAllowed(Roles.USER)
@Slf4j
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Tag(name = "Feed categories")
public class CategoryREST {
@@ -82,6 +83,7 @@ public class CategoryREST {
public static final String ALL = "all";
public static final String STARRED = "starred";
+ private final AuthenticationContext authenticationContext;
private final FeedCategoryDAO feedCategoryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedSubscriptionDAO feedSubscriptionDAO;
@@ -92,13 +94,12 @@ public class CategoryREST {
@Path("/entries")
@GET
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Get category entries",
description = "Get a list of category entries",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) })
- @Timed
- public Response getCategoryEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
+ public Response getCategoryEntries(
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
@Parameter(
description = "all entries or only unread ones",
@@ -137,22 +138,24 @@ public class CategoryREST {
excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList();
}
+ User user = authenticationContext.getCurrentUser();
if (ALL.equals(id)) {
entries.setName(Optional.ofNullable(tag).orElse("All"));
+
List subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, excludedIds);
List list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
offset, limit + 1, order, true, tag, null, null);
for (FeedEntryStatus status : list) {
- entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
+ entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
}
} else if (STARRED.equals(id)) {
entries.setName("Starred");
List starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, true);
for (FeedEntryStatus status : starred) {
- entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
+ entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
}
} else {
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id));
@@ -164,7 +167,7 @@ public class CategoryREST {
offset, limit + 1, order, true, tag, null, null);
for (FeedEntryStatus status : list) {
- entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
+ entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
}
entries.setName(parent.getName());
} else {
@@ -186,11 +189,10 @@ public class CategoryREST {
@Path("/entriesAsFeed")
@GET
- @UnitOfWork
+ @Transactional
@Operation(summary = "Get category entries as feed", description = "Get a feed of category entries")
@Produces(MediaType.APPLICATION_XML)
- @Timed
- public Response getCategoryEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
+ public Response getCategoryEntriesAsFeed(
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
@Parameter(
description = "all entries or only unread ones",
@@ -205,7 +207,7 @@ public class CategoryREST {
description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
- Response response = getCategoryEntries(user, id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
+ Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
if (response.getStatus() != Status.OK.getStatusCode()) {
return response;
}
@@ -215,7 +217,7 @@ public class CategoryREST {
feed.setFeedType("rss_2.0");
feed.setTitle("CommaFeed - " + entries.getName());
feed.setDescription("CommaFeed - " + entries.getName());
- feed.setLink(config.getApplicationSettings().getPublicUrl());
+ feed.setLink(config.publicUrl());
feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList());
SyndFeedOutput output = new SyndFeedOutput();
@@ -231,11 +233,9 @@ public class CategoryREST {
@Path("/mark")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read")
- @Timed
- public Response markCategoryEntries(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) {
+ public Response markCategoryEntries(@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
@@ -244,6 +244,7 @@ public class CategoryREST {
String keywords = req.getKeywords();
List entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
+ User user = authenticationContext.getCurrentUser();
if (ALL.equals(req.getId())) {
List subs = feedSubscriptionDAO.findAll(user);
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
@@ -268,17 +269,17 @@ public class CategoryREST {
@Path("/add")
@POST
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Add a category",
description = "Add a new feed category",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) })
- @Timed
- public Response addCategory(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(required = true) AddCategoryRequest req) {
+ public Response addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getName());
+ User user = authenticationContext.getCurrentUser();
+
FeedCategory cat = new FeedCategory();
cat.setName(req.getName());
cat.setUser(user);
@@ -296,14 +297,14 @@ public class CategoryREST {
@POST
@Path("/delete")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Delete a category", description = "Delete an existing feed category")
- @Timed
- public Response deleteCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) {
+ public Response deleteCategory(@Parameter(required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
+ User user = authenticationContext.getCurrentUser();
FeedCategory cat = feedCategoryDAO.findById(user, req.getId());
if (cat != null) {
List subs = feedSubscriptionDAO.findByCategory(user, cat);
@@ -329,14 +330,13 @@ public class CategoryREST {
@POST
@Path("/modify")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Rename a category", description = "Rename an existing feed category")
- @Timed
- public Response modifyCategory(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(required = true) CategoryModificationRequest req) {
+ public Response modifyCategory(@Valid @Parameter(required = true) CategoryModificationRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
+ User user = authenticationContext.getCurrentUser();
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
if (StringUtils.isNotBlank(req.getName())) {
@@ -380,13 +380,13 @@ public class CategoryREST {
@POST
@Path("/collapse")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category")
- @Timed
- public Response collapseCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) CollapseRequest req) {
+ public Response collapseCategory(@Parameter(required = true) CollapseRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
+ User user = authenticationContext.getCurrentUser();
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
if (category == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -399,25 +399,25 @@ public class CategoryREST {
@GET
@Path("/unreadCount")
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Get unread count for feed subscriptions",
responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UnreadCount.class)))) })
- @Timed
- public Response getUnreadCount(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user) {
+ public Response getUnreadCount() {
+ User user = authenticationContext.getCurrentUser();
Map unreadCount = feedSubscriptionService.getUnreadCount(user);
return Response.ok(Lists.newArrayList(unreadCount.values())).build();
}
@GET
@Path("/get")
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Get root category",
description = "Get all categories and subscriptions of the user",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Category.class))) })
- @Timed
- public Response getRootCategory(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response getRootCategory() {
+ User user = authenticationContext.getCurrentUser();
Category root = cache.getUserRootCategory(user);
if (root == null) {
log.debug("tree cache miss for {}", user.getId());
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java
index 2726f4d2..1ebe6954 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/EntryREST.java
@@ -2,24 +2,24 @@ package com.commafeed.frontend.resource;
import java.util.List;
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedEntryTagService;
-import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.MultipleMarkRequest;
import com.commafeed.frontend.model.request.StarRequest;
import com.commafeed.frontend.model.request.TagRequest;
+import com.commafeed.security.AuthenticationContext;
+import com.commafeed.security.Roles;
import com.google.common.base.Preconditions;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
@@ -30,42 +30,42 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
-@Path("/entry")
+@Path("/rest/entry")
+@RolesAllowed(Roles.USER)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Tag(name = "Feed entries")
public class EntryREST {
+ private final AuthenticationContext authenticationContext;
private final FeedEntryTagDAO feedEntryTagDAO;
private final FeedEntryService feedEntryService;
private final FeedEntryTagService feedEntryTagService;
@Path("/mark")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread")
- @Timed
- public Response markEntry(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
+ public Response markEntry(@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
+ User user = authenticationContext.getCurrentUser();
feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead());
return Response.ok().build();
}
@Path("/markMultiple")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread")
- @Timed
- public Response markEntries(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
+ public Response markEntries(@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getRequests());
+ User user = authenticationContext.getCurrentUser();
for (MarkRequest r : req.getRequests()) {
Preconditions.checkNotNull(r.getId());
feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead());
@@ -76,15 +76,14 @@ public class EntryREST {
@Path("/star")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread")
- @Timed
- public Response starEntry(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
+ public Response starEntry(@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
Preconditions.checkNotNull(req.getFeedId());
+ User user = authenticationContext.getCurrentUser();
feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
return Response.ok().build();
@@ -92,24 +91,23 @@ public class EntryREST {
@Path("/tags")
@GET
- @UnitOfWork
+ @Transactional
@Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user")
- @Timed
- public Response getTags(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response getTags() {
+ User user = authenticationContext.getCurrentUser();
List tags = feedEntryTagDAO.findByUser(user);
return Response.ok(tags).build();
}
@Path("/tag")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Set feed entry tags")
- @Timed
- public Response tagEntry(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
+ public Response tagEntry(@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getEntryId());
+ User user = authenticationContext.getCurrentUser();
feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags());
return Response.ok().build();
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java
index 0d25fa42..52879805 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/FeedREST.java
@@ -1,9 +1,7 @@
package com.commafeed.frontend.resource;
-import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
-import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Calendar;
import java.util.Collections;
@@ -13,9 +11,8 @@ import java.util.Objects;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
-import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.jboss.resteasy.reactive.RestForm;
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.cache.CacheService;
@@ -44,7 +41,6 @@ import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterEx
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.FeedService;
import com.commafeed.backend.service.FeedSubscriptionService;
-import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.FeedInfo;
@@ -55,6 +51,8 @@ import com.commafeed.frontend.model.request.FeedModificationRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
+import com.commafeed.security.AuthenticationContext;
+import com.commafeed.security.Roles;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.rometools.opml.feed.opml.Opml;
@@ -63,15 +61,15 @@ import com.rometools.rome.feed.synd.SyndFeedImpl;
import com.rometools.rome.io.SyndFeedOutput;
import com.rometools.rome.io.WireFeedOutput;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
@@ -90,17 +88,19 @@ import jakarta.ws.rs.core.Response.Status;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-@Path("/feed")
+@Path("/rest/feed")
+@RolesAllowed(Roles.USER)
@Slf4j
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Tag(name = "Feeds")
public class FeedREST {
private static final FeedEntry TEST_ENTRY = initTestEntry();
+ private final AuthenticationContext authenticationContext;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedCategoryDAO feedCategoryDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
@@ -129,14 +129,12 @@ public class FeedREST {
@Path("/entries")
@GET
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Get feed entries",
description = "Get a list of feed entries",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) })
- @Timed
- public Response getFeedEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
- @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
+ public Response getFeedEntries(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
@Parameter(
description = "all entries or only unread ones",
required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType,
@@ -164,6 +162,7 @@ public class FeedREST {
Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan);
+ User user = authenticationContext.getCurrentUser();
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id));
if (subscription != null) {
entries.setName(subscription.getTitle());
@@ -175,7 +174,7 @@ public class FeedREST {
entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null);
for (FeedEntryStatus status : list) {
- entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
+ entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
}
boolean hasMore = entries.getEntries().size() > limit;
@@ -195,12 +194,10 @@ public class FeedREST {
@Path("/entriesAsFeed")
@GET
- @UnitOfWork
+ @Transactional
@Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries")
@Produces(MediaType.APPLICATION_XML)
- @Timed
- public Response getFeedEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
- @Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
+ public Response getFeedEntriesAsFeed(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
@Parameter(
description = "all entries or only unread ones",
required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
@@ -210,7 +207,7 @@ public class FeedREST {
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(
description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords) {
- Response response = getFeedEntries(user, id, readType, newerThan, offset, limit, order, keywords);
+ Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords);
if (response.getStatus() != Status.OK.getStatusCode()) {
return response;
}
@@ -220,7 +217,7 @@ public class FeedREST {
feed.setFeedType("rss_2.0");
feed.setTitle("CommaFeed - " + entries.getName());
feed.setDescription("CommaFeed - " + entries.getName());
- feed.setLink(config.getApplicationSettings().getPublicUrl());
+ feed.setLink(config.publicUrl());
feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList());
SyndFeedOutput output = new SyndFeedOutput();
@@ -253,14 +250,12 @@ public class FeedREST {
@POST
@Path("/fetch")
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Fetch a feed",
description = "Fetch a feed by its url",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = FeedInfo.class))) })
- @Timed
- public Response fetchFeed(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
+ public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getUrl());
@@ -279,25 +274,23 @@ public class FeedREST {
@Path("/refreshAll")
@GET
- @UnitOfWork
+ @Transactional
@Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue")
- @Timed
- public Response queueAllForRefresh(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response queueAllForRefresh() {
+ User user = authenticationContext.getCurrentUser();
feedSubscriptionService.refreshAll(user);
return Response.ok().build();
}
@Path("/refresh")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Queue a feed for refresh", description = "Manually add a feed to the refresh queue")
- @Timed
- public Response queueForRefresh(@Parameter(hidden = true) @SecurityCheck User user,
- @Parameter(description = "Feed id", required = true) IDRequest req) {
-
+ public Response queueForRefresh(@Parameter(description = "Feed id", required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
+ User user = authenticationContext.getCurrentUser();
FeedSubscription sub = feedSubscriptionDAO.findById(user, req.getId());
if (sub != null) {
Feed feed = sub.getFeed();
@@ -309,11 +302,9 @@ public class FeedREST {
@Path("/mark")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)")
- @Timed
- public Response markFeedEntries(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
+ public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
@@ -322,6 +313,7 @@ public class FeedREST {
String keywords = req.getKeywords();
List entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
+ User user = authenticationContext.getCurrentUser();
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
if (subscription != null) {
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, insertedBefore,
@@ -332,15 +324,14 @@ public class FeedREST {
@GET
@Path("/get/{id}")
- @UnitOfWork
+ @Transactional
@Operation(
summary = "get feed",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Subscription.class))) })
- @Timed
- public Response getFeed(@Parameter(hidden = true) @SecurityCheck User user,
- @Parameter(description = "user id", required = true) @PathParam("id") Long id) {
-
+ public Response getFeed(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
Preconditions.checkNotNull(id);
+
+ User user = authenticationContext.getCurrentUser();
FeedSubscription sub = feedSubscriptionDAO.findById(user, id);
if (sub == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -351,13 +342,12 @@ public class FeedREST {
@GET
@Path("/favicon/{id}")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon")
- @Timed
- public Response getFeedFavicon(@Parameter(hidden = true) @SecurityCheck User user,
- @Parameter(description = "subscription id", required = true) @PathParam("id") Long id) {
-
+ public Response getFeedFavicon(@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) {
Preconditions.checkNotNull(id);
+
+ User user = authenticationContext.getCurrentUser();
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
if (subscription == null) {
return Response.status(Status.NOT_FOUND).build();
@@ -382,14 +372,12 @@ public class FeedREST {
@POST
@Path("/subscribe")
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Subscribe to a feed",
description = "Subscribe to a feed",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) })
- @Timed
- public Response subscribe(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) {
+ public Response subscribe(@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getTitle());
Preconditions.checkNotNull(req.getUrl());
@@ -401,6 +389,7 @@ public class FeedREST {
}
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
+ User user = authenticationContext.getCurrentUser();
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category);
return Response.ok(subscriptionId).build();
} catch (Exception e) {
@@ -413,19 +402,18 @@ public class FeedREST {
@GET
@Path("/subscribe")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
- @Timed
- public Response subscribeFromUrl(@Parameter(hidden = true) @SecurityCheck User user,
- @Parameter(description = "feed url", required = true) @QueryParam("url") String url) {
+ public Response subscribeFromUrl(@Parameter(description = "feed url", required = true) @QueryParam("url") String url) {
try {
Preconditions.checkNotNull(url);
FeedInfo info = fetchFeedInternal(prependHttp(url));
+ User user = authenticationContext.getCurrentUser();
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle());
} catch (Exception e) {
log.info("Could not subscribe to url {} : {}", url, e.getMessage());
}
- return Response.temporaryRedirect(URI.create(config.getApplicationSettings().getPublicUrl())).build();
+ return Response.temporaryRedirect(URI.create(config.publicUrl())).build();
}
private String prependHttp(String url) {
@@ -437,13 +425,13 @@ public class FeedREST {
@POST
@Path("/unsubscribe")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed")
- @Timed
- public Response unsubscribe(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) {
+ public Response unsubscribe(@Parameter(required = true) IDRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
+ User user = authenticationContext.getCurrentUser();
boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId());
if (deleted) {
return Response.ok().build();
@@ -454,11 +442,9 @@ public class FeedREST {
@POST
@Path("/modify")
- @UnitOfWork
+ @Transactional
@Operation(summary = "Modify a subscription", description = "Modify a feed subscription")
- @Timed
- public Response modifyFeed(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) {
+ public Response modifyFeed(@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getId());
@@ -468,6 +454,7 @@ public class FeedREST {
return Response.status(Status.BAD_REQUEST).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build();
}
+ User user = authenticationContext.getCurrentUser();
FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId());
subscription.setFilter(StringUtils.lowerCase(req.getFilter()));
@@ -509,39 +496,36 @@ public class FeedREST {
@POST
@Path("/import")
- @UnitOfWork
+ @Transactional
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name")
- @Timed
- public Response importOpml(@Parameter(hidden = true) @SecurityCheck User user,
- @Parameter(description = "ompl file", required = true) @FormDataParam("file") InputStream input) {
+ public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) {
+ User user = authenticationContext.getCurrentUser();
if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build();
}
try {
- String opml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
opmlImporter.importOpml(user, opml);
} catch (Exception e) {
- log.error(e.getMessage(), e);
- throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build());
+ return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
}
return Response.ok().build();
}
@GET
@Path("/export")
- @UnitOfWork
+ @Transactional
@Produces(MediaType.APPLICATION_XML)
@Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions")
- @Timed
- public Response exportOpml(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response exportOpml() {
+ User user = authenticationContext.getCurrentUser();
Opml opml = opmlExporter.export(user);
WireFeedOutput output = new WireFeedOutput();
String opmlString;
try {
opmlString = output.outputString(opml);
} catch (Exception e) {
- return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e).build();
+ return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
}
return Response.ok(opmlString).build();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java
index eb213f50..881a7a04 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/ServerREST.java
@@ -1,25 +1,23 @@
package com.commafeed.frontend.resource;
-import org.apache.commons.lang3.StringUtils;
-
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.CommaFeedVersion;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.feed.FeedUtils;
-import com.commafeed.backend.model.User;
-import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.ServerInfo;
+import com.commafeed.security.Roles;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.PermitAll;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -30,49 +28,50 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import lombok.RequiredArgsConstructor;
-@Path("/server")
+@Path("/rest/server")
+
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Tag(name = "Server")
public class ServerREST {
private final HttpGetter httpGetter;
private final CommaFeedConfiguration config;
+ private final CommaFeedVersion version;
@Path("/get")
@GET
- @UnitOfWork
+ @PermitAll
+ @Transactional
@Operation(
summary = "Get server infos",
description = "Get server infos",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ServerInfo.class))) })
- @Timed
public Response getServerInfos() {
ServerInfo infos = new ServerInfo();
- infos.setAnnouncement(config.getApplicationSettings().getAnnouncement());
- infos.setVersion(config.getVersion());
- infos.setGitCommit(config.getGitCommit());
- infos.setAllowRegistrations(config.getApplicationSettings().getAllowRegistrations());
- infos.setGoogleAnalyticsCode(config.getApplicationSettings().getGoogleAnalyticsTrackingCode());
- infos.setSmtpEnabled(StringUtils.isNotBlank(config.getApplicationSettings().getSmtpHost()));
- infos.setDemoAccountEnabled(config.getApplicationSettings().getCreateDemoAccount());
- infos.setWebsocketEnabled(config.getApplicationSettings().getWebsocketEnabled());
- infos.setWebsocketPingInterval(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds());
- infos.setTreeReloadInterval(config.getApplicationSettings().getTreeReloadInterval().toMilliseconds());
+ infos.setAnnouncement(config.announcement().orElse(null));
+ infos.setVersion(version.getVersion());
+ infos.setGitCommit(version.getGitCommit());
+ infos.setAllowRegistrations(config.users().allowRegistrations());
+ infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null));
+ infos.setSmtpEnabled(config.smtp().isPresent());
+ infos.setDemoAccountEnabled(config.users().createDemoAccount());
+ infos.setWebsocketEnabled(config.websocket().enabled());
+ infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
+ infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
return Response.ok(infos).build();
}
@Path("/proxy")
@GET
- @UnitOfWork
+ @RolesAllowed(Roles.USER)
+ @Transactional
@Operation(summary = "proxy image")
@Produces("image/png")
- @Timed
- public Response getProxiedImage(@Parameter(hidden = true) @SecurityCheck User user,
- @Parameter(description = "image url", required = true) @QueryParam("u") String url) {
- if (!config.getApplicationSettings().getImageProxyEnabled()) {
+ public Response getProxiedImage(@Parameter(description = "image url", required = true) @QueryParam("u") String url) {
+ if (!config.imageProxyEnabled()) {
return Response.status(Status.FORBIDDEN).build();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java
index 3ab79050..b391631f 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/UserREST.java
@@ -10,7 +10,6 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.net.URIBuilder;
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.Digests;
@@ -29,25 +28,25 @@ import com.commafeed.backend.model.UserSettings.ScrollMode;
import com.commafeed.backend.service.MailService;
import com.commafeed.backend.service.PasswordEncryptionService;
import com.commafeed.backend.service.UserService;
-import com.commafeed.frontend.auth.SecurityCheck;
import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel;
-import com.commafeed.frontend.model.request.LoginRequest;
import com.commafeed.frontend.model.request.PasswordResetRequest;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.RegistrationRequest;
-import com.commafeed.frontend.session.SessionHelper;
+import com.commafeed.security.AuthenticationContext;
+import com.commafeed.security.Roles;
import com.google.common.base.Preconditions;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.PermitAll;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
@@ -56,22 +55,23 @@ import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-@Path("/user")
+@Path("/rest/user")
+@RolesAllowed(Roles.USER)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Slf4j
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Tag(name = "Users")
public class UserREST {
+ private final AuthenticationContext authenticationContext;
private final UserDAO userDAO;
private final UserRoleDAO userRoleDAO;
private final UserSettingsDAO userSettingsDAO;
@@ -82,14 +82,15 @@ public class UserREST {
@Path("/settings")
@GET
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Retrieve user settings",
description = "Retrieve user settings",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Settings.class))) })
- @Timed
- public Response getUserSettings(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response getUserSettings() {
Settings s = new Settings();
+
+ User user = authenticationContext.getCurrentUser();
UserSettings settings = userSettingsDAO.findByUser(user);
if (settings != null) {
s.setReadingMode(settings.getReadingMode().name());
@@ -145,12 +146,12 @@ public class UserREST {
@Path("/settings")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Save user settings", description = "Save user settings")
- @Timed
- public Response saveUserSettings(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) Settings settings) {
+ public Response saveUserSettings(@Parameter(required = true) Settings settings) {
Preconditions.checkNotNull(settings);
+ User user = authenticationContext.getCurrentUser();
UserSettings s = userSettingsDAO.findByUser(user);
if (s == null) {
s = new UserSettings();
@@ -187,12 +188,13 @@ public class UserREST {
@Path("/profile")
@GET
- @UnitOfWork
+ @Transactional
@Operation(
summary = "Retrieve user's profile",
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) })
- @Timed
- public Response getUserProfile(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response getUserProfile() {
+ User user = authenticationContext.getCurrentUser();
+
UserModel userModel = new UserModel();
userModel.setId(user.getId());
userModel.setName(user.getName());
@@ -209,11 +211,10 @@ public class UserREST {
@Path("/profile")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Save user's profile")
- @Timed
- public Response saveUserProfile(@Parameter(hidden = true) @SecurityCheck User user,
- @Valid @Parameter(required = true) ProfileModificationRequest request) {
+ public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) {
+ User user = authenticationContext.getCurrentUser();
if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.FORBIDDEN).build();
}
@@ -242,49 +243,29 @@ public class UserREST {
user.setApiKey(userService.generateApiKey(user));
}
- userDAO.update(user);
+ userDAO.saveOrUpdate(user);
return Response.ok().build();
}
@Path("/register")
+ @PermitAll
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Register a new account")
- @Timed
- public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req,
- @Context @Parameter(hidden = true) SessionHelper sessionHelper) {
+ public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) {
try {
- User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(),
- Collections.singletonList(Role.USER));
- userService.login(req.getName(), req.getPassword());
- sessionHelper.setLoggedInUser(registeredUser);
+ userService.register(req.getName(), req.getPassword(), req.getEmail(), Collections.singletonList(Role.USER));
return Response.ok().build();
} catch (final IllegalArgumentException e) {
throw new BadRequestException(e.getMessage());
}
}
- @Path("/login")
- @POST
- @UnitOfWork
- @Operation(summary = "Login and create a session")
- @Timed
- public Response login(@Valid @Parameter(required = true) LoginRequest req,
- @Parameter(hidden = true) @Context SessionHelper sessionHelper) {
- Optional user = userService.login(req.getName(), req.getPassword());
- if (user.isPresent()) {
- sessionHelper.setLoggedInUser(user.get());
- return Response.ok().build();
- } else {
- return Response.status(Response.Status.UNAUTHORIZED).entity("wrong username or password").type(MediaType.TEXT_PLAIN).build();
- }
- }
-
@Path("/passwordReset")
+ @PermitAll
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "send a password reset email")
- @Timed
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
User user = userDAO.findByEmail(req.getEmail());
if (user == null) {
@@ -304,7 +285,7 @@ public class UserREST {
}
private String buildEmailContent(User user) throws Exception {
- String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl());
+ String publicUrl = FeedUtils.removeTrailingSlash(config.publicUrl());
publicUrl += "/rest/user/passwordResetCallback";
return String.format(
"You asked for password recovery for account '%s', follow this link to change your password. Ignore this if you didn't request a password recovery.",
@@ -320,10 +301,10 @@ public class UserREST {
}
@Path("/passwordResetCallback")
+ @PermitAll
@GET
- @UnitOfWork
+ @Transactional
@Produces(MediaType.TEXT_HTML)
- @Timed
public Response passwordRecoveryCallback(@Parameter(required = true) @QueryParam("email") String email,
@Parameter(required = true) @QueryParam("token") String token) {
Preconditions.checkNotNull(email);
@@ -352,16 +333,16 @@ public class UserREST {
String message = "Your new password is: " + passwd;
message += "
";
- message += String.format("Back to Homepage", config.getApplicationSettings().getPublicUrl());
+ message += String.format("Back to Homepage", config.publicUrl());
return Response.ok(message).build();
}
@Path("/profile/deleteAccount")
@POST
- @UnitOfWork
+ @Transactional
@Operation(summary = "Delete the user account")
- @Timed
- public Response deleteUser(@Parameter(hidden = true) @SecurityCheck User user) {
+ public Response deleteUser() {
+ User user = authenticationContext.getCurrentUser();
if (CommaFeedApplication.USERNAME_ADMIN.equals(user.getName()) || CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.FORBIDDEN).build();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java
index 551f1f8e..69b7ba1a 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/resource/fever/FeverREST.java
@@ -13,9 +13,9 @@ import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.jboss.resteasy.reactive.server.multipart.FormValue;
+import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput;
-import com.codahale.metrics.annotation.Timed;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
@@ -37,10 +37,10 @@ import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem;
-import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.v3.oas.annotations.Hidden;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.PermitAll;
import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -64,9 +64,10 @@ import lombok.RequiredArgsConstructor;
*
* See https://feedafever.com/api
*/
-@Path("/fever")
+@Path("/rest/fever")
+@PermitAll
@Produces(MediaType.APPLICATION_JSON)
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@RequiredArgsConstructor
@Singleton
@Hidden
public class FeverREST {
@@ -88,8 +89,7 @@ public class FeverREST {
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path(PATH)
@POST
- @UnitOfWork
- @Timed
+ @Transactional
public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap form) {
Map params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
@@ -101,8 +101,7 @@ public class FeverREST {
// e.g. FeedMe
@Path(PATH)
@POST
- @UnitOfWork
- @Timed
+ @Transactional
public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) {
Map params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
@@ -113,8 +112,7 @@ public class FeverREST {
// e.g. Unread
@Path(PATH)
@GET
- @UnitOfWork
- @Timed
+ @Transactional
public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) {
Map params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
@@ -126,12 +124,11 @@ public class FeverREST {
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path(PATH)
@POST
- @UnitOfWork
- @Timed
- public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, FormDataMultiPart form) {
+ @Transactional
+ public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) {
Map params = new HashMap<>();
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
- form.getFields().forEach((k, v) -> params.put(k, v.get(0).getValue()));
+ form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null)));
return handle(userId, params);
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java
deleted file mode 100644
index 29df13f8..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/AbstractCustomCodeServlet.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.commafeed.frontend.servlet;
-
-import java.io.IOException;
-import java.util.Optional;
-
-import com.commafeed.backend.dao.UnitOfWork;
-import com.commafeed.backend.dao.UserDAO;
-import com.commafeed.backend.dao.UserSettingsDAO;
-import com.commafeed.backend.model.User;
-import com.commafeed.backend.model.UserSettings;
-import com.commafeed.frontend.session.SessionHelper;
-
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-abstract class AbstractCustomCodeServlet extends HttpServlet {
-
- private static final long serialVersionUID = 1L;
-
- private final transient UnitOfWork unitOfWork;
- private final transient UserDAO userDAO;
- private final transient UserSettingsDAO userSettingsDAO;
-
- @Override
- protected final void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException {
- resp.setContentType(getMimeType());
-
- SessionHelper sessionHelper = new SessionHelper(req);
- Optional userId = sessionHelper.getLoggedInUserId();
- final Optional user = unitOfWork.call(() -> userId.map(userDAO::findById));
- if (user.isEmpty()) {
- return;
- }
-
- UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user.get()));
- if (settings == null) {
- return;
- }
-
- String customCode = getCustomCode(settings);
- if (customCode == null) {
- return;
- }
-
- resp.getWriter().write(customCode);
- }
-
- protected abstract String getMimeType();
-
- protected abstract String getCustomCode(UserSettings settings);
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java
index 7bcc1f91..0f95ee28 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomCssServlet.java
@@ -1,28 +1,39 @@
package com.commafeed.frontend.servlet;
import com.commafeed.backend.dao.UnitOfWork;
-import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
+import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
+import com.commafeed.security.AuthenticationContext;
-import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import lombok.RequiredArgsConstructor;
-public class CustomCssServlet extends AbstractCustomCodeServlet {
+@Path("/custom_css.css")
+@Produces("text/css")
+@RequiredArgsConstructor
+@Singleton
+public class CustomCssServlet {
- private static final long serialVersionUID = 1L;
+ private final AuthenticationContext authenticationContext;
+ private final UserSettingsDAO userSettingsDAO;
+ private final UnitOfWork unitOfWork;
- @Inject
- public CustomCssServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) {
- super(unitOfWork, userDAO, userSettingsDAO);
- }
+ @GET
+ public String get() {
+ User user = authenticationContext.getCurrentUser();
+ if (user == null) {
+ return "";
+ }
- @Override
- protected String getMimeType() {
- return "text/css";
- }
+ UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user));
+ if (settings == null) {
+ return "";
+ }
- @Override
- protected String getCustomCode(UserSettings settings) {
return settings.getCustomCss();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java
index 0c29b7a9..2d651db5 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/CustomJsServlet.java
@@ -1,30 +1,39 @@
package com.commafeed.frontend.servlet;
import com.commafeed.backend.dao.UnitOfWork;
-import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
+import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
+import com.commafeed.security.AuthenticationContext;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import lombok.RequiredArgsConstructor;
+@Path("/custom_js.js")
+@Produces("application/javascript")
+@RequiredArgsConstructor
@Singleton
-public class CustomJsServlet extends AbstractCustomCodeServlet {
+public class CustomJsServlet {
- private static final long serialVersionUID = 1L;
+ private final AuthenticationContext authenticationContext;
+ private final UserSettingsDAO userSettingsDAO;
+ private final UnitOfWork unitOfWork;
- @Inject
- public CustomJsServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) {
- super(unitOfWork, userDAO, userSettingsDAO);
- }
+ @GET
+ public String get() {
+ User user = authenticationContext.getCurrentUser();
+ if (user == null) {
+ return "";
+ }
- @Override
- protected String getMimeType() {
- return "application/javascript";
- }
+ UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user));
+ if (settings == null) {
+ return "";
+ }
- @Override
- protected String getCustomCode(UserSettings settings) {
return settings.getCustomJs();
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java
index 6d405709..09a7b426 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/LogoutServlet.java
@@ -1,26 +1,35 @@
package com.commafeed.frontend.servlet;
-import java.io.IOException;
+import java.net.URI;
+import java.time.Instant;
+import java.util.Date;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
import com.commafeed.CommaFeedConfiguration;
-import jakarta.inject.Inject;
+import jakarta.annotation.security.PermitAll;
import jakarta.inject.Singleton;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.NewCookie;
+import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
-@SuppressWarnings("serial")
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@Path("/logout")
+@PermitAll
+@RequiredArgsConstructor
@Singleton
-public class LogoutServlet extends HttpServlet {
+public class LogoutServlet {
+
+ @ConfigProperty(name = "quarkus.http.auth.form.cookie-name")
+ String cookieName;
private final CommaFeedConfiguration config;
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- req.getSession().invalidate();
- resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
+ @GET
+ public Response get() {
+ NewCookie removeCookie = new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build();
+ return Response.temporaryRedirect(URI.create(config.publicUrl())).cookie(removeCookie).build();
}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java
index dabda582..80ee3974 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/NextUnreadServlet.java
@@ -1,8 +1,7 @@
package com.commafeed.frontend.servlet;
-import java.io.IOException;
+import java.net.URI;
import java.util.List;
-import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
@@ -11,86 +10,68 @@ import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.dao.UnitOfWork;
-import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService;
-import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.resource.CategoryREST;
-import com.commafeed.frontend.session.SessionHelper;
+import com.commafeed.security.AuthenticationContext;
import com.google.common.collect.Iterables;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
-@SuppressWarnings("serial")
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
+@Path("/next")
+@RequiredArgsConstructor
@Singleton
-public class NextUnreadServlet extends HttpServlet {
-
- private static final String PARAM_CATEGORYID = "category";
- private static final String PARAM_READINGORDER = "order";
+public class NextUnreadServlet {
private final UnitOfWork unitOfWork;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedCategoryDAO feedCategoryDAO;
- private final UserDAO userDAO;
- private final UserService userService;
private final FeedEntryService feedEntryService;
private final CommaFeedConfiguration config;
+ private final AuthenticationContext authenticationContext;
- @Override
- protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException {
- final String categoryId = req.getParameter(PARAM_CATEGORYID);
- String orderParam = req.getParameter(PARAM_READINGORDER);
-
- SessionHelper sessionHelper = new SessionHelper(req);
- Optional userId = sessionHelper.getLoggedInUserId();
- Optional user = unitOfWork.call(() -> userId.map(userDAO::findById));
- user.ifPresent(value -> unitOfWork.run(() -> userService.performPostLoginActivities(value)));
- if (user.isEmpty()) {
- resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
- return;
+ @GET
+ public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
+ User user = authenticationContext.getCurrentUser();
+ if (user == null) {
+ return Response.temporaryRedirect(URI.create(config.publicUrl())).build();
}
- final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc;
-
FeedEntryStatus status = unitOfWork.call(() -> {
FeedEntryStatus s = null;
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
- List subs = feedSubscriptionDAO.findAll(user.get());
- List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order,
- true, null, null, null);
+ List subs = feedSubscriptionDAO.findAll(user);
+ List statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true,
+ null, null, null);
s = Iterables.getFirst(statuses, null);
} else {
- FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId));
+ FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
if (category != null) {
- List children = feedCategoryDAO.findAllChildrenCategories(user.get(), category);
- List subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children);
- List statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0,
- 1, order, true, null, null, null);
+ List children = feedCategoryDAO.findAllChildrenCategories(user, category);
+ List subscriptions = feedSubscriptionDAO.findByCategories(user, children);
+ List statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1,
+ order, true, null, null, null);
s = Iterables.getFirst(statuses, null);
}
}
if (s != null) {
- feedEntryService.markEntry(user.get(), s.getEntry().getId(), true);
+ feedEntryService.markEntry(user, s.getEntry().getId(), true);
}
return s;
});
- if (status == null) {
- resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
- } else {
- String url = status.getEntry().getUrl();
- resp.sendRedirect(resp.encodeRedirectURL(url));
- }
+ String url = status == null ? config.publicUrl() : status.getEntry().getUrl();
+ return Response.temporaryRedirect(URI.create(url)).build();
}
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java
index b9873ffe..3e29cc6d 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/servlet/RobotsTxtDisallowAllServlet.java
@@ -1,22 +1,33 @@
package com.commafeed.frontend.servlet;
-import java.io.IOException;
+import org.apache.hc.core5.http.HttpStatus;
+import com.commafeed.CommaFeedConfiguration;
+
+import jakarta.annotation.security.PermitAll;
import jakarta.inject.Singleton;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import lombok.RequiredArgsConstructor;
+@Path("/robots.txt")
+@PermitAll
+@Produces(MediaType.TEXT_PLAIN)
+@RequiredArgsConstructor
@Singleton
-public class RobotsTxtDisallowAllServlet extends HttpServlet {
+public class RobotsTxtDisallowAllServlet {
- private static final long serialVersionUID = 1L;
+ private final CommaFeedConfiguration config;
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- resp.setContentType("text/plain");
- resp.getWriter().write("User-agent: *");
- resp.getWriter().write("\n");
- resp.getWriter().write("Disallow: /");
+ @GET
+ public Response get() {
+ if (config.hideFromWebCrawlers()) {
+ return Response.ok("User-agent: *\nDisallow: /").build();
+ } else {
+ return Response.status(HttpStatus.SC_NOT_FOUND).build();
+ }
}
-}
+}
\ No newline at end of file
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java
deleted file mode 100644
index a3388db4..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHandlerFactory.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.commafeed.frontend.session;
-
-import org.eclipse.jetty.server.session.DatabaseAdaptor;
-import org.eclipse.jetty.server.session.DefaultSessionCache;
-import org.eclipse.jetty.server.session.JDBCSessionDataStore;
-import org.eclipse.jetty.server.session.SessionCache;
-import org.eclipse.jetty.server.session.SessionHandler;
-
-import com.codahale.metrics.MetricRegistry;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.ImmutableSet;
-
-import io.dropwizard.db.DataSourceFactory;
-import io.dropwizard.util.Duration;
-import jakarta.servlet.SessionTrackingMode;
-
-public class SessionHandlerFactory {
-
- @JsonProperty
- private Duration cookieMaxAge = Duration.days(30);
-
- @JsonProperty
- private Duration cookieRefreshAge = Duration.days(1);
-
- @JsonProperty
- private Duration maxInactiveInterval = Duration.days(30);
-
- @JsonProperty
- private Duration savePeriod = Duration.minutes(5);
-
- public SessionHandler build(DataSourceFactory dataSourceFactory) {
- SessionHandler sessionHandler = new SessionHandler();
- sessionHandler.setHttpOnly(true);
- sessionHandler.setSessionTrackingModes(ImmutableSet.of(SessionTrackingMode.COOKIE));
- sessionHandler.setMaxInactiveInterval((int) maxInactiveInterval.toSeconds());
- sessionHandler.setRefreshCookieAge((int) cookieRefreshAge.toSeconds());
- sessionHandler.getSessionCookieConfig().setMaxAge((int) cookieMaxAge.toSeconds());
-
- SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
- sessionHandler.setSessionCache(sessionCache);
-
- JDBCSessionDataStore dataStore = new JDBCSessionDataStore();
- dataStore.setSavePeriodSec((int) savePeriod.toSeconds());
- sessionCache.setSessionDataStore(dataStore);
-
- DatabaseAdaptor adaptor = new DatabaseAdaptor();
- adaptor.setDatasource(dataSourceFactory.build(new MetricRegistry(), "sessions"));
- dataStore.setDatabaseAdaptor(adaptor);
-
- return sessionHandler;
- }
-
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java
deleted file mode 100644
index 97341ea3..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelper.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.commafeed.frontend.session;
-
-import java.util.Optional;
-
-import com.commafeed.backend.model.User;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpSession;
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-public class SessionHelper {
-
- public static final String SESSION_KEY_USER_ID = "user-id";
-
- private final HttpServletRequest request;
-
- public Optional getLoggedInUserId() {
- HttpSession session = request.getSession(false);
- return getLoggedInUserId(session);
- }
-
- public static Optional getLoggedInUserId(HttpSession session) {
- if (session == null) {
- return Optional.empty();
- }
- Long userId = (Long) session.getAttribute(SESSION_KEY_USER_ID);
- return Optional.ofNullable(userId);
- }
-
- public void setLoggedInUser(User user) {
- request.getSession(true).setAttribute(SESSION_KEY_USER_ID, user.getId());
- }
-
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java b/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java
deleted file mode 100644
index 4ff25de6..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.commafeed.frontend.session;
-
-import java.util.function.Function;
-
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
-import org.glassfish.jersey.server.ContainerRequest;
-import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider;
-import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
-import org.glassfish.jersey.server.model.Parameter;
-import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
-
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.ws.rs.core.Context;
-
-@Singleton
-public class SessionHelperFactoryProvider extends AbstractValueParamProvider {
-
- private final HttpServletRequest request;
-
- @Inject
- public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, HttpServletRequest request) {
- super(() -> extractorProvider, Parameter.Source.CONTEXT);
- this.request = request;
- }
-
- @Override
- protected Function createValueProvider(Parameter parameter) {
- final Class> classType = parameter.getRawType();
-
- Context context = parameter.getAnnotation(Context.class);
- if (context == null) {
- return null;
- }
-
- if (!classType.isAssignableFrom(SessionHelper.class)) {
- return null;
- }
-
- return r -> new SessionHelper(request);
- }
-
- public static class Binder extends AbstractBinder {
-
- @Override
- protected void configure() {
- bind(SessionHelperFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class);
- }
- }
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java
deleted file mode 100644
index 70d6a903..00000000
--- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketConfigurator.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.commafeed.frontend.ws;
-
-import java.util.Optional;
-
-import com.commafeed.frontend.session.SessionHelper;
-
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import jakarta.servlet.http.HttpSession;
-import jakarta.websocket.HandshakeResponse;
-import jakarta.websocket.server.HandshakeRequest;
-import jakarta.websocket.server.ServerEndpointConfig;
-import jakarta.websocket.server.ServerEndpointConfig.Configurator;
-import lombok.RequiredArgsConstructor;
-
-@Singleton
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
-public class WebSocketConfigurator extends Configurator {
-
- public static final String SESSIONKEY_USERID = "userId";
-
- private final WebSocketSessions webSocketSessions;
-
- @Override
- public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
- HttpSession httpSession = (HttpSession) request.getHttpSession();
- if (httpSession != null) {
- Optional userId = SessionHelper.getLoggedInUserId(httpSession);
- userId.ifPresent(value -> config.getUserProperties().put(SESSIONKEY_USERID, value));
- }
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public T getEndpointInstance(Class endpointClass) {
- return (T) new WebSocketEndpoint(webSocketSessions);
- }
-}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java
index 4d145a75..93202ebf 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketEndpoint.java
@@ -2,39 +2,54 @@ package com.commafeed.frontend.ws;
import java.io.IOException;
-import jakarta.inject.Inject;
+import com.commafeed.CommaFeedConfiguration;
+import com.commafeed.backend.model.User;
+import com.commafeed.security.AuthenticationContext;
+
import jakarta.inject.Singleton;
import jakarta.websocket.CloseReason;
import jakarta.websocket.CloseReason.CloseCodes;
-import jakarta.websocket.Endpoint;
-import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.OnClose;
+import jakarta.websocket.OnMessage;
+import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
+import jakarta.websocket.server.ServerEndpoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
-@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
-public class WebSocketEndpoint extends Endpoint {
+@ServerEndpoint("/ws")
+@RequiredArgsConstructor
+public class WebSocketEndpoint {
+ private final AuthenticationContext authenticationContext;
+ private final CommaFeedConfiguration config;
private final WebSocketSessions sessions;
- @Override
- public void onOpen(Session session, EndpointConfig config) {
- Long userId = (Long) config.getUserProperties().get(WebSocketConfigurator.SESSIONKEY_USERID);
- if (userId == null) {
+ @OnOpen
+ public void onOpen(Session session) {
+ User user = authenticationContext.getCurrentUser();
+ if (user == null) {
reject(session);
return;
}
- log.debug("created websocket session for user {}", userId);
- sessions.add(userId, session);
+ log.debug("created websocket session for user '{}'", user.getName());
+ sessions.add(user.getId(), session);
+ session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000);
+ }
- session.addMessageHandler(String.class, message -> {
- if ("ping".equals(message)) {
- session.getAsyncRemote().sendText("pong");
- }
- });
+ @OnMessage
+ public void onMessage(String message, Session session) {
+ if ("ping".equals(message)) {
+ session.getAsyncRemote().sendText("pong");
+ }
+ }
+
+ @OnClose
+ public void onClose(Session session) {
+ sessions.remove(session);
}
private void reject(Session session) {
@@ -45,9 +60,4 @@ public class WebSocketEndpoint extends Endpoint {
}
}
- @Override
- public void onClose(Session session, CloseReason reason) {
- sessions.remove(session);
- }
-
}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java
index e82563e7..93caea5b 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java
+++ b/commafeed-server/src/main/java/com/commafeed/frontend/ws/WebSocketSessions.java
@@ -8,7 +8,6 @@ import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.model.User;
-import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.websocket.Session;
import lombok.extern.slf4j.Slf4j;
@@ -20,7 +19,6 @@ public class WebSocketSessions {
// a user may have multiple sessions (two tabs, two devices, ...)
private final Map> sessions = new ConcurrentHashMap<>();
- @Inject
public WebSocketSessions(MetricRegistry metrics) {
metrics.register(MetricRegistry.name(getClass(), "users"),
(Gauge) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count());
diff --git a/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java
new file mode 100644
index 00000000..e0f5f794
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/AuthenticationContext.java
@@ -0,0 +1,29 @@
+package com.commafeed.security;
+
+import com.commafeed.backend.dao.UserDAO;
+import com.commafeed.backend.model.User;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import jakarta.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Singleton
+public class AuthenticationContext {
+
+ private final SecurityIdentity securityIdentity;
+ private final UserDAO userDAO;
+
+ public User getCurrentUser() {
+ if (securityIdentity.isAnonymous()) {
+ return null;
+ }
+
+ String userId = securityIdentity.getPrincipal().getName();
+ if (userId == null) {
+ return null;
+ }
+
+ return userDAO.findById(Long.valueOf(userId));
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/security/Roles.java b/commafeed-server/src/main/java/com/commafeed/security/Roles.java
new file mode 100644
index 00000000..f614ca94
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/Roles.java
@@ -0,0 +1,6 @@
+package com.commafeed.security;
+
+public class Roles {
+ public static final String USER = "USER";
+ public static final String ADMIN = "ADMIN";
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java
new file mode 100644
index 00000000..5c1c95e7
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseApiKeyIdentityProvider.java
@@ -0,0 +1,50 @@
+package com.commafeed.security.identity;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.commafeed.backend.dao.UnitOfWork;
+import com.commafeed.backend.model.User;
+import com.commafeed.backend.model.UserRole.Role;
+import com.commafeed.backend.service.UserService;
+
+import io.quarkus.security.AuthenticationFailedException;
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.IdentityProvider;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TokenAuthenticationRequest;
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.smallrye.mutiny.Uni;
+import jakarta.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Singleton
+public class DatabaseApiKeyIdentityProvider implements IdentityProvider {
+
+ private final UnitOfWork unitOfWork;
+ private final UserService userService;
+
+ @Override
+ public Class getRequestType() {
+ return TokenAuthenticationRequest.class;
+ }
+
+ @Override
+ public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) {
+ return context.runBlocking(() -> {
+ Optional user = unitOfWork.call(() -> userService.login(request.getToken().getToken()));
+ if (user.isEmpty()) {
+ throw new AuthenticationFailedException("could not find a user with this api key");
+ }
+
+ Set roles = unitOfWork.call(() -> userService.getRoles(user.get()));
+ return QuarkusSecurityIdentity.builder()
+ .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
+ .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
+ .build();
+ });
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java
new file mode 100644
index 00000000..dc3537da
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/identity/DatabaseUsernamePasswordIdentityProvider.java
@@ -0,0 +1,51 @@
+package com.commafeed.security.identity;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.commafeed.backend.dao.UnitOfWork;
+import com.commafeed.backend.model.User;
+import com.commafeed.backend.model.UserRole.Role;
+import com.commafeed.backend.service.UserService;
+
+import io.quarkus.security.AuthenticationFailedException;
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.IdentityProvider;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.smallrye.mutiny.Uni;
+import jakarta.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Singleton
+public class DatabaseUsernamePasswordIdentityProvider implements IdentityProvider {
+
+ private final UnitOfWork unitOfWork;
+ private final UserService userService;
+
+ @Override
+ public Class getRequestType() {
+ return UsernamePasswordAuthenticationRequest.class;
+ }
+
+ @Override
+ public Uni authenticate(UsernamePasswordAuthenticationRequest request, AuthenticationRequestContext context) {
+ return context.runBlocking(() -> {
+ Optional user = unitOfWork
+ .call(() -> userService.login(request.getUsername(), new String(request.getPassword().getPassword())));
+ if (user.isEmpty()) {
+ throw new AuthenticationFailedException("wrong username or password");
+ }
+
+ Set roles = unitOfWork.call(() -> userService.getRoles(user.get()));
+ return QuarkusSecurityIdentity.builder()
+ .setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
+ .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
+ .build();
+ });
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java
new file mode 100644
index 00000000..ecfffc57
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/identity/TrustedIdentityProvider.java
@@ -0,0 +1,57 @@
+package com.commafeed.security.identity;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.commafeed.backend.dao.UnitOfWork;
+import com.commafeed.backend.dao.UserDAO;
+import com.commafeed.backend.model.User;
+import com.commafeed.backend.model.UserRole.Role;
+import com.commafeed.backend.service.UserService;
+import com.commafeed.backend.service.internal.PostLoginActivities;
+
+import io.quarkus.security.AuthenticationFailedException;
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.IdentityProvider;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.smallrye.mutiny.Uni;
+import jakarta.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Singleton
+public class TrustedIdentityProvider implements IdentityProvider {
+
+ private final UnitOfWork unitOfWork;
+ private final UserService userService;
+ private final UserDAO userDAO;
+ private final PostLoginActivities postLoginActivities;
+
+ @Override
+ public Class getRequestType() {
+ return TrustedAuthenticationRequest.class;
+ }
+
+ @Override
+ public Uni authenticate(TrustedAuthenticationRequest request, AuthenticationRequestContext context) {
+ return context.runBlocking(() -> {
+ Long userId = Long.valueOf(request.getPrincipal());
+ User user = unitOfWork.call(() -> userDAO.findById(userId));
+ if (user == null) {
+ throw new AuthenticationFailedException("user not found");
+ }
+
+ // execute post login activities manually because we didn't call login() since we received a trusted authentication request
+ unitOfWork.run(() -> postLoginActivities.executeFor(user));
+
+ Set roles = unitOfWork.call(() -> userService.getRoles(user));
+ return QuarkusSecurityIdentity.builder()
+ .setPrincipal(new QuarkusPrincipal(String.valueOf(userId)))
+ .addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
+ .build();
+ });
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java
new file mode 100644
index 00000000..b611f827
--- /dev/null
+++ b/commafeed-server/src/main/java/com/commafeed/security/mechanism/ApiKeyAuthenticationMecanism.java
@@ -0,0 +1,46 @@
+package com.commafeed.security.mechanism;
+
+import java.util.Optional;
+import java.util.Set;
+
+import io.quarkus.security.credential.TokenCredential;
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.AuthenticationRequest;
+import io.quarkus.security.identity.request.TokenAuthenticationRequest;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ApiKeyAuthenticationMecanism implements HttpAuthenticationMechanism {
+
+ @Override
+ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
+ // only authorize api key for GET requests
+ if (!context.request().method().name().equals("GET")) {
+ return Uni.createFrom().optional(Optional.empty());
+ }
+
+ String apiKey = context.request().getParam("apiKey");
+ if (apiKey == null) {
+ return Uni.createFrom().optional(Optional.empty());
+ }
+
+ TokenCredential token = new TokenCredential(apiKey, "apiKey");
+ TokenAuthenticationRequest request = new TokenAuthenticationRequest(token);
+ return identityProviderManager.authenticate(request);
+ }
+
+ @Override
+ public Uni getChallenge(RoutingContext context) {
+ return Uni.createFrom().optional(Optional.empty());
+ }
+
+ @Override
+ public Set> getCredentialTypes() {
+ return Set.of(TokenAuthenticationRequest.class);
+ }
+}
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java
similarity index 94%
rename from commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java
rename to commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java
index 5063770f..4b842c77 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/PasswordConstraintValidator.java
+++ b/commafeed-server/src/main/java/com/commafeed/security/password/PasswordConstraintValidator.java
@@ -1,4 +1,4 @@
-package com.commafeed.frontend.auth;
+package com.commafeed.security.password;
import java.util.List;
diff --git a/commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java
similarity index 90%
rename from commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java
rename to commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java
index 63efdb4d..fc559c50 100644
--- a/commafeed-server/src/main/java/com/commafeed/frontend/auth/ValidPassword.java
+++ b/commafeed-server/src/main/java/com/commafeed/security/password/ValidPassword.java
@@ -1,4 +1,4 @@
-package com.commafeed.frontend.auth;
+package com.commafeed.security.password;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
diff --git a/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json b/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json
new file mode 100644
index 00000000..a8a50b21
--- /dev/null
+++ b/commafeed-server/src/main/resources/META-INF/native-image/commafeed/resource-config.json
@@ -0,0 +1,10 @@
+{
+ "resources": {
+ "includes": [
+ { "pattern": "^default_banner\\.txt$" },
+ { "pattern": "^images/default_favicon\\.gif$" },
+ { "pattern": "^git\\.properties$" },
+ { "pattern": "^rome\\.properties$" }
+ ]
+ }
+}
diff --git a/commafeed-server/src/main/resources/application.properties b/commafeed-server/src/main/resources/application.properties
new file mode 100644
index 00000000..733fd569
--- /dev/null
+++ b/commafeed-server/src/main/resources/application.properties
@@ -0,0 +1,43 @@
+# http
+quarkus.http.port=8082
+quarkus.http.test-port=8085
+
+# security
+quarkus.http.auth.basic=true
+quarkus.http.auth.form.enabled=true
+quarkus.http.auth.form.http-only-cookie=true
+quarkus.http.auth.form.timeout=P30d
+quarkus.http.auth.form.landing-page=
+quarkus.http.auth.form.login-page=
+quarkus.http.auth.form.error-page=
+
+# websocket
+quarkus.websocket.dispatch-to-worker=true
+
+# database
+quarkus.datasource.db-kind=h2
+quarkus.liquibase.change-log=migrations.xml
+quarkus.liquibase.migrate-at-start=true
+
+# shutdown
+quarkus.shutdown.timeout=5s
+
+
+# dev profile overrides
+%dev.quarkus.http.port=8083
+%dev.quarkus.http.auth.session.encryption-key=123456789012345678901234567890
+%dev.quarkus.log.category."com.commafeed".level=DEBUG
+# %dev.quarkus.hibernate-orm.log.sql=true
+
+
+# test profile overrides
+%test.quarkus.log.category."org.mockserver".level=WARN
+%test.quarkus.log.category."liquibase".level=WARN
+%test.commafeed.users.create-demo-account=true
+%test.commafeed.users.allow-registrations=true
+%test.commafeed.smtp.host=localhost
+%test.commafeed.smtp.port=3025
+%test.commafeed.smtp.tls=false
+%test.commafeed.smtp.user-name=user
+%test.commafeed.smtp.password=pass
+%test.commafeed.smtp.from-address=noreply@commafeed.com
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml
index 7b60e188..b1713439 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.0.xml
@@ -1,7 +1,7 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
7:6d3ad493d25dd9c50067e804efc9ffcc
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml
index 065ead9f..094de039 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.1.xml
@@ -1,7 +1,7 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml
index 3bbe49c6..c735f521 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.2.xml
@@ -1,6 +1,7 @@
-
+
@@ -14,7 +15,7 @@
+ referencedTableName="FEEDS" referencedColumnNames="id" />
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml
index 691dbde5..3e5f07d2 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.3.xml
@@ -1,6 +1,7 @@
-
+
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml
index 4920edcf..578c0bc0 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.4.xml
@@ -1,6 +1,7 @@
-
+
@@ -31,9 +32,9 @@
+ referencedTableName="FEEDENTRIES" referencedColumnNames="id" />
+ referencedTableName="USERS" referencedColumnNames="id" />
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml
index 09ce1137..2a8f58f7 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-1.5.xml
@@ -1,6 +1,7 @@
-
+
8:58e8060bba0ec9d448f4346eb35d815c
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml
index df158560..6125c98e 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.1.xml
@@ -1,6 +1,7 @@
-
+
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml
index 1af324a6..6fb75000 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.2.xml
@@ -1,6 +1,7 @@
-
+
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml
index 726e4228..29e7fd95 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-2.6.xml
@@ -1,7 +1,7 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
@@ -26,7 +26,7 @@
8:39e5a9ff312af90d82f87c88abf1c66d
+ columnDataType="VARCHAR(4096)" />
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml
index 782777bc..9d337166 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.2.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml
index 238b9b1b..87835d25 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.5.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml
index eee22181..c3c1c916 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.6.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml
index 2eaeaa95..52bd6b36 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.8.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml
index 83ce42b0..f2c386a3 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-3.9.xml
@@ -1,7 +1,7 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml
index 6e898e72..3d3d6717 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.0.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml
index 7540a1fe..9c00a813 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.1.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml
index a52f67d6..0af84537 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.2.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
9:bf66bf7def9ec3dab1f365f7230d92cf
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml
index 806095f7..a9d696c1 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.3.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
9:663bcc7c6df5b832ec2109a3afcff5c6
diff --git a/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml b/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml
index c74b779c..c15ed003 100644
--- a/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml
+++ b/commafeed-server/src/main/resources/changelogs/db.changelog-4.4.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
9:078593b238a4639a97a3cd82f7e5e30d
diff --git a/commafeed-server/src/main/resources/banner.txt b/commafeed-server/src/main/resources/default_banner.txt
similarity index 100%
rename from commafeed-server/src/main/resources/banner.txt
rename to commafeed-server/src/main/resources/default_banner.txt
diff --git a/commafeed-server/src/main/resources/migrations.xml b/commafeed-server/src/main/resources/migrations.xml
index 2d951ed1..eae6c227 100644
--- a/commafeed-server/src/main/resources/migrations.xml
+++ b/commafeed-server/src/main/resources/migrations.xml
@@ -1,7 +1,7 @@
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
diff --git a/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java b/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java
deleted file mode 100644
index 0cc7a0fb..00000000
--- a/commafeed-server/src/test/java/com/commafeed/CommaFeedDropwizardAppExtension.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.commafeed;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-
-import javax.sql.DataSource;
-
-import org.mockserver.socket.PortFactory;
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.containers.JdbcDatabaseContainer;
-import org.testcontainers.containers.MariaDBContainer;
-import org.testcontainers.containers.MySQLContainer;
-import org.testcontainers.containers.PostgreSQLContainer;
-import org.testcontainers.utility.DockerImageName;
-
-import com.codahale.metrics.MetricRegistry;
-import com.commafeed.CommaFeedConfiguration.CacheType;
-
-import io.dropwizard.testing.ConfigOverride;
-import io.dropwizard.testing.ResourceHelpers;
-import io.dropwizard.testing.junit5.DropwizardAppExtension;
-import redis.clients.jedis.Jedis;
-import redis.clients.jedis.JedisPool;
-
-public class CommaFeedDropwizardAppExtension extends DropwizardAppExtension {
- private static final String TEST_DATABASE = System.getenv().getOrDefault("TEST_DATABASE", "h2");
- private static final boolean REDIS_ENABLED = Boolean.parseBoolean(System.getenv().getOrDefault("REDIS", "false"));
-
- private static final ConfigOverride[] CONFIG_OVERRIDES;
- private static final List DROP_ALL_STATEMENTS;
- static {
- List overrides = new ArrayList<>();
- overrides.add(ConfigOverride.config("server.applicationConnectors[0].port", String.valueOf(PortFactory.findFreePort())));
-
- Properties imageNames = readProperties("/docker-images.properties");
-
- DatabaseConfiguration config = buildConfiguration(TEST_DATABASE, imageNames.getProperty(TEST_DATABASE));
- JdbcDatabaseContainer> container = config.container();
- if (container != null) {
- container.withDatabaseName("commafeed");
- container.withEnv("TZ", "UTC");
- container.start();
-
- overrides.add(ConfigOverride.config("database.url", container.getJdbcUrl()));
- overrides.add(ConfigOverride.config("database.user", container.getUsername()));
- overrides.add(ConfigOverride.config("database.password", container.getPassword()));
- overrides.add(ConfigOverride.config("database.driverClass", container.getDriverClassName()));
- }
-
- if (REDIS_ENABLED) {
- GenericContainer> redis = new GenericContainer<>(DockerImageName.parse(imageNames.getProperty("redis")))
- .withExposedPorts(6379);
- redis.start();
-
- overrides.add(ConfigOverride.config("app.cache", "redis"));
- overrides.add(ConfigOverride.config("redis.host", redis.getHost()));
- overrides.add(ConfigOverride.config("redis.port", redis.getMappedPort(6379).toString()));
- }
-
- CONFIG_OVERRIDES = overrides.toArray(new ConfigOverride[0]);
- DROP_ALL_STATEMENTS = config.dropAllStatements();
- }
-
- public CommaFeedDropwizardAppExtension() {
- super(CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"), CONFIG_OVERRIDES);
- }
-
- private static DatabaseConfiguration buildConfiguration(String databaseName, String imageName) {
- if ("postgresql".equals(databaseName)) {
- JdbcDatabaseContainer> container = new PostgreSQLContainer<>(imageName).withTmpFs(Map.of("/var/lib/postgresql/data", "rw"));
- return new DatabaseConfiguration(container, List.of("DROP SCHEMA public CASCADE", "CREATE SCHEMA public"));
- } else if ("mysql".equals(databaseName)) {
- JdbcDatabaseContainer> container = new MySQLContainer<>(imageName).withTmpFs(Map.of("/var/lib/mysql", "rw"));
- return new DatabaseConfiguration(container, List.of("DROP DATABASE IF EXISTS commafeed", " CREATE DATABASE commafeed"));
- } else if ("mariadb".equals(databaseName)) {
- JdbcDatabaseContainer> container = new MariaDBContainer<>(imageName).withTmpFs(Map.of("/var/lib/mysql", "rw"));
- return new DatabaseConfiguration(container, List.of("DROP DATABASE IF EXISTS commafeed", " CREATE DATABASE commafeed"));
- } else {
- // h2
- return new DatabaseConfiguration(null, List.of("DROP ALL OBJECTS"));
- }
- }
-
- private static Properties readProperties(String path) {
- Properties properties = new Properties();
- try (InputStream is = CommaFeedDropwizardAppExtension.class.getResourceAsStream(path)) {
- properties.load(is);
- } catch (IOException e) {
- throw new RuntimeException("could not read resource " + path, e);
- }
- return properties;
- }
-
- @Override
- public void after() {
- super.after();
-
- // clean database after each test
- DataSource dataSource = getConfiguration().getDataSourceFactory().build(new MetricRegistry(), "cleanup");
- try (Connection connection = dataSource.getConnection()) {
- for (String statement : DROP_ALL_STATEMENTS) {
- connection.prepareStatement(statement).executeUpdate();
- }
- } catch (SQLException e) {
- throw new RuntimeException("could not cleanup database", e);
- }
-
- // clean redis cache after each test
- if (getConfiguration().getApplicationSettings().getCache() == CacheType.REDIS) {
- try (JedisPool pool = getConfiguration().getRedisPoolFactory().build(); Jedis jedis = pool.getResource()) {
- jedis.flushAll();
- }
- }
- }
-
- private record DatabaseConfiguration(JdbcDatabaseContainer> container, List dropAllStatements) {
- }
-
-}
diff --git a/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java
new file mode 100644
index 00000000..97e43ddd
--- /dev/null
+++ b/commafeed-server/src/test/java/com/commafeed/DatabaseReset.java
@@ -0,0 +1,34 @@
+package com.commafeed;
+
+import org.kohsuke.MetaInfServices;
+
+import com.commafeed.backend.service.db.DatabaseStartupService;
+
+import io.quarkus.liquibase.LiquibaseFactory;
+import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
+import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
+import jakarta.enterprise.inject.spi.CDI;
+import liquibase.Liquibase;
+import liquibase.exception.LiquibaseException;
+
+/**
+ * Resets database between tests
+ */
+@MetaInfServices
+public class DatabaseReset implements QuarkusTestBeforeEachCallback {
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void beforeEach(QuarkusTestMethodContext context) {
+ LiquibaseFactory liquibaseFactory = CDI.current().select(LiquibaseFactory.class).get();
+ try (Liquibase liquibase = liquibaseFactory.createLiquibase()) {
+ liquibase.dropAll();
+ liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels());
+ } catch (LiquibaseException e) {
+ throw new RuntimeException(e);
+ }
+
+ DatabaseStartupService databaseStartupService = CDI.current().select(DatabaseStartupService.class).get();
+ databaseStartupService.populateInitialData();
+ }
+}
diff --git a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java
index 06da263d..e0289599 100644
--- a/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java
+++ b/commafeed-server/src/test/java/com/commafeed/backend/HttpGetterTest.java
@@ -1,14 +1,16 @@
package com.commafeed.backend;
import java.io.IOException;
+import java.math.BigInteger;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.io.IOUtils;
import org.apache.hc.client5.http.ConnectTimeoutException;
-import org.eclipse.jetty.http.HttpStatus;
+import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -26,13 +28,13 @@ import org.mockserver.model.MediaType;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
-import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
+import com.commafeed.CommaFeedVersion;
import com.commafeed.backend.HttpGetter.HttpResponseException;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.google.common.net.HttpHeaders;
-import io.dropwizard.util.DataSize;
+import io.quarkus.runtime.configuration.MemorySize;
@ExtendWith(MockServerExtension.class)
class HttpGetterTest {
@@ -51,21 +53,17 @@ class HttpGetterTest {
this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/";
this.feedContent = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/feed/rss.xml")));
- ApplicationSettings settings = new ApplicationSettings();
- settings.setUserAgent("http-getter-test");
- settings.setBackgroundThreads(3);
- settings.setMaxFeedResponseSize(DataSize.kilobytes(10));
+ CommaFeedConfiguration config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
+ Mockito.when(config.feedRefresh().userAgent()).thenReturn(Optional.of("http-getter-test"));
+ Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
+ Mockito.when(config.feedRefresh().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000")));
- CommaFeedConfiguration config = new CommaFeedConfiguration();
- config.setApplicationSettings(settings);
-
- this.getter = new HttpGetter(config, Mockito.mock(MetricRegistry.class));
+ this.getter = new HttpGetter(config, Mockito.mock(CommaFeedVersion.class), Mockito.mock(MetricRegistry.class));
}
@ParameterizedTest
@ValueSource(
- ints = { HttpStatus.UNAUTHORIZED_401, HttpStatus.FORBIDDEN_403, HttpStatus.NOT_FOUND_404,
- HttpStatus.INTERNAL_SERVER_ERROR_500 })
+ ints = { HttpStatus.SC_UNAUTHORIZED, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_NOT_FOUND, HttpStatus.SC_INTERNAL_SERVER_ERROR })
void errorCodes(int code) {
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withStatusCode(code));
@@ -93,8 +91,8 @@ class HttpGetterTest {
@ParameterizedTest
@ValueSource(
- ints = { HttpStatus.MOVED_PERMANENTLY_301, HttpStatus.MOVED_TEMPORARILY_302, HttpStatus.TEMPORARY_REDIRECT_307,
- HttpStatus.PERMANENT_REDIRECT_308 })
+ ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT,
+ HttpStatus.SC_PERMANENT_REDIRECT })
void followRedirects(int code) throws Exception {
// first redirect
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/"))
@@ -129,7 +127,7 @@ class HttpGetterTest {
void connectTimeout() {
// try to connect to a non-routable address
// https://stackoverflow.com/a/904609
- Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 2000));
+ Assertions.assertThrows(ConnectTimeoutException.class, () -> getter.getBinary("http://10.255.255.1", 500));
}
@Test
@@ -144,7 +142,7 @@ class HttpGetterTest {
@Test
void lastModifiedReturns304() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456"))
- .respond(HttpResponse.response().withStatusCode(HttpStatus.NOT_MODIFIED_304));
+ .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED));
Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, "123456", null, TIMEOUT));
}
@@ -152,7 +150,7 @@ class HttpGetterTest {
@Test
void eTagReturns304() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_NONE_MATCH, "78910"))
- .respond(HttpResponse.response().withStatusCode(HttpStatus.NOT_MODIFIED_304));
+ .respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED));
Assertions.assertThrows(NotModifiedException.class, () -> getter.getBinary(this.feedUrl, null, "78910", TIMEOUT));
}
@@ -195,7 +193,7 @@ class HttpGetterTest {
@Test
void largeFeedWithContentLengthHeader() {
- byte[] bytes = new byte[(int) DataSize.kilobytes(100).toBytes()];
+ byte[] bytes = new byte[100000];
Arrays.fill(bytes, (byte) 1);
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody(bytes));
@@ -205,7 +203,7 @@ class HttpGetterTest {
@Test
void largeFeedWithoutContentLengthHeader() {
- byte[] bytes = new byte[(int) DataSize.kilobytes(100).toBytes()];
+ byte[] bytes = new byte[100000];
Arrays.fill(bytes, (byte) 1);
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
diff --git a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java
index b47ed808..c231cdd9 100644
--- a/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java
+++ b/commafeed-server/src/test/java/com/commafeed/backend/feed/FeedFetcherTest.java
@@ -1,7 +1,7 @@
package com.commafeed.backend.feed;
import java.time.Instant;
-import java.util.Set;
+import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -28,7 +28,7 @@ class FeedFetcherTest {
private HttpGetter getter;
@Mock
- private Set urlProviders;
+ private List urlProviders;
private FeedFetcher fetcher;
diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java
deleted file mode 100644
index 578cbc3e..00000000
--- a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.commafeed.backend.service.db;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Objects;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-class H2MigrationServiceTest {
-
- @TempDir
- private Path root;
-
- @Test
- void testMigrateIfNeeded() throws IOException {
- Path path = root.resolve("database.mv.db");
- Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/h2-migration/database-v2.1.214.mv.db")), path);
-
- H2MigrationService service = new H2MigrationService();
- Assertions.assertEquals(2, service.getH2FileFormat(path));
-
- service.migrateIfNeeded(path, "sa", "sa");
- Assertions.assertEquals(3, service.getH2FileFormat(path));
- }
-
-}
\ No newline at end of file
diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java
index 56f12089..2fcfbbf6 100644
--- a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java
@@ -1,20 +1,35 @@
package com.commafeed.e2e;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import com.commafeed.CommaFeedDropwizardAppExtension;
+import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.assertions.PlaywrightAssertions;
import com.microsoft.playwright.options.AriaRole;
-import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
+import io.quarkus.test.junit.QuarkusTest;
-@ExtendWith(DropwizardExtensionsSupport.class)
-class AuthentificationIT extends PlaywrightTestBase {
+@QuarkusTest
+class AuthentificationIT {
- private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension();
+ private final Playwright playwright = Playwright.create();
+ private final Browser browser = playwright.chromium().launch();
+
+ private Page page;
+
+ @BeforeEach
+ void init() {
+ page = browser.newContext().newPage();
+ }
+
+ @AfterEach
+ void cleanup() {
+ playwright.close();
+ }
@Test
void loginFail() {
@@ -29,7 +44,7 @@ class AuthentificationIT extends PlaywrightTestBase {
void loginSuccess() {
page.navigate(getLoginPageUrl());
PlaywrightTestUtils.login(page);
- PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all");
+ PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all");
}
@Test
@@ -56,10 +71,10 @@ class AuthentificationIT extends PlaywrightTestBase {
page.getByPlaceholder("E-mail address").fill("user@domain.com");
page.getByPlaceholder("Password").fill("MyPassword1!");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign up")).click();
- PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all");
+ PlaywrightAssertions.assertThat(page).hasURL("http://localhost:8085/#/app/category/all");
}
private String getLoginPageUrl() {
- return "http://localhost:" + EXT.getLocalPort() + "/#/login";
+ return "http://localhost:8085/#/login";
}
}
diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java
deleted file mode 100644
index 81735c3b..00000000
--- a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.commafeed.e2e;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Optional;
-
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.TestWatcher;
-
-import com.microsoft.playwright.Browser;
-import com.microsoft.playwright.Browser.NewContextOptions;
-import com.microsoft.playwright.BrowserContext;
-import com.microsoft.playwright.Page;
-import com.microsoft.playwright.Playwright;
-import com.microsoft.playwright.Tracing;
-
-/**
- * Base class for all Playwright tests.
- *
- *
- * - Takes a screenshot on failure
- * - Keeps the video on failure
- * - Saves a trace file on failure
- *
- *
- * inspired by https://github.com/microsoft/playwright-java/issues/503#issuecomment-872636373
- *
- */
-@ExtendWith(PlaywrightTestBase.SaveArtifactsOnTestFailed.class)
-public abstract class PlaywrightTestBase {
-
- private static Playwright playwright;
- private static Browser browser;
-
- protected Page page;
- private BrowserContext context;
-
- @BeforeAll
- static void initBrowser() {
- playwright = Playwright.create();
- browser = playwright.chromium().launch();
- }
-
- @AfterAll
- static void closeBrowser() {
- playwright.close();
- }
-
- protected void customizeNewContextOptions(NewContextOptions options) {
- // override in subclasses to customize the browser context
- }
-
- protected static class SaveArtifactsOnTestFailed implements TestWatcher, BeforeEachCallback {
-
- // defined in the config of maven-failsafe-plugin in pom.xml
- private final String buildDirectory = System.getProperty("buildDirectory", "target");
- private final String directory = buildDirectory + "/playwright-artifacts";
-
- @Override
- public void beforeEach(ExtensionContext context) {
- PlaywrightTestBase testInstance = getTestInstance(context);
-
- NewContextOptions newContextOptions = new Browser.NewContextOptions().setRecordVideoDir(Paths.get(directory));
- testInstance.customizeNewContextOptions(newContextOptions);
- testInstance.context = PlaywrightTestBase.browser.newContext(newContextOptions);
- testInstance.context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));
-
- testInstance.page = testInstance.context.newPage();
- }
-
- @Override
- public void testFailed(ExtensionContext context, Throwable cause) {
- PlaywrightTestBase testInstance = getTestInstance(context);
-
- String fileName = getFileName(context);
-
- saveScreenshot(testInstance, fileName);
- saveTrace(testInstance, fileName);
-
- testInstance.context.close();
-
- saveVideo(testInstance, fileName);
- }
-
- @Override
- public void testAborted(ExtensionContext context, Throwable cause) {
- PlaywrightTestBase testInstance = getTestInstance(context);
- testInstance.context.close();
- testInstance.page.video().delete();
- }
-
- @Override
- public void testDisabled(ExtensionContext context, Optional reason) {
- PlaywrightTestBase testInstance = getTestInstance(context);
- testInstance.context.close();
- testInstance.page.video().delete();
- }
-
- @Override
- public void testSuccessful(ExtensionContext context) {
- PlaywrightTestBase testInstance = getTestInstance(context);
- testInstance.context.close();
- testInstance.page.video().delete();
- }
-
- private PlaywrightTestBase getTestInstance(ExtensionContext context) {
- return (PlaywrightTestBase) context.getRequiredTestInstance();
- }
-
- private String getFileName(ExtensionContext context) {
- return String.format("%s.%s-%s", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(),
- DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm-ss").format(ZonedDateTime.now()));
- }
-
- private void saveScreenshot(PlaywrightTestBase testInstance, String fileName) {
- byte[] screenshot = testInstance.page.screenshot();
- try {
- Files.write(Paths.get(directory, fileName + ".png"), screenshot);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private void saveTrace(PlaywrightTestBase testInstance, String fileName) {
- testInstance.context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get(directory, fileName + ".zip")));
- }
-
- private void saveVideo(PlaywrightTestBase testInstance, String fileName) {
- testInstance.page.video().saveAs(Paths.get(directory, fileName + ".webm"));
- testInstance.page.video().delete();
- }
- }
-}
diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java
index 2264701f..fa99d241 100644
--- a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java
@@ -6,43 +6,51 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
import org.mockserver.client.MockServerClient;
-import org.mockserver.junit.jupiter.MockServerExtension;
+import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
-import com.commafeed.CommaFeedDropwizardAppExtension;
+import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.assertions.PlaywrightAssertions;
import com.microsoft.playwright.options.AriaRole;
-import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
+import io.quarkus.test.junit.QuarkusTest;
-@ExtendWith(DropwizardExtensionsSupport.class)
-@ExtendWith(MockServerExtension.class)
-class ReadingIT extends PlaywrightTestBase {
+@QuarkusTest
+class ReadingIT {
- private static final CommaFeedDropwizardAppExtension EXT = new CommaFeedDropwizardAppExtension();
+ private final Playwright playwright = Playwright.create();
+ private final Browser browser = playwright.chromium().launch();
+ private Page page;
private MockServerClient mockServerClient;
@BeforeEach
- void init(MockServerClient mockServerClient) throws IOException {
- this.mockServerClient = mockServerClient;
+ void init() throws IOException {
+ this.page = browser.newContext().newPage();
+ this.mockServerClient = ClientAndServer.startClientAndServer(0);
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
.withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8))
.withDelay(TimeUnit.MILLISECONDS, 100));
}
+ @AfterEach
+ void cleanup() {
+ playwright.close();
+ }
+
@Test
void scenario() {
// login
- page.navigate("http://localhost:" + EXT.getLocalPort());
+ page.navigate("http://localhost:8085");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Log in")).click();
PlaywrightTestUtils.login(page);
diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java
deleted file mode 100644
index 1069ef8f..00000000
--- a/commafeed-server/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.commafeed.frontend.auth;
-
-import java.util.Optional;
-
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import com.commafeed.backend.dao.UserDAO;
-import com.commafeed.backend.model.User;
-import com.commafeed.backend.service.UserService;
-import com.commafeed.backend.service.internal.PostLoginActivities;
-import com.commafeed.frontend.session.SessionHelper;
-
-class SecurityCheckFactoryTest {
-
- @Test
- void cookieLoginShouldPerformPostLoginActivities() {
- User userInSession = new User();
- UserDAO userDAO = Mockito.mock(UserDAO.class);
- Mockito.when(userDAO.findById(1L)).thenReturn(userInSession);
-
- SessionHelper sessionHelper = Mockito.mock(SessionHelper.class);
- Mockito.when(sessionHelper.getLoggedInUserId()).thenReturn(Optional.of(1L));
-
- PostLoginActivities postLoginActivities = Mockito.mock(PostLoginActivities.class);
-
- UserService service = new UserService(null, null, null, null, null, null, null, postLoginActivities);
-
- SecurityCheckFactory factory = new SecurityCheckFactory(userDAO, service, null, null, null, false);
- factory.cookieSessionLogin(sessionHelper);
-
- Mockito.verify(postLoginActivities).executeFor(userInSession);
- }
-
-}
diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java
deleted file mode 100644
index a1ccdb4d..00000000
--- a/commafeed-server/src/test/java/com/commafeed/frontend/resource/UserRestTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.commafeed.frontend.resource;
-
-import java.util.Collections;
-import java.util.Optional;
-
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentMatchers;
-import org.mockito.InOrder;
-import org.mockito.Mockito;
-
-import com.commafeed.backend.model.User;
-import com.commafeed.backend.model.UserRole.Role;
-import com.commafeed.backend.service.UserService;
-import com.commafeed.frontend.model.request.LoginRequest;
-import com.commafeed.frontend.model.request.RegistrationRequest;
-import com.commafeed.frontend.session.SessionHelper;
-
-class UserRestTest {
-
- @Test
- void loginShouldNotPopulateHttpSessionIfUnsuccessfull() {
- // Absent user
- Optional absentUser = Optional.empty();
-
- // Create UserService partial mock
- UserService service = Mockito.mock(UserService.class);
- Mockito.when(service.login("user", "password")).thenReturn(absentUser);
-
- UserREST userREST = new UserREST(null, null, null, service, null, null, null);
- SessionHelper sessionHelper = Mockito.mock(SessionHelper.class);
-
- LoginRequest req = new LoginRequest();
- req.setName("user");
- req.setPassword("password");
-
- userREST.login(req, sessionHelper);
-
- Mockito.verify(sessionHelper, Mockito.never()).setLoggedInUser(Mockito.any(User.class));
- }
-
- @Test
- void loginShouldPopulateHttpSessionIfSuccessfull() {
- // Create a user
- User user = new User();
-
- // Create UserService mock
- UserService service = Mockito.mock(UserService.class);
- Mockito.when(service.login("user", "password")).thenReturn(Optional.of(user));
-
- LoginRequest req = new LoginRequest();
- req.setName("user");
- req.setPassword("password");
-
- UserREST userREST = new UserREST(null, null, null, service, null, null, null);
- SessionHelper sessionHelper = Mockito.mock(SessionHelper.class);
-
- userREST.login(req, sessionHelper);
-
- Mockito.verify(sessionHelper).setLoggedInUser(user);
- }
-
- @Test
- void registerShouldRegisterAndThenLogin() {
- // Create UserService mock
- UserService service = Mockito.mock(UserService.class);
-
- RegistrationRequest req = new RegistrationRequest();
- req.setName("user");
- req.setPassword("password");
- req.setEmail("test@test.com");
-
- InOrder inOrder = Mockito.inOrder(service);
-
- SessionHelper sessionHelper = Mockito.mock(SessionHelper.class);
- UserREST userREST = new UserREST(null, null, null, service, null, null, null);
-
- userREST.registerUser(req, sessionHelper);
-
- inOrder.verify(service).register("user", "password", "test@test.com", Collections.singletonList(Role.USER));
- inOrder.verify(service).login("user", "password");
- }
-
- @Test
- void registerShouldPopulateHttpSession() {
- // Create a user
- User user = new User();
-
- // Create UserService mock
- UserService service = Mockito.mock(UserService.class);
- Mockito.when(service.register(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class),
- ArgumentMatchers.anyList())).thenReturn(user);
- Mockito.when(service.login(Mockito.any(String.class), Mockito.any(String.class))).thenReturn(Optional.of(user));
-
- RegistrationRequest req = new RegistrationRequest();
- req.setName("user");
- req.setPassword("password");
- req.setEmail("test@test.com");
-
- SessionHelper sessionHelper = Mockito.mock(SessionHelper.class);
- UserREST userREST = new UserREST(null, null, null, service, null, null, null);
-
- userREST.registerUser(req, sessionHelper);
-
- Mockito.verify(sessionHelper).setLoggedInUser(user);
- }
-
-}
diff --git a/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java b/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java
deleted file mode 100644
index 6f535879..00000000
--- a/commafeed-server/src/test/java/com/commafeed/frontend/session/SessionHelperTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.commafeed.frontend.session;
-
-import java.util.Optional;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpSession;
-
-class SessionHelperTest {
-
- @Test
- void gettingUserDoesNotCreateSession() {
- HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
-
- SessionHelper sessionHelper = new SessionHelper(request);
- sessionHelper.getLoggedInUserId();
-
- Mockito.verify(request).getSession(false);
- }
-
- @Test
- void gettingUserShouldNotReturnUserIfThereIsNoPreexistingHttpSession() {
- HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
- Mockito.when(request.getSession(false)).thenReturn(null);
-
- SessionHelper sessionHelper = new SessionHelper(request);
- Optional userId = sessionHelper.getLoggedInUserId();
-
- Assertions.assertFalse(userId.isPresent());
- }
-
- @Test
- void gettingUserShouldNotReturnUserIfUserNotPresentInHttpSession() {
- HttpSession session = Mockito.mock(HttpSession.class);
- Mockito.when(session.getAttribute(SessionHelper.SESSION_KEY_USER_ID)).thenReturn(null);
-
- HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
- Mockito.when(request.getSession(false)).thenReturn(session);
-
- SessionHelper sessionHelper = new SessionHelper(request);
- Optional userId = sessionHelper.getLoggedInUserId();
-
- Assertions.assertFalse(userId.isPresent());
- }
-
- @Test
- void gettingUserShouldReturnUserIfUserPresentInHttpSession() {
- HttpSession session = Mockito.mock(HttpSession.class);
- Mockito.when(session.getAttribute(SessionHelper.SESSION_KEY_USER_ID)).thenReturn(1L);
-
- HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
- Mockito.when(request.getSession(false)).thenReturn(session);
-
- SessionHelper sessionHelper = new SessionHelper(request);
- Optional userId = sessionHelper.getLoggedInUserId();
-
- Assertions.assertTrue(userId.isPresent());
- }
-
-}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java
index a6f3ad68..0d2f40a0 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/BaseIT.java
@@ -1,84 +1,82 @@
package com.commafeed.integration;
import java.io.IOException;
+import java.net.HttpCookie;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
+import java.util.List;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
+import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
-import org.eclipse.jetty.http.HttpStatus;
import org.glassfish.jersey.client.JerseyClientBuilder;
-import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider;
import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.extension.ExtendWith;
import org.mockserver.client.MockServerClient;
-import org.mockserver.junit.jupiter.MockServerExtension;
+import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
-import com.commafeed.CommaFeedDropwizardAppExtension;
+import com.commafeed.JacksonCustomizer;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Subscription;
-import com.commafeed.frontend.model.request.LoginRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
+import com.fasterxml.jackson.databind.ObjectMapper;
-import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
+import io.restassured.RestAssured;
+import io.restassured.http.Header;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import lombok.Getter;
@Getter
-@ExtendWith(DropwizardExtensionsSupport.class)
-@ExtendWith(MockServerExtension.class)
public abstract class BaseIT {
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
- private final CommaFeedDropwizardAppExtension extension = new CommaFeedDropwizardAppExtension() {
- @Override
- protected JerseyClientBuilder clientBuilder() {
- return configureClientBuilder(super.clientBuilder().register(MultiPartFeature.class));
- }
- };
-
- private Client client;
-
- private String feedUrl;
-
- private String baseUrl;
-
- private String apiBaseUrl;
-
- private String webSocketUrl;
-
private MockServerClient mockServerClient;
+ private Client client;
+ private String feedUrl;
+ private String baseUrl;
+ private String apiBaseUrl;
+ private String webSocketUrl;
protected JerseyClientBuilder configureClientBuilder(JerseyClientBuilder base) {
return base;
}
@BeforeEach
- void init(MockServerClient mockServerClient) throws IOException {
- this.mockServerClient = mockServerClient;
+ void init() throws IOException {
+ this.mockServerClient = ClientAndServer.startClientAndServer(0);
+
+ ObjectMapper mapper = new ObjectMapper();
+ new JacksonCustomizer().customize(mapper);
+ this.client = configureClientBuilder(new JerseyClientBuilder().register(new JacksonJsonProvider(mapper))).build();
+ this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/";
+ this.baseUrl = "http://localhost:8085/";
+ this.apiBaseUrl = this.baseUrl + "rest/";
+ this.webSocketUrl = "ws://localhost:8085/ws";
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml"));
- mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
-
- this.client = extension.client();
- this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/";
- this.baseUrl = "http://localhost:" + extension.getLocalPort() + "/";
- this.apiBaseUrl = this.baseUrl + "rest/";
- this.webSocketUrl = "ws://localhost:" + extension.getLocalPort() + "/ws";
+ this.mockServerClient.when(FEED_REQUEST)
+ .respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
}
@AfterEach
void cleanup() {
- this.client.close();
+ if (this.mockServerClient != null) {
+ this.mockServerClient.close();
+ }
+
+ if (this.client != null) {
+ this.client.close();
+ }
}
protected void feedNowReturnsMoreEntries() throws IOException {
@@ -88,14 +86,20 @@ public abstract class BaseIT {
mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
}
- protected String login() {
- LoginRequest req = new LoginRequest();
- req.setName("admin");
- req.setPassword("admin");
- try (Response response = client.target(apiBaseUrl + "user/login").request().post(Entity.json(req))) {
- Assertions.assertEquals(HttpStatus.OK_200, response.getStatus());
- return response.getCookies().get("JSESSIONID").getValue();
- }
+ protected List login() {
+ Form form = new Form();
+ form.param("j_username", "admin");
+ form.param("j_password", "admin");
+
+ List setCookieHeaders = RestAssured.given()
+ .formParams("j_username", "admin", "j_password", "admin")
+ .post(baseUrl + "j_security_check")
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .headers()
+ .getList(HttpHeaders.SET_COOKIE);
+ return setCookieHeaders.stream().flatMap(h -> HttpCookie.parse(h.getValue()).stream()).toList();
}
protected Long subscribe(String feedUrl) {
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java
index 11fe4b59..3864cd7b 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/SecurityIT.java
@@ -2,25 +2,28 @@ package com.commafeed.integration;
import java.util.Base64;
-import org.eclipse.jetty.http.HttpStatus;
+import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.UserModel;
+import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
+@QuarkusTest
class SecurityIT extends BaseIT {
@Test
void notLoggedIn() {
try (Response response = getClient().target(getApiBaseUrl() + "user/profile").request().get()) {
- Assertions.assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus());
+ Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus());
}
}
@@ -31,18 +34,18 @@ class SecurityIT extends BaseIT {
.request()
.header(HttpHeaders.AUTHORIZATION, auth)
.get()) {
- Assertions.assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus());
+ Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus());
}
}
@Test
void missingRole() {
String auth = "Basic " + Base64.getEncoder().encodeToString("demo:demo".getBytes());
- try (Response response = getClient().target(getApiBaseUrl() + "admin/settings")
+ try (Response response = getClient().target(getApiBaseUrl() + "admin/metrics")
.request()
.header(HttpHeaders.AUTHORIZATION, auth)
.get()) {
- Assertions.assertEquals(HttpStatus.FORBIDDEN_403, response.getStatus());
+ Assertions.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus());
}
}
@@ -84,5 +87,16 @@ class SecurityIT extends BaseIT {
.request()
.get(Entries.class);
Assertions.assertEquals("my title for this feed", entries.getName());
+
+ // mark entry as read and expect it won't work because it's not a GET request
+ MarkRequest markRequest = new MarkRequest();
+ markRequest.setId("1");
+ markRequest.setRead(true);
+ try (Response markResponse = getClient().target(getApiBaseUrl() + "entry/mark")
+ .queryParam("apiKey", apiKey)
+ .request()
+ .post(Entity.json(markRequest))) {
+ Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, markResponse.getStatus());
+ }
}
}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java
index 7cec67db..d5acb22c 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/WebSocketIT.java
@@ -1,6 +1,7 @@
package com.commafeed.integration;
import java.io.IOException;
+import java.net.HttpCookie;
import java.net.URI;
import java.util.Collections;
import java.util.List;
@@ -8,6 +9,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
import org.awaitility.Awaitility;
import org.glassfish.jersey.client.JerseyClientBuilder;
@@ -17,6 +19,7 @@ import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.request.FeedModificationRequest;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
import jakarta.websocket.ContainerProvider;
@@ -25,8 +28,10 @@ import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.Session;
import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.HttpHeaders;
import lombok.extern.slf4j.Slf4j;
+@QuarkusTest
@Slf4j
class WebSocketIT extends BaseIT {
@@ -49,18 +54,17 @@ class WebSocketIT extends BaseIT {
public void onClose(Session session, CloseReason closeReason) {
closeReasonRef.set(closeReason);
}
- }, buildConfig("fake-session-id"), URI.create(getWebSocketUrl()))) {
+ }, buildConfig(List.of()), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> closeReasonRef.get() != null);
- Assertions.assertEquals(CloseReason.CloseCodes.VIOLATED_POLICY, closeReasonRef.get().getCloseCode());
}
}
@Test
void subscribeAndGetsNotified() throws DeploymentException, IOException {
- String sessionId = login();
+ List cookies = login();
AtomicBoolean connected = new AtomicBoolean();
AtomicReference messageRef = new AtomicReference<>();
@@ -70,7 +74,7 @@ class WebSocketIT extends BaseIT {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
- }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) {
+ }, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
@@ -83,7 +87,7 @@ class WebSocketIT extends BaseIT {
@Test
void notNotifiedForFilteredEntries() throws DeploymentException, IOException {
- String sessionId = login();
+ List cookies = login();
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
FeedModificationRequest req = new FeedModificationRequest();
@@ -100,7 +104,7 @@ class WebSocketIT extends BaseIT {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
- }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) {
+ }, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
@@ -115,7 +119,7 @@ class WebSocketIT extends BaseIT {
@Test
void pingPong() throws DeploymentException, IOException {
- String sessionId = login();
+ List cookies = login();
AtomicBoolean connected = new AtomicBoolean();
AtomicReference messageRef = new AtomicReference<>();
@@ -125,7 +129,7 @@ class WebSocketIT extends BaseIT {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
- }, buildConfig(sessionId), URI.create(getWebSocketUrl()))) {
+ }, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
@@ -136,11 +140,12 @@ class WebSocketIT extends BaseIT {
}
}
- private ClientEndpointConfig buildConfig(String sessionId) {
+ private ClientEndpointConfig buildConfig(List cookies) {
return ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() {
@Override
public void beforeRequest(Map> headers) {
- headers.put("Cookie", Collections.singletonList("JSESSIONID=" + sessionId));
+ headers.put(HttpHeaders.COOKIE,
+ Collections.singletonList(cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))));
}
}).build();
}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java
index 138fd821..c6e53815 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/AdminIT.java
@@ -9,14 +9,15 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.client.Entity;
+@QuarkusTest
class AdminIT extends BaseIT {
@Override
@@ -24,12 +25,6 @@ class AdminIT extends BaseIT {
return base.register(HttpAuthenticationFeature.basic("admin", "admin"));
}
- @Test
- void getApplicationSettings() {
- ApplicationSettings settings = getClient().target(getApiBaseUrl() + "admin/settings").request().get(ApplicationSettings.class);
- Assertions.assertTrue(settings.getAllowRegistrations());
- }
-
@Nested
class Users {
@Test
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java
index c5c6c2a5..6b561c2d 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeedIT.java
@@ -9,9 +9,8 @@ import java.time.ZoneOffset;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
-import org.eclipse.jetty.http.HttpStatus;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
@@ -30,10 +29,12 @@ import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+@QuarkusTest
class FeedIT extends BaseIT {
@Override
@@ -69,19 +70,19 @@ class FeedIT extends BaseIT {
.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE)
.request()
.get()) {
- Assertions.assertEquals(HttpStatus.TEMPORARY_REDIRECT_307, response.getStatus());
+ Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus());
}
}
@Test
void unsubscribeFromUnknownFeed() {
- Assertions.assertEquals(HttpStatus.NOT_FOUND_404, unsubsribe(1L));
+ Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L));
}
@Test
void unsubscribeFromKnownFeed() {
long subscriptionId = subscribe(getFeedUrl());
- Assertions.assertEquals(HttpStatus.OK_200, unsubsribe(subscriptionId));
+ Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId));
}
private int unsubsribe(long subscriptionId) {
@@ -212,20 +213,7 @@ class FeedIT extends BaseIT {
void importExportOpml() throws IOException {
importOpml();
String opml = getClient().target(getApiBaseUrl() + "feed/export").request().get(String.class);
- String expextedOpml = """
-
-
-
- admin subscriptions in CommaFeed
-
-
-
-
-
-
-
- """;
- Assertions.assertEquals(StringUtils.normalizeSpace(expextedOpml), StringUtils.normalizeSpace(opml));
+ Assertions.assertTrue(opml.contains("admin subscriptions in CommaFeed"));
}
void importOpml() throws IOException {
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java
index 8f8ea1a0..8a52a003 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/FeverIT.java
@@ -12,9 +12,11 @@ import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.resource.fever.FeverResponse;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.Form;
+@QuarkusTest
class FeverIT extends BaseIT {
private Long userId;
@@ -26,7 +28,7 @@ class FeverIT extends BaseIT {
}
@BeforeEach
- void init() {
+ void setup() {
// create api key
ProfileModificationRequest req = new ProfileModificationRequest();
req.setCurrentPassword("admin");
@@ -73,6 +75,7 @@ class FeverIT extends BaseIT {
Form form = new Form();
form.param("api_key", Digests.md5Hex("admin:" + apiKey));
form.param(what, "1");
+
return getClient().target(getApiBaseUrl() + "fever/user/{userId}")
.resolveTemplate("userId", userId)
.request()
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java
index 82201f6f..2a9a6958 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/ServerIT.java
@@ -6,6 +6,9 @@ import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.ServerInfo;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
class ServerIT extends BaseIT {
@Test
@@ -16,7 +19,7 @@ class ServerIT extends BaseIT {
Assertions.assertTrue(serverInfos.isDemoAccountEnabled());
Assertions.assertTrue(serverInfos.isWebsocketEnabled());
Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval());
- Assertions.assertEquals(10000, serverInfos.getTreeReloadInterval());
+ Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval());
}
}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java
index 3eccde06..ae077b1c 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/rest/UserIT.java
@@ -1,30 +1,38 @@
package com.commafeed.integration.rest;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
import com.commafeed.frontend.model.request.PasswordResetRequest;
import com.commafeed.integration.BaseIT;
-import com.icegreen.greenmail.junit5.GreenMailExtension;
+import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetupTest;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.mail.internet.MimeMessage;
import jakarta.ws.rs.client.Entity;
+@QuarkusTest
class UserIT extends BaseIT {
@Nested
class PasswordReset {
- @RegisterExtension
- static final GreenMailExtension GREEN_MAIL = new GreenMailExtension(ServerSetupTest.SMTP);
+ private GreenMail greenMail;
@BeforeEach
- void init() {
- GREEN_MAIL.setUser("noreply@commafeed.com", "user", "pass");
+ void setup() {
+ this.greenMail = new GreenMail(ServerSetupTest.SMTP);
+ this.greenMail.start();
+ this.greenMail.setUser("noreply@commafeed.com", "user", "pass");
+ }
+
+ @AfterEach
+ void cleanup() {
+ this.greenMail.stop();
}
@Test
@@ -34,7 +42,7 @@ class UserIT extends BaseIT {
getClient().target(getApiBaseUrl() + "user/passwordReset").request().post(Entity.json(req), Void.TYPE);
- MimeMessage message = GREEN_MAIL.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getReceivedMessages()[0];
Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject());
Assertions.assertTrue(message.getContent().toString().startsWith("You asked for password recovery for account 'admin'"));
Assertions.assertEquals("CommaFeed ", message.getFrom()[0].toString());
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java
index 0a903fb5..9e6229c6 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/CustomCodeIT.java
@@ -1,5 +1,9 @@
package com.commafeed.integration.servlet;
+import java.net.HttpCookie;
+import java.util.List;
+import java.util.stream.Collectors;
+
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.junit.jupiter.api.Assertions;
@@ -8,10 +12,12 @@ import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.Settings;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
+@QuarkusTest
class CustomCodeIT extends BaseIT {
@Override
@@ -30,16 +36,16 @@ class CustomCodeIT extends BaseIT {
getClient().target(getApiBaseUrl() + "user/settings").request().post(Entity.json(settings), Void.TYPE);
// check custom code servlets
- String cookie = login();
+ List cookies = login();
try (Response response = getClient().target(getBaseUrl() + "custom_js.js")
.request()
- .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie)
+ .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.get()) {
Assertions.assertEquals("custom-js", response.readEntity(String.class));
}
try (Response response = getClient().target(getBaseUrl() + "custom_css.css")
.request()
- .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie)
+ .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.get()) {
Assertions.assertEquals("custom-css", response.readEntity(String.class));
}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java
index 10e97246..b12aedb1 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/LogoutIT.java
@@ -1,32 +1,34 @@
package com.commafeed.integration.servlet;
-import org.eclipse.jetty.http.HttpStatus;
+import java.net.HttpCookie;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.hc.core5.http.HttpStatus;
import org.glassfish.jersey.client.ClientProperties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
-import com.commafeed.frontend.model.UserModel;
import com.commafeed.integration.BaseIT;
-import jakarta.ws.rs.NotAuthorizedException;
-import jakarta.ws.rs.client.Invocation.Builder;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
+@QuarkusTest
class LogoutIT extends BaseIT {
@Test
void test() {
- String cookie = login();
+ List cookies = login();
try (Response response = getClient().target(getBaseUrl() + "logout")
.request()
- .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie)
+ .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE)
.get()) {
- Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus());
+ Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus());
+ List setCookieHeaders = response.getStringHeaders().get(HttpHeaders.SET_COOKIE);
+ Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0));
}
-
- Builder req = getClient().target(getApiBaseUrl() + "user/profile").request().header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie);
- Assertions.assertThrows(NotAuthorizedException.class, () -> req.get(UserModel.class));
}
}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java
index cb6ada00..60331b75 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/NextUnreadIT.java
@@ -1,6 +1,10 @@
package com.commafeed.integration.servlet;
-import org.eclipse.jetty.http.HttpStatus;
+import java.net.HttpCookie;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.hc.core5.http.HttpStatus;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
@@ -9,9 +13,11 @@ import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
+@QuarkusTest
class NextUnreadIT extends BaseIT {
@Override
@@ -23,13 +29,13 @@ class NextUnreadIT extends BaseIT {
void test() {
subscribeAndWaitForEntries(getFeedUrl());
- String cookie = login();
+ List cookies = login();
Response response = getClient().target(getBaseUrl() + "next")
.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE)
.request()
- .header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie)
+ .header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.get();
- Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus());
+ Assertions.assertEquals(HttpStatus.SC_TEMPORARY_REDIRECT, response.getStatus());
Assertions.assertEquals("https://hostname.local/commafeed/2", response.getHeaderString(HttpHeaders.LOCATION));
}
diff --git a/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java b/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java
index 142b0c27..f16a5fa9 100644
--- a/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java
+++ b/commafeed-server/src/test/java/com/commafeed/integration/servlet/RobotsTxtIT.java
@@ -5,8 +5,10 @@ import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
+import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.core.Response;
+@QuarkusTest
class RobotsTxtIT extends BaseIT {
@Test
void test() {
diff --git a/commafeed-server/src/test/resources/config.test.yml b/commafeed-server/src/test/resources/config.test.yml
deleted file mode 100644
index 209f3efc..00000000
--- a/commafeed-server/src/test/resources/config.test.yml
+++ /dev/null
@@ -1,138 +0,0 @@
-# CommaFeed settings
-# ------------------
-app:
- # url used to access commafeed
- publicUrl: http://localhost:8082/
-
- # whether to expose a robots.txt file that disallows web crawlers and search engine indexers
- hideFromWebCrawlers: true
-
- # whether to allow user registrations
- allowRegistrations: true
-
- # whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
- strictPasswordPolicy: true
-
- # create a demo account the first time the app starts
- createDemoAccount: true
-
- # put your google analytics tracking code here
- googleAnalyticsTrackingCode:
-
- # put your google server key (used for youtube favicon fetching)
- googleAuthKey:
-
- # number of http threads
- backgroundThreads: 3
-
- # number of database updating threads
- databaseUpdateThreads: 1
-
- # rows to delete per query while cleaning up old entries
- databaseCleanupBatchSize: 100
-
- # settings for sending emails (password recovery)
- smtpHost: localhost
- smtpPort: 3025
- smtpTls: false
- smtpUserName: user
- smtpPassword: pass
- smtpFromAddress: noreply@commafeed.com
-
- # Graphite Metric settings
- # Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
- graphiteEnabled: false
- graphitePrefix: "test.commafeed"
- graphiteHost: "localhost"
- graphitePort: 2003
- graphiteInterval: 60
-
- # whether this commafeed instance has a lot of feeds to refresh
- # leave this to false in almost all cases
- heavyLoad: false
-
- # minimum amount of time commafeed will wait before refreshing the same feed
- refreshIntervalMinutes: 5
-
- # if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
- # useful if commafeed is usually accessed through a restricting proxy
- imageProxyEnabled: false
-
- # database query timeout (in milliseconds), 0 to disable
- queryTimeout: 0
-
- # time to keep unread statuses (in days), 0 to disable
- keepStatusDays: 0
-
- # entries to keep per feed, old entries will be deleted, 0 to disable
- maxFeedCapacity: 500
-
- # entries older than this will be deleted, 0 to disable
- maxEntriesAgeDays: 0
-
- # limit the number of feeds a user can subscribe to, 0 to disable
- maxFeedsPerUser: 0
-
- # don't parse feeds that are too large to prevent memory issues
- maxFeedResponseSize: 5M
-
- # cache service to use, possible values are 'noop' and 'redis'
- cache: noop
-
- # announcement string displayed on the main page
- announcement:
-
- # user-agent string that will be used by the http client, leave empty for the default one
- userAgent:
-
- # enable websocket connection so the server can notify the web client that there are new entries for your feeds
- websocketEnabled: true
-
- # interval at which the client will send a ping message on the websocket to keep the connection alive
- websocketPingInterval: 15m
-
- # if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
- treeReloadInterval: 10s
-
-# Database connection
-# -------------------
-# for MariaDB
-# driverClass is org.mariadb.jdbc.Driver
-# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
-#
-# for MySQL
-# driverClass is com.mysql.cj.jdbc.Driver
-# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
-#
-# for PostgreSQL
-# driverClass is org.postgresql.Driver
-# url is jdbc:postgresql://localhost:5432/commafeed
-
-database:
- driverClass: org.h2.Driver
- url: jdbc:h2:mem:commafeed
- user: sa
- password: sa
- properties:
- charSet: UTF-8
- validationQuery: "/* CommaFeed Health Check */ SELECT 1"
- minSize: 1
- maxSize: 5
- initialSize: 1
-
-server:
- applicationConnectors:
- - type: http
- port: 8083
- adminConnectors: [ ]
-
-logging:
- level: INFO
- loggers:
- com.commafeed: DEBUG
- liquibase: INFO
- org.hibernate.SQL: INFO # or ALL for sql debugging
- org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
- appenders:
- - type: console
-
\ No newline at end of file
diff --git a/commafeed-server/src/test/resources/docker-images.properties b/commafeed-server/src/test/resources/docker-images.properties
deleted file mode 100644
index b40aa81e..00000000
--- a/commafeed-server/src/test/resources/docker-images.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-postgresql=postgres:${postgresql.image.version}
-mysql=mysql:${mysql.image.version}
-mariadb=mariadb:${mariadb.image.version}
-redis=redis:${redis.image.version}
\ No newline at end of file
diff --git a/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db b/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db
deleted file mode 100644
index b11490bc..00000000
Binary files a/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db and /dev/null differ
diff --git a/commafeed-server/src/test/resources/logback-test.xml b/commafeed-server/src/test/resources/logback-test.xml
deleted file mode 100644
index a452f81a..00000000
--- a/commafeed-server/src/test/resources/logback-test.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- %-5p %d{ISO8601} [%thread] [%c{0}:%L] %m %rEx%n
-
-
-
-
-
-
-
\ No newline at end of file