diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 95bbd28b..465d08c0 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -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") diff --git a/api/prometheus.yaml b/api/prometheus.yaml index f6f7fef4..a2f5f432 100644 --- a/api/prometheus.yaml +++ b/api/prometheus.yaml @@ -5,4 +5,4 @@ scrape_configs: - job_name: 'spring-api' metrics_path: '/actuator/prometheus' static_configs: - - targets: ['boardmate-api:8000'] \ No newline at end of file + - targets: ['boardmate-api:8080'] \ No newline at end of file diff --git a/api/src/main/java/be/naaturel/boardmateapi/common/exceptions/ServiceException.java b/api/src/main/java/be/naaturel/boardmateapi/common/exceptions/ServiceException.java new file mode 100644 index 00000000..a3248dab --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/common/exceptions/ServiceException.java @@ -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); + } +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/StartupLogger.java similarity index 93% rename from api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java rename to api/src/main/java/be/naaturel/boardmateapi/configurations/StartupLogger.java index 32b0ebcb..c495ff91 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/StartupLogger.java @@ -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() { diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/SwaggerConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/SwaggerConfig.java deleted file mode 100644 index cac40f0d..00000000 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/SwaggerConfig.java +++ /dev/null @@ -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 { - -} \ No newline at end of file diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/AppSecurity.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java similarity index 85% rename from api/src/main/java/be/naaturel/boardmateapi/configurations/AppSecurity.java rename to api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java index 0394b034..23ca6582 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/AppSecurity.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java @@ -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; } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/HttpRequests.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/HttpRequests.java similarity index 86% rename from api/src/main/java/be/naaturel/boardmateapi/configurations/HttpRequests.java rename to api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/HttpRequests.java index 54418bbb..0d780ce7 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/HttpRequests.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/HttpRequests.java @@ -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; diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/Interceptor.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/Interceptor.java similarity index 65% rename from api/src/main/java/be/naaturel/boardmateapi/configurations/Interceptor.java rename to api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/Interceptor.java index e13c4864..78dca5d8 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/Interceptor.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/Interceptor.java @@ -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; } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/MongoConfig.java similarity index 59% rename from api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java rename to api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/MongoConfig.java index 4a251ac6..0ffc32ec 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/MongoConfig.java @@ -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; } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/MqttConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/MqttConfig.java new file mode 100644 index 00000000..6a9f39b8 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/MqttConfig.java @@ -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; + } + +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/SwaggerConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/SwaggerConfig.java new file mode 100644 index 00000000..75afbf68 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/SwaggerConfig.java @@ -0,0 +1,8 @@ +package be.naaturel.boardmateapi.configurations.configurations; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + +} \ No newline at end of file diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/AppProperties.java similarity index 85% rename from api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java rename to api/src/main/java/be/naaturel/boardmateapi/configurations/properties/AppProperties.java index 88d113e8..5de097a0 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/AppProperties.java @@ -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; diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/MqttProperies.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/MqttProperies.java new file mode 100644 index 00000000..1fae37b2 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/MqttProperies.java @@ -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; } +} \ No newline at end of file diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/BrokerController.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/BrokerController.java new file mode 100644 index 00000000..c2f49498 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/BrokerController.java @@ -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> 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); + } + } +} + diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java index bea3bbab..3f70e3e7 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java @@ -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> retrieveGames(@PathVariable String id){ - ResponseBody response = ResponseBody.createEmpty(); + ResponseBody 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> CreateParty(@RequestBody GameDto game){ - ResponseBody response = ResponseBody.createEmpty(); + ResponseBody 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> AddMove(@PathVariable String gameId, @RequestBody String move){ + ResponseBody 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); } } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java b/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java index 94cb207c..fc8f9d1f 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java +++ b/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java @@ -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); + } } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java b/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java new file mode 100644 index 00000000..60239963 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java @@ -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); + } + } + +} diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 19fa78c5..d7571f8a 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -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