Integrated MQTT clients

This commit is contained in:
2025-12-12 20:58:32 +01:00
parent f457911f3b
commit b9a87309e4
18 changed files with 291 additions and 68 deletions

View File

@@ -28,26 +28,36 @@ repositories {
extra["snippetsDir"] = file("build/generated-snippets")
dependencies {
implementation("org.springframework.boot:spring-boot-security")
//======================MAIN======================
implementation("org.springframework.boot:spring-boot-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-elasticsearch")
implementation("org.springframework.boot:spring-boot-starter-mongodb")
implementation("org.springframework.boot:spring-boot-starter-opentelemetry")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")
developmentOnly("org.springframework.boot:spring-boot-devtools")
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
//======================ELASTIC SEARCH======================
implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
implementation("org.springframework.boot:spring-boot-starter-elasticsearch")
//======================MONGO DB======================
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-mongodb")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
//======================SWAGGER======================
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")
//======================MQTT======================
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
//======================PROMETHEUS======================
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
//======================OTHER======================
developmentOnly("org.springframework.boot:spring-boot-devtools")
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
testImplementation("org.springframework.boot:spring-boot-restdocs")
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
testImplementation("org.springframework.boot:spring-boot-starter-data-elasticsearch-test")

View File

@@ -5,4 +5,4 @@ scrape_configs:
- job_name: 'spring-api'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['boardmate-api:8000']
- targets: ['boardmate-api:8080']

View File

@@ -0,0 +1,11 @@
package be.naaturel.boardmateapi.common.exceptions;
public class ServiceException extends Exception{
public ServiceException(String message, Exception innerException){
super(message, innerException);
}
public ServiceException(String message){
super(message);
}
}

View File

@@ -4,11 +4,10 @@ import be.naaturel.boardmateapi.common.helpers.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.*;
import java.util.Arrays;
@Configuration
public class Startup {
public class StartupLogger {
@Bean
public String log() {

View File

@@ -1,10 +0,0 @@
package be.naaturel.boardmateapi.configurations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springdoc.core.models.GroupedOpenApi;
@Configuration
public class SwaggerConfig {
}

View File

@@ -1,12 +1,12 @@
package be.naaturel.boardmateapi.configurations;
package be.naaturel.boardmateapi.configurations.configurations;
import be.naaturel.boardmateapi.configurations.properties.AppProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.LogoutConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.cors.CorsConfiguration;
@@ -18,12 +18,12 @@ import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableTransactionManagement
public class AppSecurity {
public class AppSecurityConfig {
private final AppConfigurations conf;
private final AppProperties conf;
@Autowired
public AppSecurity(AppConfigurations appConf) {
public AppSecurityConfig(AppProperties appConf) {
this.conf = appConf;
}

View File

@@ -1,4 +1,4 @@
package be.naaturel.boardmateapi.configurations;
package be.naaturel.boardmateapi.configurations.configurations;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

View File

@@ -1,5 +1,6 @@
package be.naaturel.boardmateapi.configurations;
package be.naaturel.boardmateapi.configurations.configurations;
import be.naaturel.boardmateapi.common.helpers.Logger;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
@@ -8,7 +9,7 @@ public class Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("Received : " + request.getRequestURI());
Logger.displayInfo("Intercepted : " + request.getRequestURI());
return true;
}
}

View File

@@ -1,18 +1,15 @@
package be.naaturel.boardmateapi.configurations;
package be.naaturel.boardmateapi.configurations.configurations;
import be.naaturel.boardmateapi.configurations.properties.AppProperties;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
public class MongoConfig {
private final AppConfigurations conf;
private final AppProperties conf;
public MongoConfig(AppConfigurations appConf) {
public MongoConfig(AppProperties appConf) {
this.conf = appConf;
}

View File

@@ -0,0 +1,54 @@
package be.naaturel.boardmateapi.configurations.configurations;
import be.naaturel.boardmateapi.common.helpers.Logger;
import be.naaturel.boardmateapi.configurations.properties.MqttProperies;
import org.eclipse.paho.client.mqttv3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqttConfig {
private final MqttProperies properties;
@Autowired
public MqttConfig(MqttProperies properties){
this.properties = properties;
}
@Bean("mqttPublisher")
public MqttClient mqttPublisher() throws MqttException {
MqttClient client = new MqttClient(properties.getBrokerUrl(), properties.getClientId());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(properties.getUsername());
options.setPassword(properties.getPassword().toCharArray());
return client;
}
@Bean("mqttSubscriber")
public MqttClient mqttSubscriber() throws MqttException {
String subscriberId = properties.getClientId() + "-sub";
MqttClient client = new MqttClient(properties.getBrokerUrl(), subscriberId);
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
Logger.displayError("Connection lost: " + cause.getMessage());
}
@Override
public void messageArrived(String topic, MqttMessage message) {
Logger.displayInfo("Received message on topic " + topic + ": " + message.toString());
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// Not needed for subscriber
}
});
return client;
}
}

View File

@@ -0,0 +1,8 @@
package be.naaturel.boardmateapi.configurations.configurations;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
}

View File

@@ -1,10 +1,10 @@
package be.naaturel.boardmateapi.configurations;
package be.naaturel.boardmateapi.configurations.properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class AppConfigurations {
public class AppProperties {
@Value("${sec.cors.authorizedHots}")
public String[] authorizedHosts;

View File

@@ -0,0 +1,29 @@
package be.naaturel.boardmateapi.configurations.properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqttProperies {
@Value("${mqtt.broker-url}")
private String brokerUrl;
@Value("${mqtt.client-id}")
private String clientId;
@Value("${mqtt.username}")
private String username;
@Value("${mqtt.password}")
private String password;
@Value("${mqtt.topic}")
private String topic;
public String getBrokerUrl() { return brokerUrl; }
public String getClientId() { return clientId; }
public String getUsername() { return username; }
public String getPassword() { return password; }
public String getTopic() { return topic; }
}

View File

@@ -0,0 +1,42 @@
package be.naaturel.boardmateapi.controllers;
import be.naaturel.boardmateapi.common.exceptions.ServiceException;
import be.naaturel.boardmateapi.services.MqttService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController("/broker")
public class BrokerController {
private final MqttService service;
@Autowired
public BrokerController(MqttService service){
this.service = service;
}
@PostMapping("/publish/{topic}")
public ResponseEntity<ResponseBody<?>> publish(@PathVariable String topic, @RequestBody String message){
ResponseBody<?> body = ResponseBody.createEmpty();
try {
service.subscribe(topic);
service.publish(topic, message);
body.setSuccess(true);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(body);
} catch (ServiceException se){
body.setMessage(se.getMessage());
body.setSuccess(false);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(body);
}
}
}

View File

@@ -1,5 +1,6 @@
package be.naaturel.boardmateapi.controllers;
import be.naaturel.boardmateapi.common.exceptions.ServiceException;
import be.naaturel.boardmateapi.common.models.Game;
import be.naaturel.boardmateapi.controllers.dtos.GameDto;
import be.naaturel.boardmateapi.controllers.mappings.GameMapper;
@@ -33,53 +34,57 @@ public class GameController {
@GetMapping("/games/{id}")
public ResponseEntity<ResponseBody<GameDto>> retrieveGames(@PathVariable String id){
ResponseBody<GameDto> response = ResponseBody.createEmpty();
ResponseBody<GameDto> result = ResponseBody.createEmpty();
try{
Game g = service.retrieveGame(id);
GameDto dto = GameMapper.toDto(g);
response.setData(dto);
response.setSuccess(true);
result.setData(dto);
result.setSuccess(true);
return ResponseEntity
.status(HttpStatus.OK)
.body(response);
.body(result);
} catch (Exception e){
response.setMessage(e.getMessage());
result.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
.body(result);
}
}
@PostMapping("/create")
public ResponseEntity<ResponseBody<String>> CreateParty(@RequestBody GameDto game){
ResponseBody<String> response = ResponseBody.createEmpty();
ResponseBody<String> result = ResponseBody.createEmpty();
try{
Game model = GameMapper.toModel(game);
String result = service.create(model);
response.setData(result);
response.setSuccess(true);
String id = service.create(model);
result.setData(id);
result.setSuccess(true);
return ResponseEntity.
status(HttpStatus.OK)
.body(response);
.body(result);
} catch (Exception e){
response.setMessage(e.getMessage());
result.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
.body(result);
}
}
@PostMapping("/moves/add/{gameId}")
public ResponseEntity<?> AddMove(@PathVariable String gameId, @RequestBody String move){
public ResponseEntity<ResponseBody<String>> AddMove(@PathVariable String gameId, @RequestBody String move){
ResponseBody<String> result = ResponseBody.createEmpty();
try{
service.addMove(gameId, move);
String gamedId = service.addMove(gameId, move);
result.setSuccess(true);
result.setData(gamedId);
return ResponseEntity
.status(HttpStatus.OK)
.build();
} catch (Exception e){
.body(result);
} catch (ServiceException e){
result.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.build();
.body(result);
}
}
}

View File

@@ -1,5 +1,6 @@
package be.naaturel.boardmateapi.services;
import be.naaturel.boardmateapi.common.exceptions.ServiceException;
import be.naaturel.boardmateapi.common.models.Game;
import be.naaturel.boardmateapi.common.models.Move;
import be.naaturel.boardmateapi.repository.GameRepo;
@@ -38,9 +39,14 @@ public class GameService {
return gameDto.getId();
}
public void addMove(@RequestBody String gameId, @RequestBody String move) {
Game g = retrieveGame(gameId);
g.addMove(move);
save(g);
public String addMove(@RequestBody String gameId, @RequestBody String move) throws ServiceException {
try {
Game g = retrieveGame(gameId);
g.addMove(move);
save(g);
return g.getId();
} catch (Exception e) {
throw new ServiceException(e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,60 @@
package be.naaturel.boardmateapi.services;
import be.naaturel.boardmateapi.common.exceptions.ServiceException;
import be.naaturel.boardmateapi.common.helpers.Logger;
import org.eclipse.paho.client.mqttv3.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class MqttService {
private final MqttClient publisher;
private final MqttClient subscriber;
public MqttService(
@Qualifier("mqttPublisher") MqttClient publisher,
@Qualifier("mqttSubscriber") MqttClient subscriber
) {
this.publisher = publisher;
this.subscriber = subscriber;
}
public void publish(String topic, String payload) throws ServiceException {
try {
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(1);
if(!publisher.isConnected()){
publisher.connect();
}
publisher.publish(topic, message);
Logger.displayInfo("Published message: " + payload);
} catch (MqttException e) {
throw new ServiceException("Unable to publish message", e);
} finally {
try{
if (publisher.isConnected()) publisher.disconnect();
} catch (MqttException e){
Logger.displayError("Failed to disconnect MQTT client: " + e.getMessage());
}
}
}
public void subscribe(String topic) throws ServiceException {
try {
MqttConnectOptions options = new MqttConnectOptions();
options.setAutomaticReconnect(true);
options.setCleanSession(true);
if(!subscriber.isConnected()){
subscriber.connect(options);
}
subscriber.subscribe(topic, 1);
Logger.displayInfo("Subscribed to topic: " + topic);
} catch (MqttException e) {
throw new ServiceException("Unable to subscribe", e);
}
}
}

View File

@@ -10,10 +10,21 @@ sec.cors.authorizedHots=*
sec.cors.authorizedMethods=GET,POST,PUT,DELETE,OPTION
sec.cors.authorizedHeader=Authorization,Content-type
#=============MQTT=============
mqtt.broker-url=tcp://test.mosquitto.org:1883
mqtt.client-id=board-mate-client
mqtt.topic=board-mate-test/topic
mqtt.username=yourUsername
mqtt.password=yourPassword
#=============METRICS=============
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.endpoint.prometheus.enabled=true
management.endpoints.web.exposure.include=*
management.prometheus.metrics.export.enabled=true
management.metrics.export.otlp.endpoint=http://prometheus:4318/v1/metrics
#=============DOCUMENTATION=============
springdoc.swagger-ui.path=/docs