diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index f3080ee..7101036 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -16,12 +16,12 @@ jobs:
steps:
- uses: actions/checkout@v2
- - uses: browser-actions/setup-firefox@v1
+ - uses: browser-actions/setup-edge@v1
- - name: Set up JDK 19
+ - name: Set up JDK 21
uses: actions/setup-java@v1
with:
- java-version: 19
+ java-version: 21
- name: Build with Maven
run: mvn -B verify --file pom.xml
diff --git a/README.md b/README.md
index a51bcd5..052afc1 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
#### Variations
- Simpler version without KeyCloak and multi-modules is on separate project https://github.com/gtiwari333/spring-boot-blog-app
+- Microservice example that uses Spring Cloud features(discovery, gateway, config server etc) is on separate project https://github.com/gtiwari333/spring-boot-microservice-example-java
### App Architecture:
@@ -62,7 +63,7 @@ Misc:
- Nested comment
- Cache implemented
- Zipkin tracing
-
+- Websocket implemented to show article/comment review status/notifications..
Future: do more stuff
- CQRS with event store/streaming
@@ -83,7 +84,6 @@ Future: do more stuff
- nested comment query/performance fix
- Signup UI
- vendor neutral security with OIDC
-- realtime approval UI
- JfrUnit ( WIP )
-
### Requirements
@@ -92,7 +92,9 @@ Future: do more stuff
- http://ganeshtiwaridotcomdotnp.blogspot.com/2016/03/configuring-lombok-on-intellij.html
- For eclipse, download the lombok jar, run it, and point to eclipse installation
- Maven
-- Docker
+- Docker
+ - Make sure docker is started and running
+ - Run `$ sudo chmod 666 /var/run/docker.sock` if you get error like this "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? (Details: [13] Permission denied)"
#### How to Run
@@ -108,7 +110,7 @@ It contains following applications:
Option 1 - run with manually started ActiveMQ and MySQL servers
- Run ```mvn clean install``` at root
-- Run ```docker-compose -f _config/docker-compose.yml up``` at root to start docker containers
+- Run ```docker-compose -f config/docker-compose.yml up``` at root to start docker containers
- Go to main-app folder and run ```mvn``` to start the application
Option 2 - automatically start ActiveMQ and MySQL using TestContainer while application is starting
@@ -120,19 +122,21 @@ Option 3 - run from IDE
- Update run configuration to run maven goal `wro4j:run` Before Launch. It should be after 'Build'
-## Run Tests
+## Run Tests (use ./mvnw instead of mvn if you want to use maven wrapper)
+
+## It uses TestContainers, which requires Docker to be installed locally.
##### Running full tests
-`./mvnw clean verify`
+`mvn clean verify`
##### Running unit tests only (it uses maven surefire plugin)
-`./mvnw compiler:testCompile resources:testResources surefire:test`
+`mvn compiler:testCompile resources:testResources surefire:test`
##### Running integration tests only (it uses maven-failsafe-plugin)
-`./mvnw compiler:testCompile resources:testResources failsafe:integration-test`
+`mvn compiler:testCompile resources:testResources failsafe:integration-test`
## Code Quality
@@ -146,8 +150,8 @@ Run sonarqube server using docker
`docker run -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest`
Perform scan:
-`./mvnw sonar:sonar`
-./mvnw sonar:sonar -Dsonar.login=admin -Dsonar.password=admin
+`mvn sonar:sonar`
+mvn sonar:sonar -Dsonar.login=admin -Dsonar.password=admin
View Reports in SonarQube web ui:
@@ -160,13 +164,12 @@ View Reports in SonarQube web ui:
### Dependency vulnerability scan
-Owasp dependency check plugin is configured. Run `./mvnw dependency-check:check` to run scan and
+Owasp dependency check plugin is configured. Run `mvn dependency-check:check` to run scan and
open `dependency-check-report.html` from target to see the report.
-## Run Tests Faster using Maven Daemon + parallel run
-
-`mvnd test -Dparallel=all -DperCoreThreadCount=false -DthreadCount=4 -o`
+## Run Tests Faster by using parallel maven build
+`mvn -T 5 clean package`
Once the application starts, open `http://localhost:8081` on your browser. The default username/passwords are listed on : gt.app.Application.initData, which are:
diff --git a/_config/docker-compose.yml b/config/docker-compose.yml
similarity index 66%
rename from _config/docker-compose.yml
rename to config/docker-compose.yml
index d58796c..510c852 100644
--- a/_config/docker-compose.yml
+++ b/config/docker-compose.yml
@@ -1,18 +1,20 @@
version: '3'
services:
activemq_artemis:
- image: 'jhatdv/activemq-artemis:2.19.1-alpine'
+ # its not supported in M1 Mac, workaround is to enable Rosetta in Docker
+ # Docker settings → Features in development → check ☑ Use Rosetta for x86/amd64 emulation on Apple Silicon, and then restart Docker.
+ image: 'apache/activemq-artemis:2.31.2-alpine'
container_name: activemqArtemis
environment:
- - ARTEMIS_USERNAME=admin
+ - ARTEMIS_USER=admin
- ARTEMIS_PASSWORD=admin
ports:
- - 8161:8161 # use this to login
+ - 8161:8161 # use this to access from browser
- 61616:61616
networks:
- seedappnet
mysql:
- image: 'mysql:8.0.30'
+ image: 'mysql:8.0.35'
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=seedapp
@@ -22,10 +24,11 @@ services:
networks:
- seedappnet
emailhog:
- image: 'mailhog/mailhog'
+ image: 'richarvey/mailhog'
container_name: mailhog
ports:
- 1025:1025
+ - 8025:8025 # use this to access from browser
networks:
- seedappnet
zipkin:
diff --git a/content-checker/content-checker-service/pom.xml b/content-checker/content-checker-service/pom.xml
index 956df02..fd25aaa 100644
--- a/content-checker/content-checker-service/pom.xml
+++ b/content-checker/content-checker-service/pom.xml
@@ -90,6 +90,14 @@
true
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc-openapi-ui.version}
+ true
+
+
dev
dev
diff --git a/email/email-service/pom.xml b/email/email-service/pom.xml
index 77a913f..ae0eea1 100755
--- a/email/email-service/pom.xml
+++ b/email/email-service/pom.xml
@@ -142,11 +142,6 @@
true
-
- org.springframework.boot
- spring-boot-devtools
- true
-
org.springdoc
springdoc-openapi-starter-webmvc-ui
diff --git a/email/email-service/src/test/java/gt/mail/frwk/TestContainerConfig.java b/email/email-service/src/test/java/gt/mail/frwk/TestContainerConfig.java
index 9345d29..052b178 100644
--- a/email/email-service/src/test/java/gt/mail/frwk/TestContainerConfig.java
+++ b/email/email-service/src/test/java/gt/mail/frwk/TestContainerConfig.java
@@ -9,7 +9,7 @@
public class TestContainerConfig {
static {
- var mailHog = new GenericContainer<>("mailhog/mailhog");
+ var mailHog = new GenericContainer<>("richarvey/mailhog");
mailHog.withExposedPorts(1025);
mailHog.start();
diff --git a/main-app/main-orm/lombok.config b/main-app/main-orm/lombok.config
new file mode 100644
index 0000000..df71bb6
--- /dev/null
+++ b/main-app/main-orm/lombok.config
@@ -0,0 +1,2 @@
+config.stopBubbling = true
+lombok.addLombokGeneratedAnnotation = true
diff --git a/main-app/main-orm/pom.xml b/main-app/main-orm/pom.xml
index 896019b..204c7f4 100644
--- a/main-app/main-orm/pom.xml
+++ b/main-app/main-orm/pom.xml
@@ -102,11 +102,6 @@
-
- org.liquibase.ext
- liquibase-hibernate5
- ${liquibase.version}
-
org.springframework
spring-beans
diff --git a/main-app/main-webapp/lombok.config b/main-app/main-webapp/lombok.config
new file mode 100644
index 0000000..df71bb6
--- /dev/null
+++ b/main-app/main-webapp/lombok.config
@@ -0,0 +1,2 @@
+config.stopBubbling = true
+lombok.addLombokGeneratedAnnotation = true
diff --git a/main-app/main-webapp/pom.xml b/main-app/main-webapp/pom.xml
index 7c77e8a..05084a5 100644
--- a/main-app/main-webapp/pom.xml
+++ b/main-app/main-webapp/pom.xml
@@ -114,6 +114,10 @@
org.springframework.boot
spring-boot-starter-jooq
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
com.querydsl
querydsl-jpa
@@ -163,6 +167,10 @@
springdoc-openapi-starter-webmvc-ui
+
+ org.webjars
+ webjars-locator-core
+
org.webjars
jquery
@@ -171,7 +179,10 @@
org.webjars
bootstrap
-
+
+ org.webjars.bower
+ jquery-toast-plugin
+
com.google.guava
guava
@@ -472,6 +483,14 @@
+
+ org.pitest
+ pitest-maven
+
+ gt.app.modules.*
+ gt.app.modules.*
+
+
diff --git a/main-app/main-webapp/src/main/java/gt/app/MainApplication.java b/main-app/main-webapp/src/main/java/gt/app/MainApplication.java
index f97f07e..3260a01 100644
--- a/main-app/main-webapp/src/main/java/gt/app/MainApplication.java
+++ b/main-app/main-webapp/src/main/java/gt/app/MainApplication.java
@@ -9,6 +9,7 @@
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableScheduling;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -18,6 +19,7 @@
@Slf4j
@EnableConfigurationProperties(AppProperties.class)
@EnableCaching
+@EnableScheduling
public class MainApplication {
public static void main(String[] args) throws UnknownHostException {
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/AppHibernatePropertiesCustomizer.java b/main-app/main-webapp/src/main/java/gt/app/config/AppHibernatePropertiesCustomizer.java
index 94d38b6..bdb756d 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/AppHibernatePropertiesCustomizer.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/AppHibernatePropertiesCustomizer.java
@@ -9,7 +9,7 @@
//@Component
@RequiredArgsConstructor
//@Profile("!test")
-public class AppHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer {
+class AppHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer {
private final HibernateStatInterceptor statInterceptor;
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java
index c65264b..3c46589 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java
@@ -13,7 +13,7 @@
@Profile("withTestContainer")
@Configuration
@Slf4j
-public class DockerContainerConfig {
+class DockerContainerConfig {
/*
@@ -31,17 +31,17 @@ public class DockerContainerConfig {
String userPwd = "admin";//use same for all
- var mysql = new MySQLContainer<>("mysql:8.0.30").withDatabaseName("seedapp").withUsername(userPwd).withPassword(userPwd);
+ var mysql = new MySQLContainer<>("mysql:8.0.35").withDatabaseName("seedapp").withUsername(userPwd).withPassword(userPwd);
mysql.start();
- var activeMQ = new GenericContainer<>("jhatdv/activemq-artemis:2.19.1-alpine");
- activeMQ.setEnv(List.of("ARTEMIS_USERNAME=admin", "ARTEMIS_PASSWORD=admin"));
+ var activeMQ = new GenericContainer<>("apache/activemq-artemis:2.31.2-alpine");
+ activeMQ.setEnv(List.of("ARTEMIS_USER=admin", "ARTEMIS_PASSWORD=admin"));
activeMQ.withExposedPorts(61616);
activeMQ.start(); //using default ports
setProperty("ACTIVEMQ_ARTEMIS_HOST", activeMQ.getHost());
setProperty("ACTIVEMQ_ARTEMIS_PORT", Integer.toString(activeMQ.getMappedPort(61616)));
- setProperty("ACTIVEMQ_ARTEMIS_USERNAME", userPwd);
+ setProperty("ACTIVEMQ_ARTEMIS_USER", userPwd);
setProperty("ACTIVEMQ_ARTEMIS_PASSWORD", userPwd);
setProperty("MYSQL_HOST", mysql.getHost());
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/FeignConfiguration.java b/main-app/main-webapp/src/main/java/gt/app/config/FeignConfiguration.java
index 9d744bb..c47b4db 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/FeignConfiguration.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/FeignConfiguration.java
@@ -9,7 +9,7 @@
@Configuration
@EnableFeignClients(basePackages = "gt.app.api")
@Import(FeignClientsConfiguration.class)
-public class FeignConfiguration {
+class FeignConfiguration {
/**
* Set the Feign specific log level to log client REST requests.
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/JMSConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/JMSConfig.java
index e726e66..a7a43b7 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/JMSConfig.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/JMSConfig.java
@@ -13,7 +13,7 @@
@Configuration
@EnableJms
-public class JMSConfig {
+class JMSConfig {
@Bean
public JmsListenerContainerFactory> myFactory(ConnectionFactory connectionFactory,
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/JpaConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/JpaConfig.java
index 7a381a9..281e0dd 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/JpaConfig.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/JpaConfig.java
@@ -9,5 +9,5 @@
@EnableJpaAuditing //now @CreatedBy, @LastModifiedBy works
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "gt.app.modules")
-public class JpaConfig {
+class JpaConfig {
}
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/WebMvcConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/WebMvcConfig.java
index d6d69c9..05b4c9a 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/WebMvcConfig.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/WebMvcConfig.java
@@ -21,7 +21,7 @@
@Configuration
@RequiredArgsConstructor
-public class WebMvcConfig implements WebMvcConfigurer {
+class WebMvcConfig implements WebMvcConfigurer {
private final WebProperties webProperties;
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/WebSocketConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/WebSocketConfig.java
new file mode 100644
index 0000000..85cfe11
--- /dev/null
+++ b/main-app/main-webapp/src/main/java/gt/app/config/WebSocketConfig.java
@@ -0,0 +1,25 @@
+package gt.app.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+@Configuration
+@EnableWebSocketMessageBroker
+class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+ @Override
+ public void configureMessageBroker(MessageBrokerRegistry config) {
+ config.enableSimpleBroker("/topic");
+ config.setApplicationDestinationPrefixes("/app");
+ config.setUserDestinationPrefix("/user"); //default is /user
+ }
+
+ @Override
+ public void registerStompEndpoints(StompEndpointRegistry registry) {
+ registry.addEndpoint("/app-websockets-main-endpoint");
+ }
+
+}
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/logging/HibernateStatInterceptor.java b/main-app/main-webapp/src/main/java/gt/app/config/logging/HibernateStatInterceptor.java
index bf88238..70ac731 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/logging/HibernateStatInterceptor.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/logging/HibernateStatInterceptor.java
@@ -4,9 +4,10 @@
import org.hibernate.Interceptor;
import java.io.Serial;
+import java.io.Serializable;
@Slf4j
-public class HibernateStatInterceptor implements Interceptor {
+public class HibernateStatInterceptor implements Interceptor, Serializable {
@Serial
private static final long serialVersionUID = -7875557911815131906L;
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/metrics/RequestStatisticsConfiguration.java b/main-app/main-webapp/src/main/java/gt/app/config/metrics/RequestStatisticsConfiguration.java
index 6d83c6a..50f6c3d 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/metrics/RequestStatisticsConfiguration.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/metrics/RequestStatisticsConfiguration.java
@@ -10,7 +10,7 @@
@Configuration
@Profile("!test")
-public class RequestStatisticsConfiguration implements WebMvcConfigurer {
+class RequestStatisticsConfiguration implements WebMvcConfigurer {
@Bean
public HibernateStatInterceptor hibernateInterceptor() {
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/MethodSecurityConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/security/MethodSecurityConfig.java
index 545740a..5ee9687 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/security/MethodSecurityConfig.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/security/MethodSecurityConfig.java
@@ -10,7 +10,7 @@
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
-@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
+@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
class MethodSecurityConfig {
private final AppPermissionEvaluatorService permissionEvaluator;
diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java
index 870ed61..453f3bf 100644
--- a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java
+++ b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java
@@ -8,6 +8,8 @@
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@@ -16,7 +18,7 @@
@EnableMethodSecurity(securedEnabled = true)
@Configuration
@RequiredArgsConstructor
-public class SecurityConfig {
+class SecurityConfig {
private static final String[] AUTH_WHITELIST = {
"/swagger-resources/**",
@@ -38,25 +40,22 @@ public class SecurityConfig {
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
- .headers().frameOptions().sameOrigin()
- .and()
- .authorizeHttpRequests()
+ .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
+ .authorizeHttpRequests(ah -> ah
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers(AUTH_WHITELIST).permitAll()
.requestMatchers("/admin/**").hasAuthority(Constants.ROLE_ADMIN)
.requestMatchers("/user/**").hasAuthority(Constants.ROLE_USER)
.requestMatchers("/api/**").authenticated()//individual api will be secured differently
- .anyRequest().authenticated() //this one will catch the rest patterns
- .and()
- .csrf().disable()
- .formLogin()
+ .anyRequest().authenticated()) //this one will catch the rest patterns
+ .csrf(AbstractHttpConfigurer::disable)
+ .formLogin(f -> f
.loginProcessingUrl("/auth/login")
- .permitAll()
- .and()
- .logout()
+ .permitAll())
+ .logout(l -> l
.logoutUrl("/auth/logout")
.logoutSuccessUrl("/?logout")
- .permitAll();
+ .permitAll());
return http.build();
}
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java
index cab1965..dc615bd 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java
@@ -28,7 +28,7 @@ public interface ArticleRepository extends AbstractRepository, ArticleR
@EntityGraph(attributePaths = {"createdByUser", "comments", "comments.createdByUser", "attachedFiles"})
Optional findOneWithAllByIdAndStatus(Long id, ArticleStatus status, Sort sort);
- @EntityGraph(attributePaths = {"createdByUser"})
+ @EntityGraph(attributePaths = {"createdByUser", "lastModifiedByUser" })
Optional findOneWithUserById(Long id);
@EntityGraph(attributePaths = {"createdByUser", "attachedFiles"})
@@ -37,7 +37,8 @@ public interface ArticleRepository extends AbstractRepository, ArticleR
@Query("select n.createdByUser.id from Article n where n.id=:id ")
Long findCreatedByUserIdById(@Param("id") Long id);
- Optional findByIdAndStatus(Long id, ArticleStatus flagged);
+ @EntityGraph(attributePaths = {"createdByUser", "lastModifiedByUser"})
+ Optional findWithModifiedUserByIdAndStatus(Long id, ArticleStatus flagged);
@Override
@Caching(
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java
index 8f3d7be..f70efa5 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java
@@ -5,6 +5,7 @@
import gt.app.domain.ReceivedFile;
import gt.app.modules.file.FileService;
import gt.app.modules.review.ContentCheckService;
+import gt.app.modules.common.WebsocketHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -33,6 +34,7 @@ public class ArticleService {
private final JmsTemplate jmsTemplate;
private final CommentRepository commentRepo;
private final ContentCheckService contentCheckService;
+ private final WebsocketHandler websocketHandler;
public Article createArticle(ArticleCreateDto dto) {
@@ -139,11 +141,14 @@ public Long findCreatedByUserIdById(Long articleId) {
return articleRepository.findCreatedByUserIdById(articleId);
}
+ @Transactional
public Optional handleReview(ArticleReviewResultDto dto) {
- return articleRepository.findByIdAndStatus(dto.getId(), ArticleStatus.FLAGGED_FOR_MANUAL_REVIEW)
+ return articleRepository.findWithModifiedUserByIdAndStatus(dto.getId(), ArticleStatus.FLAGGED_FOR_MANUAL_REVIEW)
.map(n -> {
n.setStatus(dto.getVerdict());
- return articleRepository.save(n);
+ articleRepository.save(n);
+ websocketHandler.sendToUser(n.getLastModifiedByUser().getUsername(), "Your article with title " + n.getTitle() + " has been " + (dto.getVerdict() == ArticleStatus.PUBLISHED ? "approved from manual review." : "rejected from manual review."));
+ return n;
});
}
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentRepository.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentRepository.java
index a561c98..76a2a07 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentRepository.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentRepository.java
@@ -5,9 +5,11 @@
import jakarta.transaction.Transactional;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
+import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Modifying;
import java.util.List;
+import java.util.Optional;
public interface CommentRepository extends AbstractRepository, CommentRepositoryCustom {
@@ -15,6 +17,9 @@ public interface CommentRepository extends AbstractRepository, CommentR
boolean existsByIdAndArticleId(Long id, Long articleId);
+ @EntityGraph(attributePaths = {"createdByUser"})
+ Optional findWithUserById(Long id);
+
@Transactional
@Modifying
@Caching(
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentService.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentService.java
index c38652d..f94333e 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentService.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/CommentService.java
@@ -7,7 +7,6 @@
import org.springframework.stereotype.Service;
import java.util.List;
-import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@@ -35,7 +34,7 @@ public void save(NewCommentDto c) {
public List readComments(Long articleId) {
- return commentRepository.findAllByArticleId(articleId).stream().map(ArticleMapper.INSTANCE::map).collect(Collectors.toList());
+ return commentRepository.findAllByArticleId(articleId).stream().map(ArticleMapper.INSTANCE::map).toList();
}
}
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/common/WebsocketHandler.java b/main-app/main-webapp/src/main/java/gt/app/modules/common/WebsocketHandler.java
new file mode 100644
index 0000000..08178c7
--- /dev/null
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/common/WebsocketHandler.java
@@ -0,0 +1,25 @@
+package gt.app.modules.common;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.messaging.handler.annotation.SendTo;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.messaging.simp.annotation.SendToUser;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class WebsocketHandler {
+
+ final SimpMessagingTemplate messagingTemplate;
+
+ @SendToUser
+ public void sendToUser(String userName, String message) {
+ messagingTemplate.convertAndSendToUser(userName, "/topic/review-results", message);
+ }
+
+ @SendTo
+ public void sendToAll(String message) {
+ messagingTemplate.convertAndSend("/topic/global-messages", message);
+ }
+
+}
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/jobs/ServerTimeSenderTask.java b/main-app/main-webapp/src/main/java/gt/app/modules/jobs/ServerTimeSenderTask.java
new file mode 100644
index 0000000..8984475
--- /dev/null
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/jobs/ServerTimeSenderTask.java
@@ -0,0 +1,27 @@
+package gt.app.modules.jobs;
+
+import gt.app.modules.common.WebsocketHandler;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Profile;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.TimeZone;
+
+@Component
+@Slf4j
+@RequiredArgsConstructor
+@Profile("!test")
+public class ServerTimeSenderTask {
+
+ final WebsocketHandler websocketHandler;
+ static final DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
+
+ @Scheduled(fixedRate = 30 * 1000L)
+ void sendCurrentTimeToAllUsers() {
+ websocketHandler.sendToAll("Current Server Time is " + LocalDateTime.now().format(DT_FORMAT) + " (" + TimeZone.getDefault().getID() + ")");
+ }
+}
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/review/ArticleReviewResponseService.java b/main-app/main-webapp/src/main/java/gt/app/modules/review/ArticleReviewResponseService.java
index 7f2a301..42c2318 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/review/ArticleReviewResponseService.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/review/ArticleReviewResponseService.java
@@ -4,9 +4,9 @@
import gt.app.api.EmailClient;
import gt.app.config.AppProperties;
import gt.app.domain.Article;
-import gt.app.domain.ArticleStatus;
import gt.app.modules.article.ArticleMapper;
import gt.app.modules.article.ArticleRepository;
+import gt.app.modules.common.WebsocketHandler;
import gt.contentchecker.ContentCheckOutcome;
import gt.contentchecker.Response;
import lombok.RequiredArgsConstructor;
@@ -15,6 +15,9 @@
import java.util.List;
+import static gt.app.domain.ArticleStatus.*;
+import static gt.contentchecker.ContentCheckOutcome.PASSED;
+
@RequiredArgsConstructor
@Service
class ArticleReviewResponseService {
@@ -22,13 +25,13 @@ class ArticleReviewResponseService {
private final JmsTemplate jmsTemplate;
private final EmailClient emailClient;
private final AppProperties appProperties;
+ private final WebsocketHandler websocketHandler;
void handle(Response resp) {
Article a = articleRepository.findOneWithUserById(Long.valueOf(resp.getEntityId())).orElseThrow();
switch (resp.getContentCheckOutcome()) {
- case PASSED -> a.setStatus(ArticleStatus.PUBLISHED);
- case MANUAL_REVIEW_NEEDED -> a.setStatus(ArticleStatus.FLAGGED_FOR_MANUAL_REVIEW);
- case FAILED -> a.setStatus(ArticleStatus.BLOCKED);
+ case PASSED -> a.setStatus(PUBLISHED);
+ case MANUAL_REVIEW_NEEDED, FAILED -> a.setStatus(FLAGGED_FOR_MANUAL_REVIEW);
default -> throw new UnsupportedOperationException();
}
@@ -38,11 +41,15 @@ void handle(Response resp) {
jmsTemplate.convertAndSend("article-published", ArticleMapper.INSTANCE.INSTANCE.mapForPublishedEvent(a));
}
- sendNotificationToAuthor(a, resp.getContentCheckOutcome());
+ websocketHandler.sendToUser(a.getLastModifiedByUser().getUsername(), "Your article " + a.getTitle() + " has been " + (resp.getContentCheckOutcome() == PASSED ? "approved." : "queued for manual review."));
+ if (resp.getContentCheckOutcome() != PASSED) {
+ websocketHandler.sendToUser("system", "A new article " + a.getTitle() + " by " + a.getLastModifiedByUser().getUsername() + " is queued for system admin review.");
+ }
+ sendEmailNotificationToAuthor(a, resp.getContentCheckOutcome());
}
- void sendNotificationToAuthor(Article a, ContentCheckOutcome outcome) {
+ void sendEmailNotificationToAuthor(Article a, ContentCheckOutcome outcome) {
var email = EmailDto.of(appProperties.getEmail().getAuthorNotificationsFromEmail(), appProperties.getEmail().getAuthorNotificationsFromEmail(),
List.of(a.getCreatedByUser().getEmail()),
"Article Review Result " + outcome,
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/review/CommentReviewResponseService.java b/main-app/main-webapp/src/main/java/gt/app/modules/review/CommentReviewResponseService.java
index 2ef38ba..fd14d65 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/review/CommentReviewResponseService.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/review/CommentReviewResponseService.java
@@ -1,26 +1,36 @@
package gt.app.modules.review;
import gt.app.domain.Comment;
-import gt.app.domain.CommentStatus;
import gt.app.modules.article.CommentRepository;
+import gt.app.modules.common.WebsocketHandler;
import gt.contentchecker.Response;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
+import static gt.app.domain.CommentStatus.HIDDEN;
+import static gt.app.domain.CommentStatus.SHOWING;
+import static gt.contentchecker.ContentCheckOutcome.PASSED;
+
@Service
@RequiredArgsConstructor
class CommentReviewResponseService {
private final CommentRepository commentRepository;
+ private final WebsocketHandler websocketHandler;
void handle(Response resp) {
- Comment c = commentRepository.findById(Long.valueOf(resp.getEntityId())).orElseThrow();
+ Comment c = commentRepository.findWithUserById(Long.valueOf(resp.getEntityId())).orElseThrow();
switch (resp.getContentCheckOutcome()) {
- case PASSED, MANUAL_REVIEW_NEEDED -> c.setStatus(CommentStatus.SHOWING); //its okay for comment
- case FAILED -> c.setStatus(CommentStatus.HIDDEN);
+ case PASSED -> c.setStatus(SHOWING);
+ case FAILED, MANUAL_REVIEW_NEEDED -> c.setStatus(HIDDEN);
default -> throw new UnsupportedOperationException();
}
+ websocketHandler.sendToUser(c.getLastModifiedByUser().getUsername(), "Your comment " + c.getContent().substring(0, 20) + " has been " + (resp.getContentCheckOutcome() == PASSED ? "approved." : "queued for manual review."));
+ if (resp.getContentCheckOutcome() != PASSED) {
+ websocketHandler.sendToUser("system", "A new comment " + c.getContent().substring(0, 20) + " has is queued for system admin review.");
+ }
+
commentRepository.save(c);
}
}
diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java
index 75f077b..345fe52 100644
--- a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java
+++ b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java
@@ -6,7 +6,6 @@
import gt.app.config.security.AppUserDetails;
import gt.app.domain.AppUser;
import gt.app.domain.LiteUser;
-import gt.app.exception.DuplicateRecordException;
import gt.app.exception.RecordNotFoundException;
import gt.app.modules.user.dto.PasswordUpdateDTO;
import gt.app.modules.user.dto.UserProfileUpdateDTO;
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java
index 5875003..79eace3 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java
@@ -14,7 +14,7 @@
@Controller
@RequiredArgsConstructor
-public class AccountController {
+class AccountController {
final UserService userService;
@GetMapping("/account/user/{id}")
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java
index 56afe9d..e948b21 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java
@@ -23,7 +23,7 @@
@RequestMapping("/article")
@RequiredArgsConstructor
@Slf4j
-public class ArticleController {
+class ArticleController {
final ArticleService articleService;
final CommentService commentService;
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/DownloadController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/DownloadController.java
index 44be69d..7236392 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/DownloadController.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/DownloadController.java
@@ -20,7 +20,7 @@
@Controller
@RequestMapping("/download")
@RequiredArgsConstructor
-public class DownloadController {
+class DownloadController {
final ReceivedFileService receivedFileService;
final FileService fileService;
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java
index 7489bdc..e2d4fcd 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java
@@ -11,7 +11,7 @@
@ControllerAdvice
@Slf4j
-public class ErrorControllerAdvice {
+class ErrorControllerAdvice {
@ExceptionHandler(Throwable.class)
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java
index 3c02a33..c07e76d 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java
@@ -15,7 +15,7 @@
@Controller
@Slf4j
@RequiredArgsConstructor
-public class IndexController {
+class IndexController {
private final ArticleService articleService;
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ReviewController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ReviewController.java
index cc16e23..00c8a9a 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ReviewController.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ReviewController.java
@@ -20,7 +20,7 @@
@RequestMapping("/admin")
@RequiredArgsConstructor
@Slf4j
-public class ReviewController {
+class ReviewController {
final ArticleService articleService;
@@ -42,6 +42,6 @@ public String finishEditArticle(ArticleReviewResultDto reviewResult, RedirectAtt
() -> redirectAttrs.addFlashAttribute("success", "Article with id " + reviewResult.getId() + " is already reviewed or doesn't exists")
);
- return "redirect:/admin/";
+ return "redirect:/admin";
}
}
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java
index 0ac7ff6..b4ee4c2 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java
@@ -20,7 +20,7 @@
@Controller
@RequiredArgsConstructor
-public class UserController {
+class UserController {
private final UserService userService;
private final UserSignupValidator userSignupValidator;
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/rest/HelloResource.java b/main-app/main-webapp/src/main/java/gt/app/web/rest/HelloResource.java
index 93d4030..d0de49b 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/rest/HelloResource.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/rest/HelloResource.java
@@ -12,7 +12,7 @@
@RequiredArgsConstructor
@RequestMapping("/public")
@Slf4j
-public class HelloResource {
+class HelloResource {
@GetMapping("/hello")
public Map sayHello() {
diff --git a/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java b/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java
index 7907a9d..64bf0f8 100644
--- a/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java
+++ b/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java
@@ -13,7 +13,7 @@
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
-public class UserResource {
+class UserResource {
@GetMapping("/account")
public Optional getAccount() {
diff --git a/main-app/main-webapp/src/main/resources/application-dev.yml b/main-app/main-webapp/src/main/resources/application-dev.yml
index 137190b..a493f4c 100644
--- a/main-app/main-webapp/src/main/resources/application-dev.yml
+++ b/main-app/main-webapp/src/main/resources/application-dev.yml
@@ -32,7 +32,7 @@ spring:
jooq:
sql-dialect: MySQL
artemis:
- user: ${ACTIVEMQ_ARTEMIS_USERNAME:admin}
+ user: ${ACTIVEMQ_ARTEMIS_USER:admin}
password: ${ACTIVEMQ_ARTEMIS_PASSWORD:admin}
broker-url: tcp://${ACTIVEMQ_ARTEMIS_HOST:localhost}:${ACTIVEMQ_ARTEMIS_PORT:61616}
liquibase:
@@ -42,3 +42,11 @@ feign-clients:
email-service:
url: http://localhost:8085/ #TODO: use service discovery
+logging.level:
+ org.jooq.tools.LoggerListener: DEBUG
+ org.springframework.security: INFO
+ org.springframework.security.web: INFO
+ org.springframework.cloud: INFO
+# org.hibernate.SQL: debug
+# org.hibernate.type: TRACE
+ 'org.hibernate.engine.internal.StatisticalLoggingSessionEventListener': info
diff --git a/main-app/main-webapp/src/main/resources/application.yml b/main-app/main-webapp/src/main/resources/application.yml
index a7139fd..e75128a 100644
--- a/main-app/main-webapp/src/main/resources/application.yml
+++ b/main-app/main-webapp/src/main/resources/application.yml
@@ -24,18 +24,15 @@ spring:
server:
port: 8081
-logging:
- level:
- org.springframework.security: INFO
- org.springframework.security.web: INFO
- org.springframework.cloud: INFO
- sql: INFO
- web: INFO
- ROOT: WARN
- gt: DEBUG
-# org.hibernate.SQL: debug
-# org.hibernate.type: TRACE
-# 'org.hibernate.engine.internal.StatisticalLoggingSessionEventListener': info
+logging.level:
+ org.springframework.security: INFO
+ org.springframework.security.web: INFO
+ org.springframework.cloud: INFO
+ sql: INFO
+ web: INFO
+ ROOT: WARN
+ gt: DEBUG
+ 'org.springframework.web.socket': TRACE
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
diff --git a/main-app/main-webapp/src/main/resources/static/js/app.js b/main-app/main-webapp/src/main/resources/static/js/app.js
index b0389e5..0e854af 100644
--- a/main-app/main-webapp/src/main/resources/static/js/app.js
+++ b/main-app/main-webapp/src/main/resources/static/js/app.js
@@ -1,5 +1,6 @@
(function () {
+ var stompClient = null;
function displayUserInfo(e) {
console.log(e);
@@ -14,7 +15,6 @@
}
- //common behaviour
jQuery(document).ready(function () {
//init username-link
@@ -25,7 +25,54 @@
userLink.click(function (ev) {
displayUserInfo(ev);
});
+
+ initStompJs();
});
+ function initStompJs() {
+ stompClient = new StompJs.Client({
+ brokerURL: 'ws://localhost:8081/app-websockets-main-endpoint',
+ debug: function (str) {
+ console.log(str);
+ },
+ reconnectDelay: 5000,
+ heartbeatIncoming: 4000,
+ heartbeatOutgoing: 4000,
+ });
+
+ stompClient.onConnect = function (frame) {
+
+ console.log('Connected: ' + frame);
+ stompClient.subscribe('/topic/global-messages', function (msg) {
+ console.log("Global message" + msg);
+ $.toast({
+ text: msg.body,
+ icon: 'info',
+ allowToastClose: true,
+ hideAfter: 5000,
+ position: 'top-right',
+ });
+ });
+
+ //this (/user/* path) is used to send/receive messages meant for specific user
+ stompClient.subscribe('/user/topic/review-results', function (msg) {
+ console.log("User message" + msg);
+ $.toast({
+ text: msg.body,
+ icon: 'info',
+ allowToastClose: true,
+ hideAfter: 5000,
+ position: 'top-right',
+ });
+ });
+ }
+
+ stompClient.onStompError = function (frame) {
+ console.log('Broker reported error: ' + frame.headers['message']);
+ console.log('Additional details: ' + frame.body);
+ };
+
+ stompClient.activate();
+ }
})();
diff --git a/main-app/main-webapp/src/main/resources/templates/_fragments/footer.html b/main-app/main-webapp/src/main/resources/templates/_fragments/footer.html
index b357144..aee28d6 100644
--- a/main-app/main-webapp/src/main/resources/templates/_fragments/footer.html
+++ b/main-app/main-webapp/src/main/resources/templates/_fragments/footer.html
@@ -17,12 +17,14 @@
-
+
+
+
+
-