diff --git a/.env.example b/.env.example index d300f5d..4c5e64f 100644 --- a/.env.example +++ b/.env.example @@ -1,28 +1,69 @@ -# Configuración APP -APP_NAME=app -PORT=port -TITLE=title -VERSION=version -AUTHOR=author +# Application Configuration +# Basic settings for your application +APP_NAME= # Name of your application +PORT= # Port the application will listen on +TITLE= # Title of the API +DESCRIPTION=<DESCRIPTION> # Description of the API +VERSION=<VERSION> # Version of your API +AUTHOR=<AUTHOR> # Author(s) of the API # MongoDB Credentials -DATA_CONNECTION_METHOD=method # Methods (mongodb+srv, mongodb) -DATA_SOURCE_USERNAME=username -DATA_SOURCE_PASSWORD=password -DATA_SOURCE_DOMAIN=domain -DATA_SOURCE_DB=database -DATA_PARAMS=params +# Settings for connecting to MongoDB +DATA_CONNECTION_METHOD=<CONNECTION_METHOD> # Connection method (mongodb+srv or mongodb) +DATA_SOURCE_USERNAME=<USERNAME> # MongoDB username +DATA_SOURCE_PASSWORD=<PASSWORD> # MongoDB password +DATA_SOURCE_DOMAIN=<DOMAIN> # MongoDB server domain +DATA_SOURCE_DB=<DATABASE> # Database name +DATA_PARAMS=<CONNECTION_PARAMS> # Additional connection parameters + +# Redis Credentials +# Settings for connecting to Redis (used for caching) +CACHE_TYPE=<CACHE_TYPE> # Cache type (redis) +CACHE_HOST=<REDIS_HOST> # Redis host +CACHE_PORT=<REDIS_PORT> # Redis port +CACHE_DB=<REDIS_DB> # Redis database ID (usually 0) +CACHE_USERNAME=<REDIS_USERNAME> # Redis username +CACHE_PASSWORD=<REDIS_PASSWORD> # Redis password +CACHE_TIMEOUT=<CACHE_TIMEOUT> # Timeout for cache operations (in ms) +CACHE_LETTUCE_POOL_MAX_ACTIVE=<MAX_ACTIVE> # Max active connections in Redis pool +CACHE_LETTUCE_POOL_MAX_WAIT=<MAX_WAIT> # Max wait time for Redis connections (in ms) +CACHE_LETTUCE_POOL_MAX_IDLE=<MAX_IDLE> # Max idle connections in Redis pool +CACHE_LETTUCE_POOL_MIN_IDLE=<MIN_IDLE> # Min idle connections in Redis pool + +# Cache Configuration +# Settings for cache behavior +CACHE_TIME_TO_LIVE=<CACHE_TTL> # Time to live for cache items (in ms) +CACHE_NULL_VALUES=<BOOLEAN> # Whether to store null values in cache (true/false) + +# Email Credentials +# Settings for sending emails via SMTP +MAIL_HOST=<SMTP_HOST> # SMTP server (e.g., Gmail) +MAIL_PORT=<SMTP_PORT> # SMTP port (587 for TLS) +MAIL_USERNAME=<SMTP_USERNAME> # SMTP login username +MAIL_PASSWORD=<SMTP_PASSWORD> # SMTP login password +MAIL_PROPERTIES_SMTP_AUTH=<TRUE/FALSE> # Enable SMTP authentication (true/false) +MAIL_PROPERTIES_SMTP_STARTTLS_ENABLE=<TRUE/FALSE> # Enable STARTTLS for secure connection # JWT Credentials -SECURITY_JWT_SECRET_KEY=secret-key -SECURITY_JWT_EXPIRATION=jwt-expiration -SECURITY_PUBLIC_ROUTES=/** +# Settings for JWT (JSON Web Token) authentication +SECURITY_JWT_SECRET_KEY=<JWT_SECRET_KEY> # Secret key for signing JWT tokens +SECURITY_JWT_EXPIRATION=<JWT_EXPIRATION> # JWT expiration time (in ms) +SECURITY_PUBLIC_ROUTES=<PUBLIC_ROUTES> # Public routes that do not require authentication (e.g., /auth/login) + +# Rate Limiting Config +# Settings for API rate limiting +RATE_LIMITING_MAX_REQUESTS=<MAX_REQUESTS> # Max requests per client IP within the defined time window +RATE_LIMITING_TIME_WINDOW=<TIME_WINDOW> # Time window in milliseconds (1 minute) +RATE_LIMITING_PUBLIC_ROUTES=<PUBLIC_ROUTES> # Public routes excluded from rate limiting -# Https Headers -HEADER_CORS_ALLOWED_ORIGINS=* +# HTTPS Headers (CORS) +# Settings for Cross-Origin Resource Sharing (CORS) +HEADER_CORS_ALLOWED_ORIGINS=<ALLOWED_ORIGINS> # Allowed origins for CORS (e.g., http://localhost:3000) -# Config TOMCAT -SERVER_TOMCAT_TIMEOUT=ms +# Tomcat Configuration +# Settings for your Tomcat server +SERVER_TOMCAT_TIMEOUT=<TOMCAT_TIMEOUT> # Timeout for Tomcat server (in ms) -# Logs (INFO, DEBUG, OFF) -DEBUGGER_MODE=mode \ No newline at end of file +# Log Level Configuration +# Define the logging level (e.g., INFO, DEBUG, OFF) +DEBUGGER_MODE=<DEBUG_MODE> # Log level: INFO, DEBUG, or OFF diff --git a/README.md b/README.md index 54d8183..f49c082 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ## Deployment -[![Deployment](https://github.com/SmartPotTech/SmartPot-API/actions/workflows/deployment.yml/badge.svg)](https://github.com/SmartPotTech/SmartPot-API/actions/workflows/deployment.yml) +[![General CI Pipeline](https://github.com/SmartPotTech/SmartPot-API/actions/workflows/ci-pipeline.yml/badge.svg)](https://github.com/SmartPotTech/SmartPot-API/actions/workflows/ci-pipeline.yml) + +[![Checkout Code](https://github.com/SmartPotTech/SmartPot-API/actions/workflows/checkout.yml/badge.svg)](https://github.com/SmartPotTech/SmartPot-API/actions/workflows/checkout.yml) ### 1. Compilación de la Aplicación diff --git a/pom.xml b/pom.xml index e0b6626..128b5d8 100644 --- a/pom.xml +++ b/pom.xml @@ -2,132 +2,189 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <!-- Model version for Maven POM --> <modelVersion>4.0.0</modelVersion> + + <!-- Parent project information --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.1</version> <relativePath /> <!-- lookup parent from repository --> </parent> + + <!-- Basic project information --> <groupId>smarpot.com</groupId> <artifactId>api</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SmartPot-API</name> <description>SmartPot-API</description> <url /> + + <!-- Licenses for the project --> <licenses> <license /> </licenses> + + <!-- Developers involved in the project --> <developers> <developer /> </developers> + + <!-- Source control management (SCM) configuration --> <scm> <connection /> <developerConnection /> <tag /> <url /> </scm> + + <!-- Global properties --> <properties> + <!-- Java version used for the project --> <java.version>17</java.version> </properties> + + <!-- Project dependencies --> <dependencies> + <!-- ===================== Spring Boot Dependencies ===================== --> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-actuator</artifactId> + <artifactId>spring-boot-starter-actuator</artifactId> <!-- For monitoring and health checks --> </dependency> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-data-mongodb</artifactId> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-validation</artifactId> + <artifactId>spring-boot-starter-web</artifactId> <!-- For building RESTful web services --> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-devtools</artifactId> - <scope>runtime</scope> - <optional>true</optional> + <artifactId>spring-boot-starter-validation</artifactId> <!-- For validation support using annotations --> </dependency> + + <!-- ===================== Data & Persistence Dependencies ===================== --> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> + <artifactId>spring-boot-starter-data-mongodb</artifactId> <!-- MongoDB integration --> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-web</artifactId> - </dependency> - <dependency> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - <scope>compile</scope> - </dependency> - <dependency> - <groupId>org.springdoc</groupId> - <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> - <version>2.8.3</version> + <artifactId>spring-boot-starter-data-redis</artifactId> <!-- Redis integration --> + <version>3.4.1</version> </dependency> + <dependency> - <groupId>org.springframework.restdocs</groupId> - <artifactId>spring-restdocs-mockmvc</artifactId> - <scope>test</scope> + <groupId>redis.clients</groupId> + <artifactId>jedis</artifactId> <!-- Redis client library --> + <version>5.2.0</version> </dependency> + + <!-- ===================== Security Dependencies ===================== --> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-security</artifactId> + <artifactId>spring-boot-starter-security</artifactId> <!-- Spring Security for authentication and authorization --> </dependency> + <dependency> <groupId>org.springframework.security</groupId> - <artifactId>spring-security-test</artifactId> - <scope>test</scope> + <artifactId>spring-security-test</artifactId> <!-- Testing utilities for Spring Security --> + <scope>test</scope> <!-- Available only in test scope --> </dependency> - <!-- JWT --> + <!-- ===================== JWT (JSON Web Token) Dependencies ===================== --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> - <version>0.12.6</version> + <version>0.12.6</version> <!-- JWT core library --> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> - <version>0.12.6</version> + <version>0.12.6</version> <!-- JWT implementation --> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> - <version>0.12.6</version> + <version>0.12.6</version> <!-- JWT support for Jackson (JSON parsing) --> + </dependency> + + <!-- ===================== Testing Dependencies ===================== --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> <!-- Testing utilities for Spring Boot --> + <scope>test</scope> <!-- Available only in test scope --> </dependency> + + <dependency> + <groupId>org.springframework.restdocs</groupId> + <artifactId>spring-restdocs-mockmvc</artifactId> <!-- REST Docs for generating API documentation --> + <scope>test</scope> <!-- Available only in test scope --> + </dependency> + + <!-- ===================== Development & Tools Dependencies ===================== --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> <!-- Developer tools for faster development (restarts, etc.) --> + <scope>runtime</scope> <!-- Available only during runtime --> + <optional>true</optional> <!-- Optional dependency --> + </dependency> + + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> <!-- Lombok for reducing boilerplate code (getters/setters, constructors, etc.) --> + <scope>compile</scope> <!-- Available at compile time --> + </dependency> + + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <version>2.8.3</version> <!-- OpenAPI support for automatic API documentation --> + </dependency> + <dependency> <groupId>io.github.cdimascio</groupId> <artifactId>dotenv-java</artifactId> - <version>3.1.0</version> + <version>3.1.0</version> <!-- Load environment variables from a .env file --> </dependency> + <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> - <version>1.6.3</version> + <version>1.6.3</version> <!-- MapStruct for object mapping --> </dependency> + <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> - <version>1.6.3</version> + <version>1.6.3</version> <!-- MapStruct annotation processor --> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-cache</artifactId> + <artifactId>spring-boot-starter-cache</artifactId> <!-- Spring Boot caching support --> </dependency> + + <!-- ===================== Mail Dependencies ===================== --> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-mail</artifactId> + <artifactId>spring-boot-starter-mail</artifactId> <!-- Spring Boot mail support --> + </dependency> + + <!-- ===================== Miscellaneous Dependencies ===================== --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-pool2</artifactId> <!-- Apache Commons Pool for Redis connection management --> + <version>2.11.1</version> </dependency> + </dependencies> + <!-- Build configuration --> <build> <plugins> <plugin> @@ -136,21 +193,47 @@ </plugin> </plugins> </build> + + <!-- Profiles for different configurations --> <profiles> + <!-- Profiles for Docker --> <profile> <id>docker</id> <build> - <resources> - <resource> - <directory>src/main/resources</directory> - <excludes> - <exclude>.env</exclude> - </excludes> - </resource> - </resources> + <plugins> + <!-- Maven Resources Plugin --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>3.3.1</version> + <executions> + <execution> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${project.build.directory}/docker/resources</outputDirectory> + <resources> + <resource> + <!-- Including all resources in the project --> + <directory>src/main/resources</directory> + <filtering>false</filtering> + </resource> + <!-- Exclude the .env file from being included in the resources --> + <resource> + <directory>${basedir}</directory> <!-- The base directory (root of the project) --> + <excludes> + <exclude>.env</exclude> + </excludes> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> + </plugins> </build> </profile> </profiles> - </project> \ No newline at end of file diff --git a/src/main/java/smartpot/com/api/Cache/RedisConfig.java b/src/main/java/smartpot/com/api/Cache/RedisConfig.java new file mode 100644 index 0000000..f8ef184 --- /dev/null +++ b/src/main/java/smartpot/com/api/Cache/RedisConfig.java @@ -0,0 +1,82 @@ +package smartpot.com.api.Cache; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; +import org.springframework.data.redis.core.RedisTemplate; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class RedisConfig { + @Value(value = "${spring.data.redis.host}") + private String host; + + @Value(value = "${spring.data.redis.port}") + private String port; + + @Value("${spring.data.redis.username}") + private String username; + + @Value("${spring.data.redis.password}") + private String password; + + @Value("${spring.data.redis.database}") + private String database; + + @Value("${spring.data.redis.timeout}") + private long timeout; + + @Value("${CACHE_LETTUCE_POOL_MAX_ACTIVE}") + private int maxActive; + + @Value("${CACHE_LETTUCE_POOL_MAX_WAIT}") + private long maxWaitMillis; + + @Value("${CACHE_LETTUCE_POOL_MAX_IDLE}") + private int maxIdle; + + @Value("${CACHE_LETTUCE_POOL_MIN_IDLE}") + private int minIdle; + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(Integer.parseInt(port)); + redisStandaloneConfiguration.setDatabase(Integer.parseInt(database)); + redisStandaloneConfiguration.setUsername(username); + redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); + + GenericObjectPoolConfig<RedisConnection> poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMaxTotal(maxActive); + poolConfig.setMaxIdle(maxIdle); + poolConfig.setMinIdle(minIdle); + poolConfig.setBlockWhenExhausted(true); + poolConfig.setMaxWait(Duration.ofMillis(maxWaitMillis)); + + LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() + .poolConfig(poolConfig) + .commandTimeout(Duration.ofMillis(timeout)) + .shutdownTimeout(Duration.ofMillis(timeout)) + .build(); + + return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig); + } + + @Bean + public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { + RedisTemplate<String, Object> template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + return template; + } +} diff --git a/src/main/java/smartpot/com/api/Commands/Controller/CommandController.java b/src/main/java/smartpot/com/api/Commands/Controller/CommandController.java index 7aa6842..535b6de 100644 --- a/src/main/java/smartpot/com/api/Commands/Controller/CommandController.java +++ b/src/main/java/smartpot/com/api/Commands/Controller/CommandController.java @@ -1,59 +1,110 @@ package smartpot.com.api.Commands.Controller; -import org.bson.types.ObjectId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import smartpot.com.api.Commands.Model.DAO.Service.SCommandI; +import smartpot.com.api.Commands.Model.DTO.CommandDTO; import smartpot.com.api.Commands.Model.Entity.Command; -import smartpot.com.api.Crops.Model.DAO.Service.SCropI; -import smartpot.com.api.Crops.Model.Entity.Crop; - -import java.util.Date; -import java.util.List; -import java.util.Optional; +import smartpot.com.api.Commands.Service.SCommandI; +import smartpot.com.api.Crops.Model.DTO.CropDTO; +import smartpot.com.api.Responses.DeleteResponse; +import smartpot.com.api.Responses.ErrorResponse; @RestController @RequestMapping("/Comandos") public class CommandController { private final SCommandI serviceCommand; - private final SCropI serviceCrop; @Autowired - public CommandController(SCommandI serviceCommand, SCropI serviceCrop) { + public CommandController(SCommandI serviceCommand) { this.serviceCommand = serviceCommand; - this.serviceCrop = serviceCrop; } - @GetMapping("/All") - public List<Command> getAllCommand() { - return serviceCommand.getAllCommands(); + @PostMapping("/Create") + @Operation(summary = "Crear un nuevo comando", + description = "Crea un nuevo comando utilizando los datos proporcionados en el objeto CommandDTO. " + + "Si la creación es exitosa, se devuelve el comando recién creado.", + responses = { + @ApiResponse(description = "Comando creado", + responseCode = "201", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CropDTO.class))), + @ApiResponse(responseCode = "404", + description = "No se pudo crear el Comando debido a un error.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> createCommand(@Parameter(description = "Datos del nuevo comando que se va a crear. Debe incluir tipo y cultivo asociado.", + required = true) @RequestBody CommandDTO commandDTO) { + try { + return new ResponseEntity<>(serviceCommand.createCommand(commandDTO), HttpStatus.CREATED); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al crear el comando [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + + } } - @GetMapping("/id/{id}") - public Command getUserById(@PathVariable String id) { - return serviceCommand.getCommandById(id); + @GetMapping("/All") + @Operation(summary = "Obtener todos los comandos", + description = "Recupera todos los comandos registrados en el sistema. " + + "En caso de no haber comandos, se devolverá una excepción.", + responses = { + @ApiResponse(description = "Comandos encontrados", + responseCode = "200", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = CommandDTO.class)))), + @ApiResponse(responseCode = "404", + description = "No se encontraron Comandos registrados.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> getAllCommand() { + try { + return new ResponseEntity<>(serviceCommand.getAllCommands(), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al obtener los comandos [" + e.getMessage() + "]", HttpStatus.INTERNAL_SERVER_ERROR.value()), HttpStatus.INTERNAL_SERVER_ERROR); + } } - @PostMapping("/commandCreate/{cropId}") - @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity<Command> createCommand(@PathVariable String cropId, @RequestBody Command newCommand) { - Optional<Crop> cropOpt = Optional.ofNullable(serviceCrop.getCropById(cropId)); - if (cropOpt.isPresent()) { - newCommand.setCrop(new ObjectId(cropId)); - newCommand.setDateCreated(new Date()); - newCommand.setStatus("PENDING"); - Command savedCommand = serviceCommand.createCommand(newCommand); - return ResponseEntity.ok(savedCommand); - } else { - return ResponseEntity.notFound().build(); + @GetMapping("/id/{id}") + @Operation(summary = "Buscar comando por ID", + description = "Recupera un comando utilizando su ID único. " + + "Si el comando no existe, se devolverá un error con el código HTTP 404.", + responses = { + @ApiResponse(description = "Comando encontrado", + responseCode = "200", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CommandDTO.class))), + @ApiResponse(responseCode = "404", + description = "Comando no encontrado con el ID especificado.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> getUserById(@PathVariable String id) { + try { + return new ResponseEntity<>(serviceCommand.getCommandById(id), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al buscar el comando con ID '" + id + "' [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); } } - @PutMapping("/{id}/ejecutar") - public ResponseEntity<Command> executeCommand(@PathVariable String id) { + @PutMapping("/{id}/run/{response}") + @Operation(summary = "Actualizar un comando a ejecutado", + description = "Actualiza los datos de un comando existente utilizando su ID. " + + "Si el comando no existe o hay un error, se devolverá un error con código HTTP 404.", + responses = { + @ApiResponse(description = "Comando actualizado", + responseCode = "200", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = CommandDTO.class))), + @ApiResponse(responseCode = "404", + description = "No se pudo actualizar el Comando. El Comando puede no existir o los datos pueden ser incorrectos.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> executeCommand(@PathVariable String id, @PathVariable String response) { + /* Command command = serviceCommand.getCommandById(id); if (command != null) { command.setStatus("EXECUTED"); @@ -64,20 +115,38 @@ public ResponseEntity<Command> executeCommand(@PathVariable String id) { } else { return ResponseEntity.notFound().build(); } + + */ + + try { + return new ResponseEntity<>(serviceCommand.excuteCommand(id, response), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al actualizar el comando con ID '" + id + "' [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + } } - @DeleteMapping("/delete/{id}") - public ResponseEntity<Object> deleteCommand(@PathVariable String id) { - if (serviceCommand.getCommandById(id) != null) { - serviceCommand.deleteCommand(id); - return ResponseEntity.ok().build(); - } else { - return ResponseEntity.notFound().build(); + @DeleteMapping("/Delete/{id}") + @Operation(summary = "Eliminar un Comando", + description = "Elimina un Comando existente utilizando su ID. " + + "Si el Comando no existe o hay un error, se devolverá un error con código HTTP 404.", + responses = { + @ApiResponse(description = "Comando eliminado", + responseCode = "200", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = DeleteResponse.class))), + @ApiResponse(responseCode = "404", + description = "No se pudo eliminar el Comando.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> deleteCommand(@Parameter(description = "ID único del comando que se desea eliminar.", required = true) @PathVariable String id) { + try { + return new ResponseEntity<>(new DeleteResponse("Se ha eliminado un recurso [" + serviceCommand.deleteCommand(id) + "]"), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al actualizar el comando con ID '" + id + "' [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); } } @PutMapping("/Update/{id}") - public Command updateCommand(@PathVariable String id, @RequestBody Command updatedCommad) { + public Command updateCommand(@PathVariable String id, @RequestBody Command updatedCommad) throws Exception { return serviceCommand.updateCommand(id, updatedCommad); } diff --git a/src/main/java/smartpot/com/api/Commands/Model/DAO/Service/SCommandI.java b/src/main/java/smartpot/com/api/Commands/Model/DAO/Service/SCommandI.java deleted file mode 100644 index 6335d7c..0000000 --- a/src/main/java/smartpot/com/api/Commands/Model/DAO/Service/SCommandI.java +++ /dev/null @@ -1,17 +0,0 @@ -package smartpot.com.api.Commands.Model.DAO.Service; - -import smartpot.com.api.Commands.Model.Entity.Command; - -import java.util.List; - -public interface SCommandI { - List<Command> getAllCommands(); - - Command getCommandById(String id); - - Command createCommand(Command newCommand); - - Command updateCommand(String id, Command upCommand); - - void deleteCommand(String id); -} diff --git a/src/main/java/smartpot/com/api/Commands/Model/DTO/CommandDTO.java b/src/main/java/smartpot/com/api/Commands/Model/DTO/CommandDTO.java index 4c53bae..929c382 100644 --- a/src/main/java/smartpot/com/api/Commands/Model/DTO/CommandDTO.java +++ b/src/main/java/smartpot/com/api/Commands/Model/DTO/CommandDTO.java @@ -2,8 +2,10 @@ import lombok.Data; +import java.io.Serializable; + @Data -public class CommandDTO { +public class CommandDTO implements Serializable { private String id; private String commandType; private String status; diff --git a/src/main/java/smartpot/com/api/Commands/Model/DAO/Repository/RCommand.java b/src/main/java/smartpot/com/api/Commands/Repository/RCommand.java similarity index 97% rename from src/main/java/smartpot/com/api/Commands/Model/DAO/Repository/RCommand.java rename to src/main/java/smartpot/com/api/Commands/Repository/RCommand.java index ed01b5a..20c84c4 100644 --- a/src/main/java/smartpot/com/api/Commands/Model/DAO/Repository/RCommand.java +++ b/src/main/java/smartpot/com/api/Commands/Repository/RCommand.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Commands.Model.DAO.Repository; +package smartpot.com.api.Commands.Repository; import org.bson.types.ObjectId; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/src/main/java/smartpot/com/api/Commands/Model/DAO/Service/SCommand.java b/src/main/java/smartpot/com/api/Commands/Service/SCommand.java similarity index 57% rename from src/main/java/smartpot/com/api/Commands/Model/DAO/Service/SCommand.java rename to src/main/java/smartpot/com/api/Commands/Service/SCommand.java index a98cb6f..444c679 100644 --- a/src/main/java/smartpot/com/api/Commands/Model/DAO/Service/SCommand.java +++ b/src/main/java/smartpot/com/api/Commands/Service/SCommand.java @@ -1,19 +1,26 @@ -package smartpot.com.api.Commands.Model.DAO.Service; +package smartpot.com.api.Commands.Service; import lombok.Builder; import lombok.Data; import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import smartpot.com.api.Commands.Model.DAO.Repository.RCommand; +import smartpot.com.api.Commands.Mapper.MCommand; +import smartpot.com.api.Commands.Model.DTO.CommandDTO; import smartpot.com.api.Commands.Model.Entity.Command; -import smartpot.com.api.Crops.Model.DAO.Service.SCropI; +import smartpot.com.api.Commands.Repository.RCommand; +import smartpot.com.api.Crops.Service.SCropI; import smartpot.com.api.Exception.ApiException; import smartpot.com.api.Exception.ApiResponse; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Optional; @Data @Builder @@ -22,11 +29,13 @@ public class SCommand implements SCommandI { private final RCommand repositoryCommand; private final SCropI serviceCrop; + private final MCommand mapperCommand; @Autowired - public SCommand(RCommand repositoryCommand, SCropI serviceCrop) { + public SCommand(RCommand repositoryCommand, SCropI serviceCrop, MCommand mapperCommand) { this.repositoryCommand = repositoryCommand; this.serviceCrop = serviceCrop; + this.mapperCommand = mapperCommand; } @Override @@ -35,19 +44,34 @@ public List<Command> getAllCommands() { } @Override - public Command getCommandById(String id) { - return repositoryCommand.findById(new ObjectId(id)).orElse(null); + @Cacheable(value = "commands", key = "'id_'+#id") + public CommandDTO getCommandById(String id) throws Exception { + return Optional.of(id) + .map(ObjectId::new) + .map(repositoryCommand::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .map(mapperCommand::toDTO) + .orElseThrow(() -> new Exception("El Comando no existe")); } @Override - public Command createCommand(Command newCommand) { - newCommand.setDateCreated(new Date()); - newCommand.setStatus("PENDING"); - return repositoryCommand.save(newCommand); + public CommandDTO createCommand(CommandDTO commandDTO) throws IllegalStateException { + return Optional.of(commandDTO) + .map(dto -> { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dto.setDateCreated(formatter.format(new Date())); + dto.setStatus("PENDING"); + return dto; + }) + .map(mapperCommand::toEntity) + .map(repositoryCommand::save) + .map(mapperCommand::toDTO) + .orElseThrow(() -> new IllegalStateException("El Comando ya existe")); } @Override - public Command updateCommand(String id, Command upCommand) { + public Command updateCommand(String id, Command upCommand) throws Exception { if (!ObjectId.isValid(id)) { throw new ApiException(new ApiResponse( "El ID '" + id + "' no es válido. Asegúrate de que tiene 24 caracteres y solo incluye dígitos hexadecimales (0-9, a-f, A-F).", @@ -111,8 +135,31 @@ public Command updateCommand(String id, Command upCommand) { } @Override - public void deleteCommand(String id) { - repositoryCommand.deleteById(new ObjectId(id)); + @CacheEvict(value = "commands", key = "'id_'+#id") + public String deleteCommand(String id) throws Exception { + return Optional.of(getCommandById(id)) + .map(command -> { + repositoryCommand.deleteById(new ObjectId(command.getId())); + return "El Comando con ID '" + id + "' fue eliminado."; + }) + .orElseThrow(() -> new Exception("El Comando no existe.")); + } + + @Override + @CachePut(value = "commands", key = "'id:'+#id") + public CommandDTO excuteCommand(String id, String response) throws Exception { + return Optional.of(getCommandById(id)) + .map(commandDTO -> { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + commandDTO.setDateCreated(formatter.format(new Date())); + commandDTO.setStatus("EXECUTED"); + commandDTO.setResponse(response); + return commandDTO; + }) + .map(mapperCommand::toEntity) + .map(repositoryCommand::save) + .map(mapperCommand::toDTO) + .orElseThrow(() -> new Exception("El Comando no se pudo actualizar")); } /* diff --git a/src/main/java/smartpot/com/api/Commands/Service/SCommandI.java b/src/main/java/smartpot/com/api/Commands/Service/SCommandI.java new file mode 100644 index 0000000..5a54198 --- /dev/null +++ b/src/main/java/smartpot/com/api/Commands/Service/SCommandI.java @@ -0,0 +1,20 @@ +package smartpot.com.api.Commands.Service; + +import smartpot.com.api.Commands.Model.DTO.CommandDTO; +import smartpot.com.api.Commands.Model.Entity.Command; + +import java.util.List; + +public interface SCommandI { + List<Command> getAllCommands(); + + CommandDTO getCommandById(String id) throws Exception; + + CommandDTO createCommand(CommandDTO newCommand); + + Command updateCommand(String id, Command upCommand) throws Exception; + + String deleteCommand(String id) throws Exception; + + CommandDTO excuteCommand(String id, String reponse) throws Exception; +} diff --git a/src/main/java/smartpot/com/api/Crops/Controller/CropController.java b/src/main/java/smartpot/com/api/Crops/Controller/CropController.java index 7c69839..2af2ca7 100644 --- a/src/main/java/smartpot/com/api/Crops/Controller/CropController.java +++ b/src/main/java/smartpot/com/api/Crops/Controller/CropController.java @@ -1,6 +1,7 @@ package smartpot.com.api.Crops.Controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -10,8 +11,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import smartpot.com.api.Crops.Model.DAO.Service.SCropI; import smartpot.com.api.Crops.Model.DTO.CropDTO; +import smartpot.com.api.Crops.Service.SCropI; import smartpot.com.api.Responses.ErrorResponse; import smartpot.com.api.Users.Model.DTO.UserDTO; @@ -42,7 +43,7 @@ public CropController(SCropI serviceCrop) { * Si el cultivo es creado exitosamente, se devolverá el objeto con la información del cultivo recién creado.</p> * <p>En caso de que ocurra un error durante la creación del cultivo, se devolverá un mensaje de error con el código HTTP 404.</p> * - * @param newCropDto El objeto {@link CropDTO} que contiene los datos del nuevo cultivo a crear. Este objeto debe incluir toda la información necesaria para crear el cultivo. + * @param cropDTO El objeto {@link CropDTO} que contiene los datos del nuevo cultivo a crear. Este objeto debe incluir toda la información necesaria para crear el cultivo. * @return Un objeto {@link ResponseEntity} que contiene: * <ul> * <li>El cultivo recién creado (código HTTP 201).</li> @@ -67,9 +68,10 @@ public CropController(SCropI serviceCrop) { description = "No se pudo crear el cultivo debido a un error.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> createCrop(@RequestBody CropDTO newCropDto) { + public ResponseEntity<?> createCrop(@Parameter(description = "Datos del nuevo cultivo que se va a crear. Debe incluir tipo y usuario asociado.", + required = true) @RequestBody CropDTO cropDTO) { try { - return new ResponseEntity<>(serviceCrop.createCrop(newCropDto), HttpStatus.CREATED); + return new ResponseEntity<>(serviceCrop.createCrop(cropDTO), HttpStatus.CREATED); } catch (Exception e) { return new ResponseEntity<>(new ErrorResponse("Error al crear el cultivo [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); @@ -95,9 +97,9 @@ public ResponseEntity<?> createCrop(@RequestBody CropDTO newCropDto) { @GetMapping("/All") @Operation(summary = "Obtener todos los cultivos", description = "Recupera todos los cultivos registrados en el sistema. " - + "En caso de no haber cultivos, se devolverá una lista vacía.", + + "En caso de no haber cultivos, se devolverá una excepción.", responses = { - @io.swagger.v3.oas.annotations.responses.ApiResponse(description = "Cultivos encontrados", + @ApiResponse(description = "Cultivos encontrados", responseCode = "200", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = UserDTO.class)))), @@ -107,7 +109,7 @@ public ResponseEntity<?> createCrop(@RequestBody CropDTO newCropDto) { }) public ResponseEntity<?> getAllCrops() { try { - return new ResponseEntity<>(serviceCrop.getCrops(), HttpStatus.OK); + return new ResponseEntity<>(serviceCrop.getAllCrops(), HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(new ErrorResponse(e.getMessage(), HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); } @@ -145,7 +147,7 @@ public ResponseEntity<?> getAllCrops() { description = "Cultivo no encontrado con el ID especificado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getCropById(@PathVariable String id) { + public ResponseEntity<?> getCropById(@Parameter(description = "ID único del cultivo", required = true) @PathVariable String id) { try { return new ResponseEntity<>(serviceCrop.getCropById(id), HttpStatus.OK); } catch (Exception e) { @@ -183,7 +185,7 @@ public ResponseEntity<?> getCropById(@PathVariable String id) { description = "No se encontraron cultivos con el estado proporcionado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getCropsByStatus(@PathVariable String status) { + public ResponseEntity<?> getCropsByStatus(@Parameter(description = "Estado de los cultivos a buscar", required = true) @PathVariable String status) { try { return new ResponseEntity<>(serviceCrop.getCropsByStatus(status), HttpStatus.OK); } catch (Exception e) { @@ -191,6 +193,44 @@ public ResponseEntity<?> getCropsByStatus(@PathVariable String status) { } } + /** + * Recupera todos los estados de cultivo registrados en el sistema. + * <p>Este método obtiene una lista de todos los estados de cultivo disponibles en el sistema. Si no se encuentran estados de cultivo, + * se lanzará una excepción y se devolverá un código HTTP 404 con un mensaje de error.</p> + * + * @return Un objeto {@link ResponseEntity} que contiene: + * <ul> + * <li>Una lista de cadenas {@link String} con los estados de cultivo (código HTTP 200).</li> + * <li>Un mensaje de error si ocurre un problema al obtener los estados de cultivo o no se encuentran registrados (código HTTP 404).</li> + * </ul> + * + * <p><b>Respuestas posibles:</b></p> + * <ul> + * <li><b>200 OK</b>: Si se encuentran estados de cultivo registrados, se retorna una lista de cadenas con los nombres de los estados de cultivo en formato JSON.<br></li> + * <li><b>404 Not Found</b>: Si no se encuentran estados de cultivo registrados o ocurre un error al obtenerlos, se retorna un objeto {@link ErrorResponse} con un mensaje de error.<br></li> + * </ul> + */ + @GetMapping("/status/All") + @Operation(summary = "Obtener todos los estados de cultivo", + description = "Recupera todos los estados de cultivos registrados en el sistema. " + + "En caso de no haber estados de cultivos, se devolverá una excepción.", + responses = { + @ApiResponse(description = "Estados de cultivo encontrados", + responseCode = "200", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = String.class)))), + @ApiResponse(responseCode = "404", + description = "No se encontraron estados de cultivo registrados.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> getAllStatus() { + try { + return new ResponseEntity<>(serviceCrop.getAllStatus(), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al buscar los estados de cultivo [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + } + } + /** * Busca cultivos por su tipo. * <p>Este método recupera una lista de cultivos en el sistema filtrados por su tipo. El parámetro `type` es utilizado para determinar el tipo de los cultivos que se desean recuperar. Si se encuentran cultivos con el tipo especificado, se devolverá una lista de objetos {@link CropDTO} que representan los cultivos encontrados.</p> @@ -221,7 +261,7 @@ public ResponseEntity<?> getCropsByStatus(@PathVariable String status) { description = "No se encontraron cultivos con el tipo proporcionado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getCropsByType(@PathVariable String type) { + public ResponseEntity<?> getCropsByType(@Parameter(description = "Tipo de los cultivos a buscar", required = true) @PathVariable String type) { try { return new ResponseEntity<>(serviceCrop.getCropsByType(type), HttpStatus.OK); } catch (Exception e) { @@ -229,6 +269,44 @@ public ResponseEntity<?> getCropsByType(@PathVariable String type) { } } + /** + * Recupera todos los tipos de cultivo registrados en el sistema. + * <p>Este método obtiene una lista de todos los tipos de cultivo disponibles en el sistema. Si no se encuentran tipos de cultivo, + * se lanzará una excepción y se devolverá un código HTTP 404 con un mensaje de error.</p> + * + * @return Un objeto {@link ResponseEntity} que contiene: + * <ul> + * <li>Una lista de cadenas {@link String} con los tipos de cultivo (código HTTP 200).</li> + * <li>Un mensaje de error si ocurre un problema al obtener los tipos de cultivo o no se encuentran registrados (código HTTP 404).</li> + * </ul> + * + * <p><b>Respuestas posibles:</b></p> + * <ul> + * <li><b>200 OK</b>: Si se encuentran tipos de cultivo registrados, se retorna una lista de cadenas con los nombres de los tipos de cultivo en formato JSON.<br></li> + * <li><b>404 Not Found</b>: Si no se encuentran tipos de cultivo registrados o ocurre un error al obtenerlos, se retorna un objeto {@link ErrorResponse} con un mensaje de error.<br></li> + * </ul> + */ + @GetMapping("/type/All") + @Operation(summary = "Obtener todos los tipos de cultivo", + description = "Recupera todos los tipos de cultivos registrados en el sistema. " + + "En caso de no haber tipos de cultivos, se devolverá una excepción.", + responses = { + @ApiResponse(description = "Tipos de cultivo encontrados", + responseCode = "200", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = String.class)))), + @ApiResponse(responseCode = "404", + description = "No se encontraron tipos de cultivo registrados.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> getAllTypes() { + try { + return new ResponseEntity<>(serviceCrop.getAllTypes(), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al buscar los tipos de cultivo [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + } + } + /** * Busca todos los cultivos asociados a un usuario específico. * <p>Este método recupera todos los cultivos pertenecientes a un usuario específico utilizando su ID único. Si el usuario con el ID proporcionado tiene cultivos, se devolverá una lista de objetos {@link CropDTO} con la información de cada cultivo.</p> @@ -260,7 +338,7 @@ public ResponseEntity<?> getCropsByType(@PathVariable String type) { description = "No se encontraron cultivos para el usuario con el ID especificado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getCropByUser(@PathVariable String id) { + public ResponseEntity<?> getCropByUser(@PathVariable @Parameter(description = "ID único del usuario para buscar sus cultivos", required = true) String id) { try { return new ResponseEntity<>(serviceCrop.getCropsByUser(id), HttpStatus.OK); } catch (Exception e) { @@ -301,7 +379,7 @@ public ResponseEntity<?> getCropByUser(@PathVariable String id) { description = "No se encontraron cultivos para el usuario con el ID especificado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> countCropsByUser(@PathVariable String id) { + public ResponseEntity<?> countCropsByUser(@PathVariable @Parameter(description = "ID único del usuario para buscar sus cultivos", required = true) String id) { try { return new ResponseEntity<>(serviceCrop.countCropsByUser(id), HttpStatus.OK); } catch (Exception e) { @@ -315,8 +393,8 @@ public ResponseEntity<?> countCropsByUser(@PathVariable String id) { * Si el cultivo con el ID proporcionado existe, se actualizarán sus detalles con la información proporcionada en el objeto {@link CropDTO}.</p> * <p>Si el cultivo no existe o si ocurre algún error durante el proceso de actualización, se devolverá un mensaje de error con el código HTTP 404.</p> * - * @param id El identificador único del cultivo que se desea actualizar. Este parámetro es obligatorio para identificar el cultivo en la base de datos. - * El ID debe ser válido y hacer referencia a un cultivo existente. + * @param id El identificador único del cultivo que se desea actualizar. Este parámetro es obligatorio para identificar el cultivo en la base de datos. + * El ID debe ser válido y hacer referencia a un cultivo existente. * @param cropDetails El objeto {@link CropDTO} que contiene los nuevos datos del cultivo que se desean actualizar. Este objeto debe incluir toda la información que reemplazará los detalles actuales del cultivo. * @return Un objeto {@link ResponseEntity} que contiene: * <ul> @@ -342,7 +420,7 @@ public ResponseEntity<?> countCropsByUser(@PathVariable String id) { description = "Cultivo no encontrado o error en la actualización.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> updateCrop(@PathVariable String id, @RequestBody CropDTO cropDetails) { + public ResponseEntity<?> updateCrop(@Parameter(description = "ID único del cultivo a actualizar", required = true) @PathVariable String id, @Parameter(description = "Información del cultivo a actualizar", required = true) @RequestBody CropDTO cropDetails) { try { return new ResponseEntity<>(serviceCrop.updatedCrop(id, cropDetails), HttpStatus.OK); } catch (Exception e) { @@ -382,9 +460,9 @@ public ResponseEntity<?> updateCrop(@PathVariable String id, @RequestBody CropDT description = "Cultivo no encontrado o error en la eliminación.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> deleteCrop(@PathVariable String id) { + public ResponseEntity<?> deleteCrop(@Parameter(description = "ID único del cultivo a eliminar", required = true) @PathVariable String id) { try { - return new ResponseEntity<>(serviceCrop.deleteCrop(serviceCrop.getCropById(id)), HttpStatus.NO_CONTENT); + return new ResponseEntity<>(serviceCrop.deleteCrop(id), HttpStatus.NO_CONTENT); } catch (Exception e) { return new ResponseEntity<>(new ErrorResponse("Error al eliminar el cultivo con ID '" + id + "' [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); } diff --git a/src/main/java/smartpot/com/api/Crops/Mapper/MCrop.java b/src/main/java/smartpot/com/api/Crops/Mapper/MCrop.java index bc6e3d3..1c77bc6 100644 --- a/src/main/java/smartpot/com/api/Crops/Mapper/MCrop.java +++ b/src/main/java/smartpot/com/api/Crops/Mapper/MCrop.java @@ -3,14 +3,11 @@ import org.bson.types.ObjectId; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.factory.Mappers; import smartpot.com.api.Crops.Model.DTO.CropDTO; import smartpot.com.api.Crops.Model.Entity.Crop; @Mapper(componentModel = "spring") public interface MCrop { - MCrop INSTANCE = Mappers.getMapper(MCrop.class); - @Mapping(source = "id", target = "id", qualifiedByName = "stringToObjectId") @Mapping(source = "user", target = "user", qualifiedByName = "stringToObjectId") Crop toEntity(CropDTO cropDTO); diff --git a/src/main/java/smartpot/com/api/Crops/Model/DAO/Repository/RCrop.java b/src/main/java/smartpot/com/api/Crops/Model/DAO/Repository/RCrop.java deleted file mode 100644 index 7f67882..0000000 --- a/src/main/java/smartpot/com/api/Crops/Model/DAO/Repository/RCrop.java +++ /dev/null @@ -1,34 +0,0 @@ -package smartpot.com.api.Crops.Model.DAO.Repository; - -import org.bson.types.ObjectId; -import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.data.mongodb.repository.Query; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import smartpot.com.api.Crops.Model.Entity.Crop; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface RCrop extends MongoRepository<Crop, ObjectId> { - - @Query("{ '_id' : ?0 }") - Optional<Crop> findById(ObjectId id); - - @Query("{ 'type' : ?0 }") - List<Crop> findByType(String type); - - @Query("{ 'status' : ?0 }") - List<Crop> findByStatus(String status); - - /*@Transactional - @Query("{ '_id' : ?0 }") - void deleteById(ObjectId id); -*/ - @Transactional - @Query("{ '_id' : ?0 }") - Crop updateUser(ObjectId id, Crop crop); - - -} diff --git a/src/main/java/smartpot/com/api/Crops/Model/DAO/Service/SCrop.java b/src/main/java/smartpot/com/api/Crops/Model/DAO/Service/SCrop.java deleted file mode 100644 index 94643c8..0000000 --- a/src/main/java/smartpot/com/api/Crops/Model/DAO/Service/SCrop.java +++ /dev/null @@ -1,264 +0,0 @@ -package smartpot.com.api.Crops.Model.DAO.Service; - -import lombok.Builder; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.bson.types.ObjectId; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; -import smartpot.com.api.Crops.Model.DAO.Repository.RCrop; -import smartpot.com.api.Crops.Model.DTO.CropDTO; -import smartpot.com.api.Crops.Model.Entity.Crop; -import smartpot.com.api.Crops.Model.Entity.Status; -import smartpot.com.api.Crops.Model.Entity.Type; -import smartpot.com.api.Exception.ApiException; -import smartpot.com.api.Exception.ApiResponse; -import smartpot.com.api.Users.Model.DAO.Service.SUserI; -import smartpot.com.api.Users.Model.DTO.UserDTO; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - - -/** - * Servicio que gestiona las operaciones relacionadas con los cultivos. - * Proporciona métodos para crear, leer, actualizar y eliminar cultivos, - * así como búsquedas específicas por diferentes criterios. - */ -@Slf4j -@Data -@Builder -@Service -public class SCrop implements SCropI { - - private final RCrop repositoryCrop; - private final SUserI serviceUser; - - @Autowired - public SCrop(RCrop repositoryCrop, SUserI serviceUser) { - this.repositoryCrop = repositoryCrop; - this.serviceUser = serviceUser; - } - - /** - * Este metodo maneja el ID como un ObjectId de MongoDB. Si el ID es válido como ObjectId, - * se utiliza en la búsqueda como tal. - * - * @param id El identificador del cultivo a buscar. Se recibe como String para evitar errores de conversion. - * @return El cultivo correspondiente al id proporcionado. - * @throws ResponseStatusException Si el id proporcionado no es válido o no se encuentra el cultivo. - * @throws Exception Si no se encuentra el cultivo con el id proporcionado. - */ - @Override - public Crop getCropById(String id) { - if (!ObjectId.isValid(id)) { - throw new ApiException(new ApiResponse( - "El cultivo con id '" + id + "' no es válido. Asegúrate de que tiene 24 caracteres y solo incluye dígitos hexadecimales (0-9, a-f, A-F).", - HttpStatus.BAD_REQUEST.value() - )); - } - return repositoryCrop.findById(new ObjectId(id)) - .orElseThrow(() -> new ApiException( - new ApiResponse("El cultivo con id '" + id + "' no fue encontrado.", - HttpStatus.NOT_FOUND.value()) - )); - } - - /** - * Obtiene todos los cultivos almacenados en el sistema. - * - * @return Lista de todos los cultivos existentes - */ - @Override - public List<Crop> getCrops() { - List<Crop> crops = repositoryCrop.findAll(); - if (crops == null || crops.isEmpty()) { - throw new ApiException(new ApiResponse( - "No se encontro ningun cultivo en la db", - HttpStatus.NOT_FOUND.value() - )); - } - return crops; - } - - /** - * Busca todos los cultivos asociados a un usuario específico. - * - * @param id del Usuario propietario de los cultivos - * @return Lista de cultivos pertenecientes al usuario - */ - @Override - public List<Crop> getCropsByUser(String id) throws Exception { - UserDTO user = serviceUser.getUserById(id); - List<Crop> crops = repositoryCrop.findAll(); - List<Crop> cropsUser = new ArrayList<>(); - ObjectId userId = new ObjectId(user.getId()); - for (Crop crop : crops) { - if (crop.getUser().equals(userId)) { - cropsUser.add(crop); - } - } - if (cropsUser.isEmpty()) { - throw new ApiException(new ApiResponse( - "No se encuentra ningun Cultivo perteneciente al usuario con el id: '" + id + "'.", - HttpStatus.NOT_FOUND.value() - )); - } - return cropsUser; - } - - /** - * Busca cultivos por su tipo . - * - * @param type Tipo del cultivo - * @return Lista de cultivos que coinciden con el tipo especificado - */ - @Override - public List<Crop> getCropsByType(String type) { - boolean isValidSType = Stream.of(Type.values()) - .anyMatch(r -> r.name().equalsIgnoreCase(type)); - if (!isValidSType) { - throw new ApiException(new ApiResponse( - "El Type '" + type + "' no es válido.", - HttpStatus.BAD_REQUEST.value())); - } - List<Crop> cropsByType = repositoryCrop.findByType(type); - return repositoryCrop.findByType(type); - } - - /** - * Cuenta el número total de cultivos que tiene un usuario. - * - * @param id del Usuario del que se quieren contar los cultivos - * @return Número total de cultivos del usuario - */ - @Override - public long countCropsByUser(String id) throws Exception { - System.out.println("//////////////////////////////////////////////" + getCropsByUser(id).size()); - return getCropsByUser(id).size(); - - } - - /** - * Busca cultivos por su estado actual. - * - * @param status Estado del cultivo a buscar - * @return Lista de cultivos que se encuentran en el estado especificado - */ - @Override - public List<Crop> getCropsByStatus(String status) { - boolean isValidStatus = Stream.of(Status.values()) - .anyMatch(r -> r.name().equalsIgnoreCase(status)); - if (!isValidStatus) { - throw new ApiException(new ApiResponse( - "El Status '" + status + "' no es válido.", - HttpStatus.BAD_REQUEST.value())); - } - List<Crop> cropsByStatus = repositoryCrop.findByStatus(status); - return cropsByStatus; - } - - /** - * Crea un cultivo en el sistema. - * - * @return Cultivo guardado - */ - @Override - public Crop createCrop(CropDTO newCropDto) throws Exception { - serviceUser.getUserById(newCropDto.getUser()); - Crop newCrop = cropDtotoCrop(newCropDto); - boolean isValidStatus = Stream.of(Status.values()) - .anyMatch(r -> r.name().equalsIgnoreCase(newCrop.getStatus().name())); - if (!isValidStatus) { - throw new ApiException(new ApiResponse( - "El Status '" + newCrop.getStatus().name() + "' no es válido.", - HttpStatus.BAD_REQUEST.value())); - } - boolean isValidSType = Stream.of(Type.values()) - .anyMatch(r -> r.name().equalsIgnoreCase(newCrop.getType().name())); - if (!isValidSType) { - throw new ApiException(new ApiResponse( - "El Type '" + newCrop.getType().name() + "' no es válido.", - HttpStatus.BAD_REQUEST.value())); - } - return repositoryCrop.save(newCrop); - } - - /** - * Actualiza la información de un Crop existente. - * - * @param id El identificador del Crop a actualizar. - * @return El Crop actualizado después de guardarlo en el servicio. - */ - @Override - public Crop updatedCrop(String id, CropDTO cropDto) throws Exception { - serviceUser.getUserById(cropDto.getUser()); - Crop updatedCrop = cropDtotoCrop(cropDto); - boolean isValidStatus = Stream.of(Status.values()) - .anyMatch(r -> r.name().equalsIgnoreCase(updatedCrop.getStatus().name())); - if (!isValidStatus) { - throw new ApiException(new ApiResponse( - "El Status '" + updatedCrop.getStatus().name() + "' no es válido.", - HttpStatus.BAD_REQUEST.value())); - } - boolean isValidSType = Stream.of(Type.values()) - .anyMatch(r -> r.name().equalsIgnoreCase(updatedCrop.getType().name())); - if (!isValidSType) { - throw new ApiException(new ApiResponse( - "El Type '" + updatedCrop.getType().name() + "' no es válido.", - HttpStatus.BAD_REQUEST.value())); - } - return repositoryCrop.updateUser(getCropById(id).getId(), updatedCrop); - } - - private Crop cropDtotoCrop(CropDTO cropDto) { - Crop crop = new Crop(); - crop.setType(Type.valueOf(cropDto.getType())); - crop.setStatus(Status.valueOf(cropDto.getStatus())); - crop.setUser(new ObjectId(cropDto.getUser())); - return crop; - } - - - /** - * Elimina un cultivo existente por su identificador. - * - * @param id Es el identificador del cultivo que se desea eliminar. - */ - /* public void deleteCrop(String id) { - if (!ObjectId.isValid(id)) { - throw new ApiException(new ApiResponse( - "El ID '" + id + "' no es válido. Asegúrate de que tiene 24 caracteres y solo incluye dígitos hexadecimales (0-9, a-f, A-F).", - HttpStatus.BAD_REQUEST.value() - )); - } - Crop existingCrop = repositoryCrop.findById(new ObjectId(id)) - .orElseThrow(() -> new ApiException( - new ApiResponse("El usuario con ID '" + id + "' no fue encontrado.", - HttpStatus.NOT_FOUND.value()) - )); - - repositoryCrop.deleteById(existingCrop.getId()); - }*/ - @Override - public ResponseEntity<ApiResponse> deleteCrop(Crop existingCrop) { - try { - repositoryCrop.deleteById(existingCrop.getId()); - return ResponseEntity.status(HttpStatus.OK.value()).body( - new ApiResponse("El cultivo con ID '" + existingCrop.getId() + "' fue eliminado.", - HttpStatus.OK.value()) - ); - } catch (Exception e) { - log.error("e: ", e); - throw new ApiException( - new ApiResponse("No se pudo eliminar el usuario con ID '" + existingCrop.getId() + "'.", - HttpStatus.INTERNAL_SERVER_ERROR.value())); - } - } -} - - diff --git a/src/main/java/smartpot/com/api/Crops/Model/DAO/Service/SCropI.java b/src/main/java/smartpot/com/api/Crops/Model/DAO/Service/SCropI.java deleted file mode 100644 index 80440f7..0000000 --- a/src/main/java/smartpot/com/api/Crops/Model/DAO/Service/SCropI.java +++ /dev/null @@ -1,43 +0,0 @@ -package smartpot.com.api.Crops.Model.DAO.Service; - -import org.springframework.http.ResponseEntity; -import smartpot.com.api.Crops.Model.DTO.CropDTO; -import smartpot.com.api.Crops.Model.Entity.Crop; -import smartpot.com.api.Exception.ApiResponse; - -import java.util.List; - -public interface SCropI { - Crop getCropById(String id); - - List<Crop> getCrops(); - - List<Crop> getCropsByUser(String id) throws Exception; - - List<Crop> getCropsByType(String type); - - long countCropsByUser(String id) throws Exception; - - List<Crop> getCropsByStatus(String status); - - Crop createCrop(CropDTO newCropDto) throws Exception; - - Crop updatedCrop(String id, CropDTO cropDto) throws Exception; - - /* public void deleteCrop(String id) { - if (!ObjectId.isValid(id)) { - throw new ApiException(new ApiResponse( - "El ID '" + id + "' no es válido. Asegúrate de que tiene 24 caracteres y solo incluye dígitos hexadecimales (0-9, a-f, A-F).", - HttpStatus.BAD_REQUEST.value() - )); - } - Crop existingCrop = repositoryCrop.findById(new ObjectId(id)) - .orElseThrow(() -> new ApiException( - new ApiResponse("El usuario con ID '" + id + "' no fue encontrado.", - HttpStatus.NOT_FOUND.value()) - )); - - repositoryCrop.deleteById(existingCrop.getId()); - }*/ - ResponseEntity<ApiResponse> deleteCrop(Crop existingCrop); -} diff --git a/src/main/java/smartpot/com/api/Crops/Model/DTO/CropDTO.java b/src/main/java/smartpot/com/api/Crops/Model/DTO/CropDTO.java index 15205d3..f7cecd9 100644 --- a/src/main/java/smartpot/com/api/Crops/Model/DTO/CropDTO.java +++ b/src/main/java/smartpot/com/api/Crops/Model/DTO/CropDTO.java @@ -1,11 +1,37 @@ package smartpot.com.api.Crops.Model.DTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.RequiredArgsConstructor; +import java.io.Serializable; + + +/** + * DTO para representar los datos de un cultivo. + * Este DTO es utilizado para transferir la información de un cultivo + * entre diferentes capas del sistema, especialmente para su visualización + * o manipulación en las operaciones CRUD de cultivos. + */ @Data -public class CropDTO { +@AllArgsConstructor +@RequiredArgsConstructor +public class CropDTO implements Serializable { + + @Schema(description = "ID único del cultivo, generado automáticamente por la base de datos.", + example = "60b63b8f3e111f8d44d45e72", hidden = true) private String id; + + @Schema(description = "Estado actual del cultivo.", + example = "Perfect_plant") private String status; + + @Schema(description = "Tipo de cultivo.", + example = "TOMATO") private String type; + + @Schema(description = "ID del usuario asociado a este cultivo. Este campo se utiliza para asociar un cultivo a un usuario específico del sistema.", + example = "676ae2a9b909de5f9607fcb6") private String user; } diff --git a/src/main/java/smartpot/com/api/Crops/Model/Entity/Crop.java b/src/main/java/smartpot/com/api/Crops/Model/Entity/Crop.java index 40a695d..623a424 100644 --- a/src/main/java/smartpot/com/api/Crops/Model/Entity/Crop.java +++ b/src/main/java/smartpot/com/api/Crops/Model/Entity/Crop.java @@ -34,13 +34,16 @@ public class Crop implements Serializable { private ObjectId id; @Field("status") - private Status status; + private CropStatus cropStatus; @NotEmpty(message = "El tipo no puede estar vacío") @Field("type") - private Type type; + private CropType cropType; - /*@DBRef*/ + /** + * ! No se puede hacer referencia a los objetos, dado que obliga a usar la entidad completa, no solo el ObjectId. + */ + //@DBRef @NotNull(message = "El cultivo debe pertenecer a un usuario") @Field("user") private ObjectId user; diff --git a/src/main/java/smartpot/com/api/Crops/Model/Entity/Status.java b/src/main/java/smartpot/com/api/Crops/Model/Entity/CropStatus.java similarity index 83% rename from src/main/java/smartpot/com/api/Crops/Model/Entity/Status.java rename to src/main/java/smartpot/com/api/Crops/Model/Entity/CropStatus.java index f447c33..51f9f53 100644 --- a/src/main/java/smartpot/com/api/Crops/Model/Entity/Status.java +++ b/src/main/java/smartpot/com/api/Crops/Model/Entity/CropStatus.java @@ -1,6 +1,6 @@ package smartpot.com.api.Crops.Model.Entity; -public enum Status { +public enum CropStatus { Dead, Extreme_decomposition, Severe_deterioration, Moderate_deterioration, Healthy_state, intermittent, - Moderate_health, Good_health, Very_healthy, Excellent, Perfect_plant + Moderate_health, Good_health, Very_healthy, Excellent, Perfect_plant, Unknown; } diff --git a/src/main/java/smartpot/com/api/Crops/Model/Entity/Type.java b/src/main/java/smartpot/com/api/Crops/Model/Entity/CropType.java similarity index 52% rename from src/main/java/smartpot/com/api/Crops/Model/Entity/Type.java rename to src/main/java/smartpot/com/api/Crops/Model/Entity/CropType.java index 1e4d894..6cb8fb4 100644 --- a/src/main/java/smartpot/com/api/Crops/Model/Entity/Type.java +++ b/src/main/java/smartpot/com/api/Crops/Model/Entity/CropType.java @@ -1,5 +1,5 @@ package smartpot.com.api.Crops.Model.Entity; -public enum Type { - TOMATTO, LETTUCE +public enum CropType { + TOMATO, LETTUCE; } diff --git a/src/main/java/smartpot/com/api/Crops/Repository/RCrop.java b/src/main/java/smartpot/com/api/Crops/Repository/RCrop.java new file mode 100644 index 0000000..22e5abf --- /dev/null +++ b/src/main/java/smartpot/com/api/Crops/Repository/RCrop.java @@ -0,0 +1,68 @@ +package smartpot.com.api.Crops.Repository; + +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; +import smartpot.com.api.Crops.Model.Entity.Crop; + +import java.util.List; + +/** + * Repositorio para la gestión de la entidad {@link Crop} en la base de datos MongoDB. + * <p> + * Esta interfaz extiende {@link MongoRepository}, lo que proporciona métodos CRUD básicos + * para interactuar con la base de datos MongoDB de manera sencilla. + * Además, incluye consultas personalizadas para buscar cultivos por diferentes atributos + * como tipo, estado y usuario. + * </p> + * <p> + * Las consultas personalizadas están anotadas con {@link Query}, que utilizan la sintaxis de MongoDB + * para realizar búsquedas más específicas en los campos de la colección de cultivos. + * </p> + * + * @see MongoRepository + * @see Crop + */ +@Repository +public interface RCrop extends MongoRepository<Crop, ObjectId> { + /** + * Busca una lista de cultivos cuyo tipo coincida exactamente con el tipo proporcionado. + * + * <p>Esta consulta busca cultivos cuyo campo 'type' coincida exactamente con el valor del parámetro + * proporcionado, lo que permite filtrar los cultivos por su tipo.</p> + * + * @param type El tipo de cultivo a buscar en el campo 'type'. + * @return Una lista de cultivos cuyo tipo coincida exactamente. + * @see Query + */ + @Query("{ 'type' : ?0 }") + List<Crop> findByType(String type); + + /** + * Busca una lista de cultivos cuyo estado coincida exactamente con el estado proporcionado. + * + * <p>Esta consulta busca cultivos cuyo campo 'status' coincida exactamente con el valor del parámetro + * proporcionado, lo que permite filtrar los cultivos por su estado (por ejemplo, 'activo', 'inactivo', etc.).</p> + * + * @param status El estado del cultivo a buscar en el campo 'status'. + * @return Una lista de cultivos cuyo estado coincida exactamente. + * @see Query + */ + @Query("{ 'status' : ?0 }") + List<Crop> findByStatus(String status); + + /** + * Busca una lista de cultivos cuyo campo 'user' coincida con el ID de usuario proporcionado. + * + * <p>Esta consulta busca cultivos cuyo campo 'user' coincida con el ID del usuario proporcionado. + * Permite filtrar los cultivos asociados a un usuario específico.</p> + * + * @param id El ID del usuario (en formato {@link ObjectId}) cuyo campo 'user' se desea buscar. + * @return Una lista de cultivos asociados al usuario con el ID proporcionado. + * @see Query + * @see ObjectId + */ + @Query("{ 'user': ?0}") + List<Crop> findByUser(ObjectId id); +} diff --git a/src/main/java/smartpot/com/api/Crops/Service/SCrop.java b/src/main/java/smartpot/com/api/Crops/Service/SCrop.java new file mode 100644 index 0000000..ff6dbc5 --- /dev/null +++ b/src/main/java/smartpot/com/api/Crops/Service/SCrop.java @@ -0,0 +1,429 @@ +package smartpot.com.api.Crops.Service; + +import jakarta.validation.ValidationException; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import smartpot.com.api.Crops.Mapper.MCrop; +import smartpot.com.api.Crops.Model.DTO.CropDTO; +import smartpot.com.api.Crops.Model.Entity.CropStatus; +import smartpot.com.api.Crops.Model.Entity.CropType; +import smartpot.com.api.Crops.Repository.RCrop; +import smartpot.com.api.Crops.Validator.VCropI; +import smartpot.com.api.Users.Service.SUserI; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +/** + * Servicio que gestiona las operaciones relacionadas con los cultivos. + * Proporciona métodos para crear, leer, actualizar y eliminar cultivos, + * así como búsquedas específicas por diferentes criterios. + */ +@Slf4j +@Data +@Builder +@Service +public class SCrop implements SCropI { + + private final RCrop repositoryCrop; + private final SUserI serviceUser; + private final MCrop mapperCrop; + private final VCropI validatorCrop; + + /** + * Constructor del servicio de cultivos. + * + * <p>Inyecta las dependencias necesarias para realizar las operaciones relacionadas con los cultivos, + * incluyendo el repositorio de cultivos {@link RCrop}, el servicio de usuarios {@link SUserI}, + * el convertidor de cultivos {@link MCrop}, y el validador de cultivos {@link VCropI}.</p> + * + * @param repositoryCrop El repositorio que maneja las operaciones de base de datos para cultivos. + * @param serviceUser El servicio de usuarios, utilizado para interactuar con los detalles de los usuarios. + * @param mapperCrop El convertidor que convierte las entidades de cultivos a objetos DTO correspondientes. + * @param validatorCrop El validador que valida los datos relacionados con los cultivos. + * @see RCrop + * @see SUserI + * @see MCrop + * @see VCropI + */ + @Autowired + public SCrop(RCrop repositoryCrop, SUserI serviceUser, MCrop mapperCrop, VCropI validatorCrop) { + this.repositoryCrop = repositoryCrop; + this.serviceUser = serviceUser; + this.mapperCrop = mapperCrop; + this.validatorCrop = validatorCrop; + } + + /** + * Crea un nuevo cultivo en la base de datos a partir de un objeto {@link CropDTO}. + * + * <p>Este método recibe un objeto {@link CropDTO}, valida el tipo del cultivo y el ID del usuario + * asociado utilizando el validador {@link VCropI}. Si las validaciones son correctas, se asigna el estado + * por defecto "Unknown" al cultivo y se persiste en la base de datos utilizando el repositorio {@link RCrop}. + * Finalmente, el método mapea la entidad guardada nuevamente a un {@link CropDTO} para devolverlo como respuesta.</p> + * + * <p>Si las validaciones fallan, se lanza una {@link ValidationException} con los detalles del error. + * Si el cultivo ya existe, se lanza una {@link Exception} indicando que el cultivo no puede ser creado.</p> + * + * @param cropDTO El objeto {@link CropDTO} que contiene los datos del cultivo a crear. Este parámetro es obligatorio. + * @return El objeto {@link CropDTO} que representa el cultivo creado, con el estado actualizado a "Unknown". + * @throws Exception Si el cultivo ya existe o si ocurre algún otro error durante la creación. + * @throws ValidationException Si las validaciones del tipo o ID del usuario fallan según el validador {@link VCropI}. + * @see CropDTO + * @see VCropI + * @see RCrop + * @see MCrop + */ + @Override + @CachePut(value = "crops", key = "#cropDTO.id") + public CropDTO createCrop(CropDTO cropDTO) throws Exception { + return Optional.of(cropDTO) + .map(ValidCropDTO -> { + validatorCrop.validateType(ValidCropDTO.getType()); + try { + serviceUser.getUserById(ValidCropDTO.getUser()); + } catch (Exception e) { + throw new ValidationException(e.getMessage() + ", asocia el cultivo a un usuario existente"); + } + + if (!validatorCrop.isValid()) { + throw new ValidationException(validatorCrop.getErrors().toString()); + } + validatorCrop.Reset(); + return ValidCropDTO; + }) + .map(dto -> { + dto.setStatus("Unknown"); + return dto; + }) + .map(mapperCrop::toEntity) + .map(repositoryCrop::save) + .map(mapperCrop::toDTO) + .orElseThrow(() -> new Exception("El cultivo ya existe")); + } + + /** + * Obtiene todos los cultivos registrados en la base de datos y los convierte en objetos DTO. + * + * <p>Este método consulta todos los cultivos almacenados en la base de datos mediante el repositorio {@link RCrop}. + * Si la lista de cultivos está vacía, se lanza una excepción. Los cultivos obtenidos se mapean a objetos + * {@link CropDTO} utilizando el convertidor {@link MCrop}.</p> + * + * @return Una lista de objetos {@link CropDTO} que representan todos los cultivos en la base de datos. + * @throws Exception Si no existen cultivos registrados en la base de datos. + * @see CropDTO + * @see RCrop + * @see MCrop + */ + @Override + @Cacheable(value = "crops", key = "'all_crops'") + public List<CropDTO> getAllCrops() throws Exception { + return Optional.of(repositoryCrop.findAll()) + .filter(crops -> !crops.isEmpty()) + .map(crops -> crops.stream() + .map(mapperCrop::toDTO) + .collect(Collectors.toList())) + .orElseThrow(() -> new Exception("No existe ningún cultivo")); + } + + /** + * Obtiene un cultivo de la base de datos a partir de su identificador. + * + * <p>Este método busca un cultivo en la base de datos utilizando el ID proporcionado. + * Primero, valida que el ID sea válido utilizando el validador {@link VCropI}. + * Si el ID es válido, realiza una búsqueda en la base de datos usando el repositorio {@link RCrop}. + * Si el cultivo existe, se convierte a un objeto {@link CropDTO} utilizando el convertidor {@link MCrop}. + * Si el cultivo no existe o el ID no es válido, se lanza una excepción correspondiente.</p> + * + * @param id El identificador del cultivo que se desea obtener. El ID debe ser una cadena que representa un {@link ObjectId}. + * @return Un objeto {@link CropDTO} que representa el cultivo encontrado. + * @throws Exception Si el cultivo no existe en la base de datos o si el ID no es válido. + * @throws ValidationException Si el ID proporcionado no es válido según las reglas de validación del validador {@link VCropI}. + * @see CropDTO + * @see VCropI + * @see RCrop + * @see MCrop + */ + @Override + @Cacheable(value = "crops", key = "'id_'+#id") + public CropDTO getCropById(String id) throws Exception { + return Optional.of(id) + .map(ValidCropId -> { + validatorCrop.validateId(ValidCropId); + if (!validatorCrop.isValid()) { + throw new ValidationException(validatorCrop.getErrors().toString()); + } + validatorCrop.Reset(); + return new ObjectId(ValidCropId); + }) + .map(repositoryCrop::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .map(mapperCrop::toDTO) + .orElseThrow(() -> new Exception("El cultivo no existe")); + } + + /** + * Obtiene los cultivos asociados a un usuario específico mediante su ID. + * + * <p>Este método busca todos los cultivos relacionados con un usuario en la base de datos utilizando el + * ID del usuario. Primero, valida que el ID del usuario sea válido llamando al servicio de usuarios {@link SUserI}. + * Si el usuario existe, se realiza una búsqueda de los cultivos asociados a ese usuario a través del repositorio {@link RCrop}. + * Si se encuentran cultivos, se mapean a objetos {@link CropDTO} mediante el convertidor {@link MCrop}. + * Si el usuario no tiene cultivos o el ID del usuario es inválido, se lanza una excepción correspondiente.</p> + * + * @param id El identificador del usuario cuyos cultivos se desean obtener. El ID debe ser una cadena que representa un {@link ObjectId}. + * @return Una lista de objetos {@link CropDTO} que representan los cultivos asociados al usuario. + * @throws Exception Si el usuario no tiene cultivos o si el ID del usuario es inválido o no existe. + * @see CropDTO + * @see SUserI + * @see RCrop + * @see MCrop + */ + @Override + @Cacheable(value = "crops", key = "'user_'+#id") + public List<CropDTO> getCropsByUser(String id) throws Exception { + return Optional.of(serviceUser.getUserById(id)) + .map(validUser -> new ObjectId(validUser.getId())) + .map(repositoryCrop::findByUser) + .filter(crops -> !crops.isEmpty()) + .map(crops -> crops.stream() + .map(mapperCrop::toDTO) + .collect(Collectors.toList())) + .orElseThrow(() -> new Exception("No tiene ningún cultivo")); + } + + /** + * Cuenta la cantidad de cultivos asociados a un usuario específico. + * + * <p>Este método obtiene la lista de cultivos asociados a un usuario mediante su ID utilizando el método + * {@link #getCropsByUser(String)}, y luego cuenta la cantidad de cultivos encontrados. Si el usuario no tiene cultivos, + * el método devolverá un valor de 0.</p> + * + * @param id El identificador del usuario cuyos cultivos se desean contar. El ID debe ser una cadena que representa un {@link ObjectId}. + * @return El número de cultivos asociados al usuario especificado. + * @throws Exception Sí ocurre algún error al obtener los cultivos del usuario. + * @see #getCropsByUser(String) + */ + @Override + @Cacheable(value = "crops", key = "'count_user_'+#id") + public long countCropsByUser(String id) throws Exception { + return getCropsByUser(id).size(); + } + + /** + * Obtiene una lista de cultivos de la base de datos según el tipo proporcionado. + * + * <p>Este método busca cultivos en la base de datos utilizando el tipo de cultivo proporcionado como parámetro. + * Primero, valida que el tipo de cultivo sea válido mediante el validador {@link VCropI}. + * Si el tipo es válido, realiza una búsqueda en la base de datos usando el repositorio {@link RCrop}. + * Si se encuentran cultivos con el tipo proporcionado, se mapean a objetos {@link CropDTO} usando el convertidor {@link MCrop}. + * Si no se encuentran cultivos o si el tipo no es válido, se lanza una excepción correspondiente.</p> + * + * @param type El tipo de cultivo que se desea obtener. + * @return Una lista de objetos {@link CropDTO} que representan los cultivos encontrados con el tipo proporcionado. + * @throws Exception Si no se encuentran cultivos con el tipo proporcionado o si ocurre algún otro error. + * @throws ValidationException Si el tipo de cultivo proporcionado no es válido según las reglas de validación del validador {@link VCropI}. + * @see CropDTO + * @see VCropI + * @see RCrop + * @see MCrop + * @see SCrop + */ + @Override + @Cacheable(value = "crops", key = "'type_'+#type") + public List<CropDTO> getCropsByType(String type) throws Exception { + return Optional.of(type) + .map(ValidType -> { + validatorCrop.validateType(ValidType); + if (!validatorCrop.isValid()) { + throw new ValidationException(validatorCrop.getErrors().toString()); + } + validatorCrop.Reset(); + return ValidType; + }) + .map(repositoryCrop::findByType) + .filter(crops -> !crops.isEmpty()) + .map(crops -> crops.stream() + .map(mapperCrop::toDTO) + .collect(Collectors.toList())) + .orElseThrow(() -> new Exception("No existen cultivos")); + } + + /** + * Obtiene una lista de todos los tipos de cultivo registrados en el sistema. + * + * <p>Este método consulta todos los tipos de cultivo disponibles. Si no se encuentran tipos de cultivo, + * se lanza una excepción que indica que no existen tipos registrados.</p> + * + * @return Una lista de cadenas {@link String} que representan los nombres de los tipos de cultivo encontrados. + * @throws Exception Si ocurre un error al obtener los tipos de cultivo o si no se encuentran tipos registrados. + * @see String + * @see CropType + */ + @Override + @Cacheable(value = "crops", key = "'all_types'") + public List<String> getAllTypes() throws Exception { + return Optional.of( + Arrays.stream(CropType.values()) + .map(Enum::name) + .collect(Collectors.toList()) + ) + .filter(types -> !types.isEmpty()) + .orElseThrow(() -> new Exception("No existe ningún tipo de cultivo")); + } + + + /** + * Obtiene una lista de cultivos de la base de datos según el estado proporcionado. + * + * <p>Este método busca cultivos en la base de datos utilizando el estado de cultivo proporcionado como parámetro. + * Primero, valida que el estado sea válido mediante el validador {@link VCropI}. Si el estado es válido, + * realiza una búsqueda en la base de datos usando el repositorio {@link RCrop}. Si se encuentran cultivos con + * el estado proporcionado, se mapean a objetos {@link CropDTO} utilizando el convertidor {@link MCrop}. + * Si no se encuentran cultivos o si el estado no es válido, se lanza una excepción correspondiente.</p> + * + * @param status El estado del cultivo que se desea obtener. + * @return Una lista de objetos {@link CropDTO} que representan los cultivos encontrados con el estado proporcionado. + * @throws Exception Si no se encuentran cultivos con el estado proporcionado o si ocurre algún otro error. + * @throws ValidationException Si el estado proporcionado no es válido según las reglas de validación del validador {@link VCropI}. + * @see CropDTO + * @see VCropI + * @see RCrop + * @see MCrop + */ + @Override + @Cacheable(value = "crops", key = "'status_'+#status") + public List<CropDTO> getCropsByStatus(String status) throws Exception { + return Optional.of(status) + .map(ValidStatus -> { + validatorCrop.validateStatus(ValidStatus); + if (!validatorCrop.isValid()) { + throw new ValidationException(validatorCrop.getErrors().toString()); + } + validatorCrop.Reset(); + return ValidStatus; + }) + .map(repositoryCrop::findByStatus) + .filter(crops -> !crops.isEmpty()) + .map(crops -> crops.stream() + .map(mapperCrop::toDTO) + .collect(Collectors.toList())) + .orElseThrow(() -> new Exception("No existen cultivos")); + } + + /** + * Obtiene una lista de todos los estados de cultivo registrados en la base de datos. + * + * <p>Este método consulta los estados de cultivo disponibles en la base de datos. Si no se encuentran estados de cultivo, + * se lanza una excepción que indica que no existen estados registrados.</p> + * + * @return Una lista de cadenas {@link String} que representan los estados de cultivo encontrados. + * @throws Exception Si ocurre un error al buscar los estados de cultivo o si no se encuentran estados registrados. + * @see String + * @see CropStatus + */ + @Override + @Cacheable(value = "crops", key = "'all_status'") + public List<String> getAllStatus() throws Exception { + return Optional.of( + Arrays.stream(CropStatus.values()) + .map(Enum::name) + .collect(Collectors.toList()) + ) + .filter(status -> !status.isEmpty()) + .orElseThrow(() -> new Exception("No existe ningún estados para los cultivos")); + } + + /** + * Actualiza un cultivo existente en la base de datos con los nuevos datos proporcionados en un objeto {@link CropDTO}. + * + * <p>Este método recibe el ID del cultivo a actualizar y un objeto {@link CropDTO} con los nuevos datos. + * Primero, busca el cultivo existente usando el ID proporcionado. Luego, actualiza los campos que no sean + * nulos en el objeto {@link CropDTO}, manteniendo los valores existentes cuando el campo es nulo. Después, + * valida los nuevos datos utilizando el validador {@link VCropI}. Si las validaciones son correctas, el cultivo + * se actualiza en la base de datos. Finalmente, el método devuelve el objeto actualizado {@link CropDTO}.</p> + * + * <p>Si las validaciones no pasan o si ocurre un error durante el proceso de actualización, se lanza una + * {@link ValidationException} o una {@link Exception}, respectivamente.</p> + * + * @param id El ID único del cultivo que se desea actualizar. Este parámetro es obligatorio. + * @param updateCrop El objeto {@link CropDTO} que contiene los nuevos datos para actualizar el cultivo. + * Los campos nulos no modificarán el valor existente. + * @return El objeto {@link CropDTO} actualizado que representa el cultivo después de la actualización. + * @throws Exception Si ocurre un error al intentar actualizar el cultivo, como si el cultivo no existe. + * @throws ValidationException Si alguna de las validaciones del estado, tipo o usuario falla según el validador {@link VCropI}. + * @see CropDTO + * @see VCropI + * @see RCrop + * @see MCrop + */ + @Override + @CachePut(value = "crops", key = "'id_'+#id") + public CropDTO updatedCrop(String id, CropDTO updateCrop) throws Exception { + CropDTO existingCrop = getCropById(id); + return Optional.of(updateCrop) + .map(dto -> { + existingCrop.setType(dto.getType() != null ? dto.getType() : existingCrop.getType()); + existingCrop.setStatus(dto.getStatus() != null ? dto.getStatus() : existingCrop.getStatus()); + existingCrop.setUser(dto.getUser() != null ? dto.getUser() : existingCrop.getUser()); + return existingCrop; + }) + .map(dto -> { + validatorCrop.validateStatus(dto.getStatus()); + validatorCrop.validateType(dto.getType()); + try { + serviceUser.getUserById(existingCrop.getId()); + } catch (Exception e) { + throw new ValidationException(e.getMessage() + ", asocia el cultivo a un usuario existente"); + } + if (!validatorCrop.isValid()) { + throw new ValidationException(validatorCrop.getErrors().toString()); + } + + validatorCrop.Reset(); + return dto; + }) + .map(mapperCrop::toEntity) + .map(repositoryCrop::save) + .map(mapperCrop::toDTO) + .orElseThrow(() -> new Exception("El cultivo no se pudo actualizar")); + } + + + /** + * Elimina un cultivo de la base de datos según el ID proporcionado. + * + * <p>Este método recibe el ID de un cultivo y lo busca en la base de datos. Si el cultivo existe, + * se elimina de la base de datos utilizando el repositorio {@link RCrop}. Después de eliminarlo, + * se devuelve un mensaje confirmando la eliminación exitosa del cultivo. Si el cultivo no existe, + * se lanza una {@link Exception} indicando que el cultivo no fue encontrado.</p> + * + * @param id El ID único del cultivo que se desea eliminar. + * @return Un mensaje de confirmación que indica que el cultivo con el ID proporcionado fue eliminado correctamente. + * @throws Exception Si el cultivo no existe en la base de datos o si ocurre algún otro error durante la eliminación. + * @see RCrop + */ + @Override + @CacheEvict(value = "crops", key = "'id_'+#id") + public String deleteCrop(String id) throws Exception { + return Optional.of(getCropById(id)) + .map(user -> { + repositoryCrop.deleteById(new ObjectId(user.getId())); + return "El Cultivo con ID '" + id + "' fue eliminado."; + }) + .orElseThrow(() -> new Exception("El Cultivo no existe.")); + } +} + + diff --git a/src/main/java/smartpot/com/api/Crops/Service/SCropI.java b/src/main/java/smartpot/com/api/Crops/Service/SCropI.java new file mode 100644 index 0000000..f5d4b51 --- /dev/null +++ b/src/main/java/smartpot/com/api/Crops/Service/SCropI.java @@ -0,0 +1,31 @@ +package smartpot.com.api.Crops.Service; + +import smartpot.com.api.Crops.Model.DTO.CropDTO; + +import java.util.List; + +public interface SCropI { + + List<CropDTO> getAllCrops() throws Exception; + + CropDTO getCropById(String id) throws Exception; + + List<CropDTO> getCropsByUser(String id) throws Exception; + + long countCropsByUser(String id) throws Exception; + + List<CropDTO> getCropsByType(String type) throws Exception; + + List<String> getAllTypes() throws Exception; + + List<CropDTO> getCropsByStatus(String status) throws Exception; + + List<String> getAllStatus() throws Exception; + + CropDTO createCrop(CropDTO newCropDto) throws Exception; + + CropDTO updatedCrop(String id, CropDTO cropDto) throws Exception; + + String deleteCrop(String id) throws Exception; + +} diff --git a/src/main/java/smartpot/com/api/Crops/Validator/VCrop.java b/src/main/java/smartpot/com/api/Crops/Validator/VCrop.java new file mode 100644 index 0000000..67e2c31 --- /dev/null +++ b/src/main/java/smartpot/com/api/Crops/Validator/VCrop.java @@ -0,0 +1,108 @@ +package smartpot.com.api.Crops.Validator; + +import org.bson.types.ObjectId; +import org.springframework.stereotype.Component; +import smartpot.com.api.Crops.Model.Entity.CropStatus; +import smartpot.com.api.Crops.Model.Entity.CropType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class VCrop implements VCropI { + /** + * Indica si la validación fue exitosa. + */ + public boolean valid; + + /** + * Lista de errores de validación. + */ + public List<String> errors; + + /** + * Constructor de la clase de validación de usuario. + * Inicializa el estado de validación a "válido" y crea una lista vacía de errores. + */ + public VCrop() { + this.valid = true; + this.errors = new ArrayList<>(); + } + + /** + * Devuelve el estado de validez. + * + * @return <code>true</code> si el cultivo es válido, <code>false</code> si hay errores de validación. + */ + @Override + public boolean isValid() { + return valid; + } + + @Override + public void validateId(String id) { + if (id == null || id.isEmpty()) { + errors.add("El Id no puede estar vacío"); + valid = false; + } else if (!ObjectId.isValid(id)) { + errors.add("El Id debe ser un hexadecimal de 24 caracteres"); + valid = false; + } + } + + /** + * Obtiene la lista de errores encontrados durante la validación. + * + * @return Una lista de cadenas con los mensajes de error. + */ + @Override + public List<String> getErrors() { + List<String> currentErrors = errors; + Reset(); + return currentErrors; + } + + /** + * Resetea el estado de la validación, marcando el cultivo como válido y limpiando la lista de errores. + */ + @Override + public void Reset() { + valid = true; + errors = new ArrayList<>(); + } + + @Override + public void validateType(String type) { + if (type == null || type.isEmpty()) { + errors.add("El tipo de cultivo no puede estar vacío"); + valid = false; + } + + Set<String> validTypes = Arrays.stream(CropType.values()) + .map(Enum::name).collect(Collectors.toSet()); + + if (!validTypes.contains(type)) { + errors.add("El Tipo de cultivo debe ser uno de los siguientes: " + String.join(", ", validTypes)); + valid = false; + } + } + + @Override + public void validateStatus(String status) { + if (status == null || status.isEmpty()) { + errors.add("El Estado del cultivo no puede estar vacío"); + valid = false; + } + Set<String> validStatus = Arrays.stream(CropStatus.values()) + .map(Enum::name).collect(Collectors.toSet()); + + if (!validStatus.contains(status)) { + errors.add("El Estado del cultivo debe ser uno de los siguientes: " + String.join(", ", validStatus)); + valid = false; + } + } + +} diff --git a/src/main/java/smartpot/com/api/Crops/Validator/VCropI.java b/src/main/java/smartpot/com/api/Crops/Validator/VCropI.java new file mode 100644 index 0000000..697eee0 --- /dev/null +++ b/src/main/java/smartpot/com/api/Crops/Validator/VCropI.java @@ -0,0 +1,17 @@ +package smartpot.com.api.Crops.Validator; + +import java.util.List; + +public interface VCropI { + boolean isValid(); + + void validateId(String id); + + List<String> getErrors(); + + void Reset(); + + void validateType(String type); + + void validateStatus(String validStatus); +} diff --git a/src/main/java/smartpot/com/api/Documentation/SwaggerConfig.java b/src/main/java/smartpot/com/api/Documentation/SwaggerConfig.java index 7a01baa..50f0093 100644 --- a/src/main/java/smartpot/com/api/Documentation/SwaggerConfig.java +++ b/src/main/java/smartpot/com/api/Documentation/SwaggerConfig.java @@ -33,7 +33,7 @@ public OpenAPI customOpenAPI() { .description(description) .termsOfService("https://github.com/SmarPotTech/SmartPot-API/blob/main/LICENSE") .license(new License().name("MIT License").url("https://opensource.org/license/mit")) - .contact(new Contact().name(author).url("https://github.com/SmarPotTech")) + .contact(new Contact().name(author).url("https://github.com/SmarPotTech").email("smartpottech@gmail.com")) ); } } \ No newline at end of file diff --git a/src/main/java/smartpot/com/api/Mail/Config/AsyncConfig.java b/src/main/java/smartpot/com/api/Mail/Config/AsyncConfig.java new file mode 100644 index 0000000..33ec504 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Config/AsyncConfig.java @@ -0,0 +1,91 @@ +package smartpot.com.api.Mail.Config; + +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * Clase de configuración para habilitar y gestionar la ejecución asincrónica en la aplicación. + * + * <p>Esta clase habilita la ejecución de tareas asincrónicas mediante la anotación {@link Async} en la aplicación Spring. + * Además, configura un {@link ThreadPoolTaskExecutor} para gestionar la ejecución de tareas en un pool de hilos de manera + * eficiente. El pool de hilos permite controlar el número de hilos simultáneos, la capacidad de la cola de tareas y otras + * propiedades relacionadas con la gestión de hilos.</p> + * + * <p>Usar esta configuración es recomendable para aplicaciones de producción, donde es importante tener control sobre la + * ejecución de tareas asincrónicas para optimizar el uso de recursos y mejorar el rendimiento.</p> + * + * <p>Esta clase se activa con la anotación {@link EnableAsync}, lo que habilita el soporte de ejecución asincrónica en la + * aplicación Spring.</p> + * + * @see org.springframework.scheduling.annotation.Async + * @see ThreadPoolTaskExecutor + * @see TaskExecutor + */ +@Configuration +@EnableAsync +public class AsyncConfig { + + /** + * Crea un {@link ThreadPoolTaskExecutor} que gestiona las tareas asincrónicas en un pool de hilos. + * + * <p>Este método define un pool de hilos con un número mínimo y máximo de hilos, y configura la capacidad de la cola + * de tareas pendientes. El ejecutor se utiliza para ejecutar tareas anotadas con {@link Async} de manera eficiente.</p> + * + * <p>El {@link ThreadPoolTaskExecutor} asegura que las tareas asincrónicas no consuman recursos innecesarios al crear + * nuevos hilos de manera indiscriminada, y permite la reutilización de hilos para mejorar el rendimiento en aplicaciones + * de alto rendimiento.</p> + * + * <p>Los parámetros configurados en el {@link ThreadPoolTaskExecutor} son:</p> + * + * <ul> + * <li><b>corePoolSize</b>: El número mínimo de hilos que se deben mantener en el pool de hilos, incluso si están + * inactivos. Este valor asegura que siempre haya una cantidad mínima de hilos disponibles para ejecutar tareas + * sin la necesidad de crear hilos nuevos.</li> + * <li><b>maxPoolSize</b>: El número máximo de hilos que pueden ejecutarse simultáneamente en el pool. Cuando el número + * de tareas concurrentes es mayor que el número de hilos en el pool, se creará un nuevo hilo hasta que se alcance + * este límite.</li> + * <li><b>queueCapacity</b>: La capacidad de la cola de tareas pendientes. Cuando todos los hilos del pool están ocupados, + * las tareas adicionales se colocan en la cola. Si la cola se llena, y no se pueden crear más hilos, las tareas + * adicionales se rechazarán o esperarán hasta que haya espacio disponible.</li> + * <li><b>threadNamePrefix</b>: El prefijo utilizado para nombrar los hilos creados por el ejecutor. Esto ayuda a identificar + * fácilmente los hilos de ejecución asincrónica en los registros (logs) y durante la depuración.</li> + * </ul> + * + * @return El {@link TaskExecutor} configurado, utilizado por Spring para ejecutar tareas asincrónicas. + */ + @Bean + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(25); + executor.setThreadNamePrefix("Async-Executor-"); + executor.initialize(); + return executor; + } + + /** + * Método de cierre que se invoca al destruir la aplicación. + * Asegura que el {@link ThreadPoolTaskExecutor} se detenga correctamente, evitando fugas de memoria. + */ + @PreDestroy + public void shutdownExecutor() { + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) taskExecutor(); + if (executor != null) { + executor.shutdown(); + try { + if (!executor.getThreadPoolExecutor().awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) { + executor.getThreadPoolExecutor().shutdownNow(); + } + } catch (InterruptedException e) { + executor.getThreadPoolExecutor().shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/src/main/java/smartpot/com/api/Mail/Controller/EmailController.java b/src/main/java/smartpot/com/api/Mail/Controller/EmailController.java new file mode 100644 index 0000000..3531193 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Controller/EmailController.java @@ -0,0 +1,75 @@ +package smartpot.com.api.Mail.Controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import smartpot.com.api.Mail.Model.DTO.EmailDTO; +import smartpot.com.api.Mail.Service.EmailServiceI; +import smartpot.com.api.Responses.ErrorResponse; + +@RestController +@RequestMapping("/Emails") +@Tag(name = "Correos", description = "Operaciones relacionadas con correos") +public class EmailController { + private final EmailServiceI emailServiceI; + + /** + * Constructor del controlador {@link EmailController}. + * <p>Se utiliza la inyección de dependencias para asignar el servicio {@link EmailServiceI} que gestionará las operaciones + * relacionadas con los correos.</p> + * + * @param emailServiceI El servicio que contiene la lógica de negocio para manejar correos. + * @throws NullPointerException Si el servicio proporcionado es {@code null}. + * @see EmailServiceI + */ + @Autowired + public EmailController(EmailServiceI emailServiceI) { + this.emailServiceI = emailServiceI; + } + + /** + * Recupera todos los correos registrados en el sistema. + * <p>Este método devuelve una lista con todos los correos que están registrados en el sistema.</p> + * <p>Si no se encuentran coreos, se devolverá una lista vacía con el código HTTP 200.</p> + * <p>En caso de error (por ejemplo, problemas con la conexión a la base de datos o un fallo en el servicio), + * se devolverá un mensaje de error con el código HTTP 404.</p> + * + * @return Un objeto {@link ResponseEntity} que contiene una lista de todos los correos registrados (código HTTP 200). + * En caso de error, se devolverá un mensaje de error con el código HTTP 404. + * + * <p><b>Respuestas posibles:</b></p> + * <ul> + * <li><b>200 OK</b>: Se retorna una lista de objetos {@link EmailDTO} con la información de todos los correos registrados.<br></li> + * <li><b>404 Not Found</b>: No se encontraron correos registrados o hubo un error al recuperar los datos.<br></li> + * </ul> + */ + @GetMapping("/All") + @Operation(summary = "Obtener todos los correos", + description = "Recupera todos los correos registrados en el sistema. " + + "En caso de no haber correos, se devolverá una excepción.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(description = "Correos encontrados", + responseCode = "200", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = EmailDTO.class)))), + @ApiResponse(responseCode = "404", + description = "No se encontraron correos registrados.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> getAllCrops() { + try { + return new ResponseEntity<>(emailServiceI.getAllMails(), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse(e.getMessage(), HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + } + } +} diff --git a/src/main/java/smartpot/com/api/Mail/Mapper/EmailMapper.java b/src/main/java/smartpot/com/api/Mail/Mapper/EmailMapper.java new file mode 100644 index 0000000..bdba046 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Mapper/EmailMapper.java @@ -0,0 +1,26 @@ +package smartpot.com.api.Mail.Mapper; + +import org.bson.types.ObjectId; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import smartpot.com.api.Mail.Model.DTO.EmailDTO; +import smartpot.com.api.Mail.Model.Entity.EmailDetails; + +@Mapper(componentModel = "spring") +public interface EmailMapper { + @Mapping(source = "id", target = "id", qualifiedByName = "stringToObjectId") + EmailDetails toEntity(EmailDTO emailDTO); + + @Mapping(source = "id", target = "id", qualifiedByName = "objectIdToString") + EmailDTO toDTO(EmailDetails emailDetails); + + @org.mapstruct.Named("objectIdToString") + default String objectIdToString(ObjectId objectId) { + return objectId != null ? objectId.toHexString() : null; + } + + @org.mapstruct.Named("stringToObjectId") + default ObjectId stringToObjectId(String id) { + return id != null ? new ObjectId(id) : null; + } +} diff --git a/src/main/java/smartpot/com/api/Mail/Model/DTO/EmailDTO.java b/src/main/java/smartpot/com/api/Mail/Model/DTO/EmailDTO.java new file mode 100644 index 0000000..43c81e9 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Model/DTO/EmailDTO.java @@ -0,0 +1,18 @@ +package smartpot.com.api.Mail.Model.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +public class EmailDTO implements Serializable { + private String id; + private String recipient; + private String msgBody; + private String subject; + private String attachment; +} diff --git a/src/main/java/smartpot/com/api/Mail/Model/Entity/EmailDetails.java b/src/main/java/smartpot/com/api/Mail/Model/Entity/EmailDetails.java new file mode 100644 index 0000000..eb7fc2b --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Model/Entity/EmailDetails.java @@ -0,0 +1,33 @@ +package smartpot.com.api.Mail.Model.Entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Document(collection = "correos") +public class EmailDetails implements Serializable { + @Id + @Field("_id") + private ObjectId id; + + @Field("recipient") + private String recipient; + + @Field("msgBody") + private String msgBody; + + @Field("subject") + private String subject; + + @Field("attachment") + private String attachment; +} diff --git a/src/main/java/smartpot/com/api/Mail/Repository/EmailRepository.java b/src/main/java/smartpot/com/api/Mail/Repository/EmailRepository.java new file mode 100644 index 0000000..40cb3f3 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Repository/EmailRepository.java @@ -0,0 +1,10 @@ +package smartpot.com.api.Mail.Repository; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; +import smartpot.com.api.Mail.Model.Entity.EmailDetails; + +@Repository +public interface EmailRepository extends MongoRepository<EmailDetails, String> { + +} diff --git a/src/main/java/smartpot/com/api/Mail/Service/EmailService.java b/src/main/java/smartpot/com/api/Mail/Service/EmailService.java new file mode 100644 index 0000000..5b93d44 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Service/EmailService.java @@ -0,0 +1,102 @@ +package smartpot.com.api.Mail.Service; + +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import smartpot.com.api.Mail.Mapper.EmailMapper; +import smartpot.com.api.Mail.Model.DTO.EmailDTO; +import smartpot.com.api.Mail.Model.Entity.EmailDetails; +import smartpot.com.api.Mail.Repository.EmailRepository; + +import java.io.File; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class EmailService implements EmailServiceI { + private final JavaMailSender javaMailSender; + private final EmailRepository emailRepository; + private final EmailMapper emailMapper; + @Value("${spring.mail.username}") + private String sender; + + @Autowired + public EmailService(JavaMailSender javaMailSender, EmailRepository emailRepository, EmailMapper emailMapper) { + this.javaMailSender = javaMailSender; + this.emailRepository = emailRepository; + this.emailMapper = emailMapper; + } + + @Override + @Async + public void sendSimpleMail(EmailDetails details) { + try { + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setFrom(sender); + mailMessage.setTo(details.getRecipient()); + mailMessage.setText(details.getMsgBody()); + mailMessage.setSubject(details.getSubject()); + javaMailSender.send(mailMessage); + emailRepository.save(details); + log.warn("Correo Enviado Exitosamente"); + } catch (Exception e) { + log.error("Error al Enviar Correo " + e.getMessage()); + } + } + + @Override + @Async + public void sendMailWithAttachment(EmailDetails details) { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper mimeMessageHelper; + try { + mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); + mimeMessageHelper.setFrom(sender); + mimeMessageHelper.setTo(details.getRecipient()); + mimeMessageHelper.setText(details.getMsgBody()); + mimeMessageHelper.setSubject(details.getSubject()); + FileSystemResource file = new FileSystemResource(new File(details.getAttachment())); + mimeMessageHelper.addAttachment(Objects.requireNonNull(file.getFilename()), file); + javaMailSender.send(mimeMessage); + emailRepository.save(details); + log.warn("Correo Enviado Exitosamente"); + } catch (Exception e) { + log.error("Error al Enviar Correo " + e.getMessage()); + } + } + + /** + * Obtiene todos los correos registrados en la base de datos y los convierte en objetos DTO. + * + * <p>Este método consulta todos los correos almacenados en la base de datos mediante el repositorio {@link EmailRepository}. + * Si la lista de correos está vacía, se lanza una excepción. Los correos obtenidos se mapean a objetos + * {@link EmailDTO} utilizando el convertidor {@link EmailMapper}.</p> + * + * @return Una lista de objetos {@link EmailDTO} que representan todos los correos en la base de datos. + * @throws Exception Si no existen correos registrados en la base de datos. + * @see EmailDTO + * @see EmailRepository + * @see EmailMapper + */ + @Override + @Cacheable(value = "mails", key = "'all_mails'") + public List<EmailDTO> getAllMails() throws Exception { + return Optional.of(emailRepository.findAll()) + .filter(emails -> !emails.isEmpty()) + .map(emails -> emails.stream() + .map(emailMapper::toDTO) + .collect(Collectors.toList())) + .orElseThrow(() -> new Exception("No existe ningún correo")); + } +} diff --git a/src/main/java/smartpot/com/api/Mail/Service/EmailServiceI.java b/src/main/java/smartpot/com/api/Mail/Service/EmailServiceI.java new file mode 100644 index 0000000..d1cf5b2 --- /dev/null +++ b/src/main/java/smartpot/com/api/Mail/Service/EmailServiceI.java @@ -0,0 +1,15 @@ +package smartpot.com.api.Mail.Service; + +import smartpot.com.api.Mail.Model.DTO.EmailDTO; +import smartpot.com.api.Mail.Model.Entity.EmailDetails; + +import java.util.List; + +public interface EmailServiceI { + + void sendSimpleMail(EmailDetails details); + + void sendMailWithAttachment(EmailDetails details); + + List<EmailDTO> getAllMails() throws Exception; +} diff --git a/src/main/java/smartpot/com/api/Notifications/Controller/NotificationController.java b/src/main/java/smartpot/com/api/Notifications/Controller/NotificationController.java index 37a6acb..6919073 100644 --- a/src/main/java/smartpot/com/api/Notifications/Controller/NotificationController.java +++ b/src/main/java/smartpot/com/api/Notifications/Controller/NotificationController.java @@ -2,8 +2,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import smartpot.com.api.Notifications.Model.DAO.Service.SNotificationI; import smartpot.com.api.Notifications.Model.Entity.Notification; +import smartpot.com.api.Notifications.Service.SNotificationI; import java.util.List; diff --git a/src/main/java/smartpot/com/api/Notifications/Model/DAO/Repository/RNotification.java b/src/main/java/smartpot/com/api/Notifications/Repository/RNotification.java similarity index 95% rename from src/main/java/smartpot/com/api/Notifications/Model/DAO/Repository/RNotification.java rename to src/main/java/smartpot/com/api/Notifications/Repository/RNotification.java index c5abe4e..00cd61b 100644 --- a/src/main/java/smartpot/com/api/Notifications/Model/DAO/Repository/RNotification.java +++ b/src/main/java/smartpot/com/api/Notifications/Repository/RNotification.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Notifications.Model.DAO.Repository; +package smartpot.com.api.Notifications.Repository; import org.bson.types.ObjectId; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/src/main/java/smartpot/com/api/Notifications/Model/DAO/Service/SNotification.java b/src/main/java/smartpot/com/api/Notifications/Service/SNotification.java similarity index 96% rename from src/main/java/smartpot/com/api/Notifications/Model/DAO/Service/SNotification.java rename to src/main/java/smartpot/com/api/Notifications/Service/SNotification.java index 3de3759..ed3eb83 100644 --- a/src/main/java/smartpot/com/api/Notifications/Model/DAO/Service/SNotification.java +++ b/src/main/java/smartpot/com/api/Notifications/Service/SNotification.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Notifications.Model.DAO.Service; +package smartpot.com.api.Notifications.Service; import lombok.Builder; import lombok.Data; @@ -8,8 +8,8 @@ import org.springframework.stereotype.Service; import smartpot.com.api.Exception.ApiException; import smartpot.com.api.Exception.ApiResponse; -import smartpot.com.api.Notifications.Model.DAO.Repository.RNotification; import smartpot.com.api.Notifications.Model.Entity.Notification; +import smartpot.com.api.Notifications.Repository.RNotification; import java.util.List; diff --git a/src/main/java/smartpot/com/api/Notifications/Model/DAO/Service/SNotificationI.java b/src/main/java/smartpot/com/api/Notifications/Service/SNotificationI.java similarity index 89% rename from src/main/java/smartpot/com/api/Notifications/Model/DAO/Service/SNotificationI.java rename to src/main/java/smartpot/com/api/Notifications/Service/SNotificationI.java index 6fcea5f..46a4115 100644 --- a/src/main/java/smartpot/com/api/Notifications/Model/DAO/Service/SNotificationI.java +++ b/src/main/java/smartpot/com/api/Notifications/Service/SNotificationI.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Notifications.Model.DAO.Service; +package smartpot.com.api.Notifications.Service; import smartpot.com.api.Notifications.Model.Entity.Notification; diff --git a/src/main/java/smartpot/com/api/Records/Controller/HistoryController.java b/src/main/java/smartpot/com/api/Records/Controller/HistoryController.java index bfd643b..964982e 100644 --- a/src/main/java/smartpot/com/api/Records/Controller/HistoryController.java +++ b/src/main/java/smartpot/com/api/Records/Controller/HistoryController.java @@ -5,11 +5,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import smartpot.com.api.Exception.ApiResponse; -import smartpot.com.api.Responses.ErrorResponse; -import smartpot.com.api.Records.Model.DAO.Service.SHistoryI; import smartpot.com.api.Records.Model.DTO.RecordDTO; import smartpot.com.api.Records.Model.Entity.DateRange; import smartpot.com.api.Records.Model.Entity.History; +import smartpot.com.api.Records.Service.SHistoryI; +import smartpot.com.api.Responses.ErrorResponse; import java.util.List; @@ -42,7 +42,7 @@ public List<History> getAllHistories() { */ @PostMapping("/Create") @ResponseStatus(HttpStatus.CREATED) - public History createHistory(@RequestBody RecordDTO newHistory) { + public History createHistory(@RequestBody RecordDTO newHistory) throws Exception { return serviceHistory.Createhistory(newHistory); } @@ -53,7 +53,7 @@ public History createHistory(@RequestBody RecordDTO newHistory) { * @return Los históricos encontrado */ @GetMapping("/crop/{id}") - public List<History> getByCrop(@PathVariable String id) { + public List<History> getByCrop(@PathVariable String id) throws Exception { return serviceHistory.getByCrop(id); } diff --git a/src/main/java/smartpot/com/api/Records/Model/DTO/CropRecordDTO.java b/src/main/java/smartpot/com/api/Records/Model/DTO/CropRecordDTO.java index a6d7983..bb803a5 100644 --- a/src/main/java/smartpot/com/api/Records/Model/DTO/CropRecordDTO.java +++ b/src/main/java/smartpot/com/api/Records/Model/DTO/CropRecordDTO.java @@ -1,7 +1,7 @@ package smartpot.com.api.Records.Model.DTO; import lombok.Data; -import smartpot.com.api.Crops.Model.Entity.Crop; +import smartpot.com.api.Crops.Model.DTO.CropDTO; import smartpot.com.api.Records.Model.Entity.History; import smartpot.com.api.Records.Model.Entity.Measures; @@ -14,7 +14,7 @@ public class CropRecordDTO { private String date; private MeasuresDTO measures; - public CropRecordDTO(Crop crop, History history) { + public CropRecordDTO(CropDTO crop, History history) { this.crop = String.valueOf(crop.getId()); this.status = String.valueOf(crop.getStatus()); this.type = String.valueOf(crop.getType()); diff --git a/src/main/java/smartpot/com/api/Records/Model/DAO/Repository/RHistory.java b/src/main/java/smartpot/com/api/Records/Repository/RHistory.java similarity index 94% rename from src/main/java/smartpot/com/api/Records/Model/DAO/Repository/RHistory.java rename to src/main/java/smartpot/com/api/Records/Repository/RHistory.java index 404292f..ea039ec 100644 --- a/src/main/java/smartpot/com/api/Records/Model/DAO/Repository/RHistory.java +++ b/src/main/java/smartpot/com/api/Records/Repository/RHistory.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Records.Model.DAO.Repository; +package smartpot.com.api.Records.Repository; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PastOrPresent; diff --git a/src/main/java/smartpot/com/api/Records/Model/DAO/Service/SHistory.java b/src/main/java/smartpot/com/api/Records/Service/SHistory.java similarity index 95% rename from src/main/java/smartpot/com/api/Records/Model/DAO/Service/SHistory.java rename to src/main/java/smartpot/com/api/Records/Service/SHistory.java index c323f58..b6ed77e 100644 --- a/src/main/java/smartpot/com/api/Records/Model/DAO/Service/SHistory.java +++ b/src/main/java/smartpot/com/api/Records/Service/SHistory.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Records.Model.DAO.Service; +package smartpot.com.api.Records.Service; import lombok.Builder; import lombok.Data; @@ -8,17 +8,17 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import smartpot.com.api.Crops.Model.DAO.Service.SCropI; -import smartpot.com.api.Crops.Model.Entity.Crop; +import smartpot.com.api.Crops.Model.DTO.CropDTO; +import smartpot.com.api.Crops.Service.SCropI; import smartpot.com.api.Exception.ApiException; import smartpot.com.api.Exception.ApiResponse; import smartpot.com.api.Records.Mapper.MRecords; -import smartpot.com.api.Records.Model.DAO.Repository.RHistory; import smartpot.com.api.Records.Model.DTO.CropRecordDTO; import smartpot.com.api.Records.Model.DTO.MeasuresDTO; import smartpot.com.api.Records.Model.DTO.RecordDTO; import smartpot.com.api.Records.Model.Entity.DateRange; import smartpot.com.api.Records.Model.Entity.History; +import smartpot.com.api.Records.Repository.RHistory; import java.util.ArrayList; import java.util.List; @@ -212,8 +212,8 @@ public History getHistoryById(String id) { * @throws ApiException Si el cultivo con el ID proporcionado no se encuentra o si ocurre un error en la consulta. */ @Override - public List<History> getByCrop(String cropId) { - return repositoryHistory.getHistoriesByCrop(serviceCrop.getCropById(cropId).getId()); + public List<History> getByCrop(String cropId) throws Exception { + return repositoryHistory.getHistoriesByCrop(new ObjectId(serviceCrop.getCropById(cropId).getId())); } /** @@ -262,7 +262,7 @@ public List<History> getHistoriesByCropAndDateBetween(String cropId, DateRange r @Override public List<CropRecordDTO> getByUser(String id) throws Exception { List<CropRecordDTO> records = new ArrayList<>(); - List<Crop> crops = serviceCrop.getCropsByUser(id); + List<CropDTO> crops = serviceCrop.getCropsByUser(id); // Verificar si el usuario tiene cultivos if (crops.isEmpty()) { throw new ApiException(new ApiResponse( @@ -270,8 +270,8 @@ public List<CropRecordDTO> getByUser(String id) throws Exception { HttpStatus.NOT_FOUND.value() )); } - for (Crop crop : crops) { - List<History> histories = repositoryHistory.getHistoriesByCrop(crop.getId()); + for (CropDTO crop : crops) { + List<History> histories = repositoryHistory.getHistoriesByCrop(new ObjectId(crop.getId())); for (History history : histories) { records.add(new CropRecordDTO(crop, history)); @@ -288,7 +288,7 @@ public List<CropRecordDTO> getByUser(String id) throws Exception { * @return El histórico creado */ @Override - public History Createhistory(RecordDTO recordDTO) { + public History Createhistory(RecordDTO recordDTO) throws Exception { ValidationMesuares(recordDTO.getMeasures()); serviceCrop.getCropById(recordDTO.getCrop()); History history = MRecords.INSTANCE.toEntity(recordDTO); diff --git a/src/main/java/smartpot/com/api/Records/Model/DAO/Service/SHistoryI.java b/src/main/java/smartpot/com/api/Records/Service/SHistoryI.java similarity index 81% rename from src/main/java/smartpot/com/api/Records/Model/DAO/Service/SHistoryI.java rename to src/main/java/smartpot/com/api/Records/Service/SHistoryI.java index 2f20e0e..1ba12e4 100644 --- a/src/main/java/smartpot/com/api/Records/Model/DAO/Service/SHistoryI.java +++ b/src/main/java/smartpot/com/api/Records/Service/SHistoryI.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Records.Model.DAO.Service; +package smartpot.com.api.Records.Service; import org.springframework.http.ResponseEntity; import smartpot.com.api.Exception.ApiResponse; @@ -14,13 +14,13 @@ public interface SHistoryI { History getHistoryById(String id); - List<History> getByCrop(String cropId); + List<History> getByCrop(String cropId) throws Exception; List<History> getHistoriesByCropAndDateBetween(String cropId, DateRange ranges); List<CropRecordDTO> getByUser(String id) throws Exception; - History Createhistory(RecordDTO recordDTO); + History Createhistory(RecordDTO recordDTO) throws Exception; History updatedHistory(History existingHistory, RecordDTO updateHistory); diff --git a/src/main/java/smartpot/com/api/Security/Filter/JwtAuthFilter.java b/src/main/java/smartpot/com/api/Security/Config/Filters/JwtAuthFilter.java similarity index 92% rename from src/main/java/smartpot/com/api/Security/Filter/JwtAuthFilter.java rename to src/main/java/smartpot/com/api/Security/Config/Filters/JwtAuthFilter.java index 1b5012d..9308d00 100644 --- a/src/main/java/smartpot/com/api/Security/Filter/JwtAuthFilter.java +++ b/src/main/java/smartpot/com/api/Security/Config/Filters/JwtAuthFilter.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Security.Filter; +package smartpot.com.api.Security.Config.Filters; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -11,8 +11,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import smartpot.com.api.Security.Service.JwtService; -import smartpot.com.api.Users.Model.DAO.Service.SUser; import smartpot.com.api.Users.Model.DTO.UserDTO; +import smartpot.com.api.Users.Service.SUser; import java.io.IOException; @@ -43,7 +43,8 @@ protected void doFilterInternal( user, user.getPassword(), null /* user.getAuthorities() */); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } filterChain.doFilter(request, response); } } \ No newline at end of file diff --git a/src/main/java/smartpot/com/api/Security/Config/Filters/RateLimitingFilter.java b/src/main/java/smartpot/com/api/Security/Config/Filters/RateLimitingFilter.java new file mode 100644 index 0000000..fc38844 --- /dev/null +++ b/src/main/java/smartpot/com/api/Security/Config/Filters/RateLimitingFilter.java @@ -0,0 +1,79 @@ +package smartpot.com.api.Security.Config.Filters; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class RateLimitingFilter implements Filter { + + private final Map<String, AtomicInteger> requestsCount = new ConcurrentHashMap<>(); + + @Value("${rate.limiting.max-requests}") + private int MAX_REQUESTS; + + @Value("${rate.limiting.time-window}") + private long TIME_WINDOW; + + @Value("${rate.limiting.public-routes}") + private String publicRoutes; + + private long windowStart = System.currentTimeMillis(); + + private List<String> publicRoutesList; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Convertir el string de rutas públicas en una lista + if (publicRoutes != null && !publicRoutes.isEmpty()) { + publicRoutesList = Arrays.asList(publicRoutes.split(",")); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + + if (isPublicRoute(httpRequest.getRequestURI())) { + chain.doFilter(request, response); + return; + } + + String clientIP = request.getRemoteAddr(); + long currentTime = System.currentTimeMillis(); + + if (currentTime - windowStart > TIME_WINDOW) { + windowStart = currentTime; + requestsCount.clear(); + } + + requestsCount.putIfAbsent(clientIP, new AtomicInteger(0)); + int currentCount = requestsCount.get(clientIP).incrementAndGet(); + + if (currentCount >= MAX_REQUESTS) { + ((HttpServletResponse) response).setStatus(429); // Too Many Requests + response.getWriter().write("Haz enviado demasiadas solicitudes, intenta de nuevo más tarde"); + return; + } + + chain.doFilter(request, response); + } + + private boolean isPublicRoute(String uri) { + for (String route : publicRoutesList) { + if (uri.startsWith(route)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/smartpot/com/api/Security/Config/SecurityConfiguration.java b/src/main/java/smartpot/com/api/Security/Config/SecurityConfiguration.java index 4074b22..2f4d12b 100644 --- a/src/main/java/smartpot/com/api/Security/Config/SecurityConfiguration.java +++ b/src/main/java/smartpot/com/api/Security/Config/SecurityConfiguration.java @@ -16,9 +16,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import smartpot.com.api.Security.Filter.JwtAuthFilter; -import smartpot.com.api.Security.Headers.CorsConfig; -import smartpot.com.api.Users.Model.DAO.Service.SUser; +import smartpot.com.api.Security.Config.headers.CorsConfig; +import smartpot.com.api.Security.Config.Filters.JwtAuthFilter; +import smartpot.com.api.Users.Service.SUser; import java.util.Arrays; import java.util.List; @@ -51,7 +51,6 @@ public SecurityConfiguration(CorsConfig corsConfig, JwtAuthFilter jwtAuthFilter, @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSec) throws Exception { - // Public Routes List<String> publicRoutesList; if (publicRoutes.contains(",")) { publicRoutesList = Arrays.asList(publicRoutes.split(",")); diff --git a/src/main/java/smartpot/com/api/Security/Headers/CorsConfig.java b/src/main/java/smartpot/com/api/Security/Config/headers/CorsConfig.java similarity index 95% rename from src/main/java/smartpot/com/api/Security/Headers/CorsConfig.java rename to src/main/java/smartpot/com/api/Security/Config/headers/CorsConfig.java index 3b23b90..9cc324c 100644 --- a/src/main/java/smartpot/com/api/Security/Headers/CorsConfig.java +++ b/src/main/java/smartpot/com/api/Security/Config/headers/CorsConfig.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Security.Headers; +package smartpot.com.api.Security.Config.headers; import jakarta.servlet.http.HttpServletRequest; import lombok.NonNull; diff --git a/src/main/java/smartpot/com/api/Security/Controller/AuthController.java b/src/main/java/smartpot/com/api/Security/Controller/AuthController.java index f7d990c..2720cff 100644 --- a/src/main/java/smartpot/com/api/Security/Controller/AuthController.java +++ b/src/main/java/smartpot/com/api/Security/Controller/AuthController.java @@ -8,13 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; import smartpot.com.api.Exception.InvalidTokenException; import smartpot.com.api.Responses.ErrorResponse; import smartpot.com.api.Responses.TokenResponse; import smartpot.com.api.Security.Service.JwtServiceI; -import smartpot.com.api.Users.Model.DAO.Service.SUserI; import smartpot.com.api.Users.Model.DTO.UserDTO; @RestController @@ -22,12 +20,10 @@ @Tag(name = "Autentificación", description = "Operaciones relacionadas con autentificación de usuarios") public class AuthController { - private final SUserI serviceUser; private final JwtServiceI jwtService; @Autowired - public AuthController(final SUserI serviceUser, final JwtServiceI jwtService) { - this.serviceUser = serviceUser; + public AuthController(JwtServiceI jwtService) { this.jwtService = jwtService; } @@ -83,10 +79,9 @@ public ResponseEntity<?> login(@RequestBody UserDTO reqUser) { public ResponseEntity<?> verify(@RequestHeader("Authorization") String authHeader) { try { return new ResponseEntity<>(jwtService.validateAuthHeader(authHeader), HttpStatus.OK); - }catch (InvalidTokenException e) { + } catch (InvalidTokenException e) { return new ResponseEntity<>(new ErrorResponse("Token Invalido [" + e.getMessage() + "]", HttpStatus.I_AM_A_TEAPOT.value()), HttpStatus.I_AM_A_TEAPOT); - } - catch (Exception e) { + } catch (Exception e) { return new ResponseEntity<>(new ErrorResponse("Error al verificar token [" + e.getMessage() + "]", HttpStatus.BAD_REQUEST.value()), HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/smartpot/com/api/Security/Service/JwtService.java b/src/main/java/smartpot/com/api/Security/Service/JwtService.java index bfc6e74..b086689 100644 --- a/src/main/java/smartpot/com/api/Security/Service/JwtService.java +++ b/src/main/java/smartpot/com/api/Security/Service/JwtService.java @@ -6,15 +6,15 @@ import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import smartpot.com.api.Exception.InvalidTokenException; -import smartpot.com.api.Responses.ErrorResponse; -import smartpot.com.api.Users.Model.DAO.Service.SUserI; +import smartpot.com.api.Mail.Model.Entity.EmailDetails; +import smartpot.com.api.Mail.Service.EmailServiceI; import smartpot.com.api.Users.Model.DTO.UserDTO; +import smartpot.com.api.Users.Service.SUserI; + import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; @@ -24,29 +24,40 @@ @Service public class JwtService implements JwtServiceI { + private final SUserI serviceUser; + private final EmailServiceI emailService; @Value("${application.security.jwt.secret-key}") private String secretKey; - @Value("${application.security.jwt.expiration}") private long expiration; - private final SUserI serviceUser; - /** * Constructor que inyecta las dependencias del servicio. * * @param serviceUser servicio que maneja las operaciones de base de datos. */ @Autowired - public JwtService(SUserI serviceUser) { + public JwtService(SUserI serviceUser, EmailServiceI emailService) { this.serviceUser = serviceUser; + this.emailService = emailService; } @Override public String Login(UserDTO reqUser) throws Exception { return Optional.of(serviceUser.getUserByEmail(reqUser.getEmail())) - .filter( userDTO -> new BCryptPasswordEncoder().matches(reqUser.getPassword(), userDTO.getPassword())) + .filter(userDTO -> new BCryptPasswordEncoder().matches(reqUser.getPassword(), userDTO.getPassword())) .map(validUser -> generateToken(validUser.getId(), validUser.getEmail())) + .map(validToken -> { + emailService.sendSimpleMail( + new EmailDetails( + null, + "smartpottech@gmail.com", + "Se ha iniciado sesion en su cuenta, verifique su token de seguridad '" + validToken + "'", + "Inicio de Sesion en Smartpot", + "" + )); + return validToken; + }) .orElseThrow(() -> new Exception("Credenciales Invalidas")); } @@ -63,7 +74,7 @@ private String generateToken(String id, String email) { } @Override - public UserDTO validateAuthHeader(String authHeader) throws Exception, InvalidTokenException { + public UserDTO validateAuthHeader(String authHeader) throws Exception { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new Exception("El encabezado de autorización es inválido. Se esperaba 'Bearer <token>'."); } diff --git a/src/main/java/smartpot/com/api/Security/Service/JwtServiceI.java b/src/main/java/smartpot/com/api/Security/Service/JwtServiceI.java index 8c39acb..5b815bc 100644 --- a/src/main/java/smartpot/com/api/Security/Service/JwtServiceI.java +++ b/src/main/java/smartpot/com/api/Security/Service/JwtServiceI.java @@ -1,10 +1,7 @@ package smartpot.com.api.Security.Service; -import org.springframework.security.core.userdetails.UserDetails; import smartpot.com.api.Users.Model.DTO.UserDTO; -import java.util.Date; - public interface JwtServiceI { String Login(UserDTO reqUser) throws Exception; diff --git a/src/main/java/smartpot/com/api/Sessions/Controller/SessionController.java b/src/main/java/smartpot/com/api/Sessions/Controller/SessionController.java index d707d8f..68b1102 100644 --- a/src/main/java/smartpot/com/api/Sessions/Controller/SessionController.java +++ b/src/main/java/smartpot/com/api/Sessions/Controller/SessionController.java @@ -5,8 +5,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import smartpot.com.api.Responses.ErrorResponse; -import smartpot.com.api.Sessions.Model.DAO.Service.SSessionI; import smartpot.com.api.Sessions.Model.Entity.Session; +import smartpot.com.api.Sessions.Service.SSessionI; import java.util.List; diff --git a/src/main/java/smartpot/com/api/Sessions/Model/DAO/Repository/RSession.java b/src/main/java/smartpot/com/api/Sessions/Repository/RSession.java similarity index 94% rename from src/main/java/smartpot/com/api/Sessions/Model/DAO/Repository/RSession.java rename to src/main/java/smartpot/com/api/Sessions/Repository/RSession.java index 495df7d..02a57e2 100644 --- a/src/main/java/smartpot/com/api/Sessions/Model/DAO/Repository/RSession.java +++ b/src/main/java/smartpot/com/api/Sessions/Repository/RSession.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Sessions.Model.DAO.Repository; +package smartpot.com.api.Sessions.Repository; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; diff --git a/src/main/java/smartpot/com/api/Sessions/Model/DAO/Service/SSession.java b/src/main/java/smartpot/com/api/Sessions/Service/SSession.java similarity index 97% rename from src/main/java/smartpot/com/api/Sessions/Model/DAO/Service/SSession.java rename to src/main/java/smartpot/com/api/Sessions/Service/SSession.java index af88b24..7e4388f 100644 --- a/src/main/java/smartpot/com/api/Sessions/Model/DAO/Service/SSession.java +++ b/src/main/java/smartpot/com/api/Sessions/Service/SSession.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Sessions.Model.DAO.Service; +package smartpot.com.api.Sessions.Service; import lombok.Builder; import lombok.Data; @@ -8,10 +8,10 @@ import org.springframework.stereotype.Service; import smartpot.com.api.Exception.ApiException; import smartpot.com.api.Exception.ApiResponse; -import smartpot.com.api.Sessions.Model.DAO.Repository.RSession; import smartpot.com.api.Sessions.Model.Entity.Session; -import smartpot.com.api.Users.Model.DAO.Service.SUserI; +import smartpot.com.api.Sessions.Repository.RSession; import smartpot.com.api.Users.Model.DTO.UserDTO; +import smartpot.com.api.Users.Service.SUserI; import java.util.Date; import java.util.List; diff --git a/src/main/java/smartpot/com/api/Sessions/Model/DAO/Service/SSessionI.java b/src/main/java/smartpot/com/api/Sessions/Service/SSessionI.java similarity index 90% rename from src/main/java/smartpot/com/api/Sessions/Model/DAO/Service/SSessionI.java rename to src/main/java/smartpot/com/api/Sessions/Service/SSessionI.java index 7204362..ddb8f71 100644 --- a/src/main/java/smartpot/com/api/Sessions/Model/DAO/Service/SSessionI.java +++ b/src/main/java/smartpot/com/api/Sessions/Service/SSessionI.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Sessions.Model.DAO.Service; +package smartpot.com.api.Sessions.Service; import smartpot.com.api.Sessions.Model.Entity.Session; diff --git a/src/main/java/smartpot/com/api/SmartPotApiApplication.java b/src/main/java/smartpot/com/api/SmartPotApiApplication.java index f909b0a..29d4606 100644 --- a/src/main/java/smartpot/com/api/SmartPotApiApplication.java +++ b/src/main/java/smartpot/com/api/SmartPotApiApplication.java @@ -1,29 +1,38 @@ package smartpot.com.api; import io.github.cdimascio.dotenv.Dotenv; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; @SpringBootApplication @EnableMongoRepositories(basePackages = "smartpot.com.api") +@EnableCaching +@Slf4j public class SmartPotApiApplication { public static void main(String[] args) { - //loadEnv(); + loadEnv(); SpringApplication.run(SmartPotApiApplication.class, args); } private static void loadEnv() { - Dotenv dotenv = Dotenv.load(); + try { + Dotenv dotenv = Dotenv.load(); - dotenv.entries().forEach(entry -> { - String key = entry.getKey(); - String value = entry.getValue(); + dotenv.entries().forEach(entry -> { + String key = entry.getKey(); + String value = entry.getValue(); - if (value != null) { - System.setProperty(key, value); - } - }); + if (value != null) { + System.setProperty(key, value); + } + }); + } catch (Exception e) { + log.warn("No se pudo cargar el archivo .env. Se continuará con las variables de entorno ya presentes."); + log.debug("Detalles del error: ", e); + } } } diff --git a/src/main/java/smartpot/com/api/Users/Controller/UserController.java b/src/main/java/smartpot/com/api/Users/Controller/UserController.java index 074d2c7..c433630 100644 --- a/src/main/java/smartpot/com/api/Users/Controller/UserController.java +++ b/src/main/java/smartpot/com/api/Users/Controller/UserController.java @@ -7,22 +7,26 @@ 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.validation.ValidationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheConfig; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import smartpot.com.api.Responses.DeleteResponse; import smartpot.com.api.Responses.ErrorResponse; -import smartpot.com.api.Users.Model.DAO.Service.SUserI; import smartpot.com.api.Users.Model.DTO.UserDTO; +import smartpot.com.api.Users.Service.SUserI; /** * Controlador REST para las operaciones relacionadas con los usuarios. * <p>Este controlador proporciona una serie de métodos para gestionar usuarios en el sistema.</p> + * * @see SUserI */ @RestController @RequestMapping("/Users") +@CacheConfig(cacheNames = "users") @Tag(name = "Usuarios", description = "Operaciones relacionadas con usuarios") public class UserController { @@ -75,8 +79,11 @@ public UserController(SUserI serviceUser) { responseCode = "201", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserDTO.class))), - @ApiResponse(responseCode = "404", - description = "No se pudo crear el usuario.", + @ApiResponse(responseCode = "403", + description = "No se pudo crear el usuario, error de validación", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "409", + description = "Conflicto al crear el usuario.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity<?> createUser( @@ -84,8 +91,10 @@ public ResponseEntity<?> createUser( required = true) @RequestBody UserDTO userDTO) { try { return new ResponseEntity<>(serviceUser.CreateUser(userDTO), HttpStatus.CREATED); - } catch (Exception e) { - return new ResponseEntity<>(new ErrorResponse("Error al crear el usuario [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + } catch (ValidationException e) { + return new ResponseEntity<>(new ErrorResponse("Error al crear el usuario [" + e.getMessage() + "]", HttpStatus.FORBIDDEN.value()), HttpStatus.FORBIDDEN); + } catch (IllegalStateException e) { + return new ResponseEntity<>(new ErrorResponse("Error al crear el usuario [" + e.getMessage() + "]", HttpStatus.CONFLICT.value()), HttpStatus.CONFLICT); } } @@ -108,7 +117,7 @@ public ResponseEntity<?> createUser( @GetMapping("/All") @Operation(summary = "Obtener todos los usuarios", description = "Recupera todos los usuarios registrados en el sistema. " - + "En caso de no haber usuarios, se devolverá una lista vacía.", + + "En caso de no haber usuarios, se devolverá una excepción.", responses = { @ApiResponse(description = "Usuarios encontrados", responseCode = "200", @@ -126,6 +135,7 @@ public ResponseEntity<?> getAllUsers() { } } + /** * Busca un usuario utilizando su ID único. * <p>Este método recupera un usuario de la base de datos utilizando su identificador único. Si el usuario con el ID proporcionado existe, se devolverá un objeto {@link UserDTO} con la información del usuario.</p> @@ -195,7 +205,7 @@ public ResponseEntity<?> getUserById(@PathVariable @Parameter(description = "ID description = "Usuario no encontrado con el correo electrónico proporcionado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getUsersByEmail(@PathVariable @Parameter(description = "Correo electrónico del usuario", required = true) String email) { + public ResponseEntity<?> getUsersByEmail(@Parameter(description = "Correo electrónico del usuario", required = true) @PathVariable String email) { try { return new ResponseEntity<>(serviceUser.getUserByEmail(email), HttpStatus.OK); } catch (Exception e) { @@ -235,7 +245,7 @@ public ResponseEntity<?> getUsersByEmail(@PathVariable @Parameter(description = description = "No se encontraron usuarios con el nombre proporcionado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getUsersByName(@PathVariable @Parameter(description = "Nombre del usuario a buscar.", required = true) String name) { + public ResponseEntity<?> getUsersByName(@Parameter(description = "Nombre del usuario a buscar.", required = true) @PathVariable String name) { try { return new ResponseEntity<>(serviceUser.getUsersByName(name), HttpStatus.OK); } catch (Exception e) { @@ -275,7 +285,7 @@ public ResponseEntity<?> getUsersByName(@PathVariable @Parameter(description = " description = "No se encontraron usuarios con el apellido proporcionado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getUsersByLastname(@PathVariable @Parameter(description = "Apellido del usuario a buscar", required = true) String lastname) { + public ResponseEntity<?> getUsersByLastname(@Parameter(description = "Apellido del usuario a buscar", required = true) @PathVariable String lastname) { try { return new ResponseEntity<>(serviceUser.getUsersByLastname(lastname), HttpStatus.OK); } catch (Exception e) { @@ -314,7 +324,7 @@ public ResponseEntity<?> getUsersByLastname(@PathVariable @Parameter(description description = "No se encontraron usuarios con el rol proporcionado.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> getUsersByRole(@PathVariable @Parameter(description = "Rol del usuario a buscar", required = true) String role) { + public ResponseEntity<?> getUsersByRole(@Parameter(description = "Rol del usuario a buscar", required = true) @PathVariable String role) { try { return new ResponseEntity<>(serviceUser.getUsersByRole(role), HttpStatus.OK); } catch (Exception e) { @@ -322,6 +332,44 @@ public ResponseEntity<?> getUsersByRole(@PathVariable @Parameter(description = " } } + /** + * Recupera todos los roles de usuario registrados en el sistema. + * <p>Este método obtiene una lista de todos los roles de usuario disponibles en el sistema. Si no se encuentran roles, + * se devolverá una lista vacía con el código HTTP 200.</p> + * + * @return Un objeto {@link ResponseEntity} que contiene: + * <ul> + * <li>Una lista de cadenas {@link String} con los roles de usuario (código HTTP 200).</li> + * <li>Un mensaje de error si ocurre un problema al obtener los roles o no se encuentran roles registrados (código HTTP 404).</li> + * </ul> + * + * <p><b>Respuestas posibles:</b></p> + * <ul> + * <li><b>200 OK</b>: Si se encuentran roles registrados, se retorna una lista de cadenas con los nombres de los roles en formato JSON.<br></li> + * <li><b>404 Not Found</b>: Si no se encuentran roles o ocurre un error al obtenerlos, se retorna un objeto {@link ErrorResponse} con un mensaje de error.<br></li> + * </ul> + */ + @GetMapping("/role/All") + @Operation(summary = "Obtener todos los roles de usuario", + description = "Recupera todos los roles de usuario registrados en el sistema. " + + "En caso de no haber roles de usuario, se devolverá una excepción.", + responses = { + @ApiResponse(description = "Roles encontrados", + responseCode = "200", + content = @Content(mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = String.class)))), + @ApiResponse(responseCode = "404", + description = "No se encontraron roles registrados.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + public ResponseEntity<?> getAllRoles() { + try { + return new ResponseEntity<>(serviceUser.getAllRoles(), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("Error al buscar los roles [" + e.getMessage() + "]", HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND); + } + } + /** * Actualiza la información de un usuario existente. * <p>Este método recibe un ID de usuario y un objeto {@link UserDTO} con los datos actualizados. @@ -355,8 +403,8 @@ public ResponseEntity<?> getUsersByRole(@PathVariable @Parameter(description = " content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity<?> updateUser( - @PathVariable @Parameter(description = "ID único del usuario que se desea actualizar.", required = true) String id, - @RequestBody @Parameter(description = "Datos actualizados del usuario.") UserDTO updatedUser) { + @Parameter(description = "ID único del usuario que se desea actualizar.", required = true) @PathVariable String id, + @Parameter(description = "Datos actualizados del usuario.") @RequestBody UserDTO updatedUser) { try { return new ResponseEntity<>(serviceUser.UpdateUser(id, updatedUser), HttpStatus.OK); } catch (Exception e) { @@ -394,7 +442,7 @@ public ResponseEntity<?> updateUser( description = "No se pudo eliminar el usuario.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity<?> deleteUser(@PathVariable @Parameter(description = "ID único del usuario que se desea eliminar.", required = true) String id) { + public ResponseEntity<?> deleteUser(@Parameter(description = "ID único del usuario que se desea eliminar.", required = true) @PathVariable String id) { try { return new ResponseEntity<>(new DeleteResponse("Se ha eliminado un recurso [" + serviceUser.DeleteUser(id) + "]"), HttpStatus.OK); } catch (Exception e) { diff --git a/src/main/java/smartpot/com/api/Users/Mapper/MUser.java b/src/main/java/smartpot/com/api/Users/Mapper/MUser.java index 97e20f6..99f3d0a 100644 --- a/src/main/java/smartpot/com/api/Users/Mapper/MUser.java +++ b/src/main/java/smartpot/com/api/Users/Mapper/MUser.java @@ -3,7 +3,6 @@ import org.bson.types.ObjectId; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.factory.Mappers; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import smartpot.com.api.Users.Model.DTO.UserDTO; import smartpot.com.api.Users.Model.Entity.User; @@ -14,8 +13,6 @@ @Mapper(componentModel = "spring") public interface MUser { - MUser INSTANCE = Mappers.getMapper(MUser.class); - @Mapping(source = "id", target = "id", qualifiedByName = "stringToObjectId") @Mapping(source = "password", target = "password", qualifiedByName = "encodePassword") @Mapping(source = "createAt", target = "createAt", qualifiedByName = "stringToDate") diff --git a/src/main/java/smartpot/com/api/Users/Model/DTO/UserDTO.java b/src/main/java/smartpot/com/api/Users/Model/DTO/UserDTO.java index 6ebb6d5..ef3b8c0 100644 --- a/src/main/java/smartpot/com/api/Users/Model/DTO/UserDTO.java +++ b/src/main/java/smartpot/com/api/Users/Model/DTO/UserDTO.java @@ -4,9 +4,11 @@ import lombok.Data; import lombok.RequiredArgsConstructor; +import java.io.Serializable; + @Data @RequiredArgsConstructor -public class UserDTO { +public class UserDTO implements Serializable { @Schema(description = "ID único del usuario, generado automáticamente por la base de datos.", example = "676ae2a9b909de5f9607fcb6", hidden = true) private String id = null; diff --git a/src/main/java/smartpot/com/api/Users/Model/Entity/User.java b/src/main/java/smartpot/com/api/Users/Model/Entity/User.java index f5c9211..6b9281b 100644 --- a/src/main/java/smartpot/com/api/Users/Model/Entity/User.java +++ b/src/main/java/smartpot/com/api/Users/Model/Entity/User.java @@ -8,8 +8,6 @@ import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import smartpot.com.api.Users.Model.DTO.UserDTO; import java.io.Serializable; import java.util.Date; @@ -76,21 +74,5 @@ public class User implements Serializable { @NotEmpty(message = "El rol no puede estar vacío") @Field("role") - private Role role; - - /** - * Constructor que mapea los datos desde un {@link UserDTO}. - * Este constructor toma los datos de un DTO y los convierte a la entidad {@link User}. - * La contraseña se encripta utilizando BCrypt antes de ser almacenada. - * - * @param userDTO El objeto DTO con los datos del usuario. - */ - public User(UserDTO userDTO) { - this.name = userDTO.getName(); - this.lastname = userDTO.getLastname(); - this.email = userDTO.getEmail(); - BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); - this.password = passwordEncoder.encode(userDTO.getPassword()); - this.role = Role.valueOf(userDTO.getRole()); - } + private UserRole userRole; } diff --git a/src/main/java/smartpot/com/api/Users/Model/Entity/Role.java b/src/main/java/smartpot/com/api/Users/Model/Entity/UserRole.java similarity index 75% rename from src/main/java/smartpot/com/api/Users/Model/Entity/Role.java rename to src/main/java/smartpot/com/api/Users/Model/Entity/UserRole.java index 2334658..d8fbfcf 100644 --- a/src/main/java/smartpot/com/api/Users/Model/Entity/Role.java +++ b/src/main/java/smartpot/com/api/Users/Model/Entity/UserRole.java @@ -1,5 +1,5 @@ package smartpot.com.api.Users.Model.Entity; -public enum Role { +public enum UserRole { USER, ADMIN, SYSTEM } diff --git a/src/main/java/smartpot/com/api/Users/Model/DAO/Repository/RUser.java b/src/main/java/smartpot/com/api/Users/Repository/RUser.java similarity index 87% rename from src/main/java/smartpot/com/api/Users/Model/DAO/Repository/RUser.java rename to src/main/java/smartpot/com/api/Users/Repository/RUser.java index 5f4f102..4148188 100644 --- a/src/main/java/smartpot/com/api/Users/Model/DAO/Repository/RUser.java +++ b/src/main/java/smartpot/com/api/Users/Repository/RUser.java @@ -1,5 +1,4 @@ -package smartpot.com.api.Users.Model.DAO.Repository; - +package smartpot.com.api.Users.Repository; import org.bson.types.ObjectId; import org.springframework.data.mongodb.repository.MongoRepository; @@ -75,5 +74,14 @@ public interface RUser extends MongoRepository<User, ObjectId> { @Query("{ 'role' : ?0 }") List<User> findByRole(String role); + /** + * Verifica si existe un usuario con el correo electrónico proporcionado. + * + * <p>Este método devuelve {@code true} si existe un usuario con el correo electrónico especificado, + * y {@code false} en caso contrario.</p> + * + * @param email El correo electrónico a verificar. + * @return {@code true} si existe un usuario con el correo electrónico proporcionado, {@code false} en caso contrario. + */ boolean existsByEmail(String email); } diff --git a/src/main/java/smartpot/com/api/Users/Model/DAO/Service/SUser.java b/src/main/java/smartpot/com/api/Users/Service/SUser.java similarity index 82% rename from src/main/java/smartpot/com/api/Users/Model/DAO/Service/SUser.java rename to src/main/java/smartpot/com/api/Users/Service/SUser.java index 4b855b2..c09bc0b 100644 --- a/src/main/java/smartpot/com/api/Users/Model/DAO/Service/SUser.java +++ b/src/main/java/smartpot/com/api/Users/Service/SUser.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Users.Model.DAO.Service; +package smartpot.com.api.Users.Service; import jakarta.validation.ValidationException; import lombok.Builder; @@ -6,15 +6,20 @@ import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import smartpot.com.api.Users.Mapper.MUser; -import smartpot.com.api.Users.Model.DAO.Repository.RUser; import smartpot.com.api.Users.Model.DTO.UserDTO; -import smartpot.com.api.Users.Validation.VUserI; +import smartpot.com.api.Users.Model.Entity.UserRole; +import smartpot.com.api.Users.Repository.RUser; +import smartpot.com.api.Users.Validator.VUserI; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; @@ -30,6 +35,7 @@ @Builder @Service public class SUser implements SUserI { + private final RUser repositoryUser; private final MUser mapperUser; private final VUserI validatorUser; @@ -38,7 +44,7 @@ public class SUser implements SUserI { * Constructor que inyecta las dependencias del servicio. * * @param repositoryUser repositorio que maneja las operaciones de base de datos. - * @param mapperUser mapeador que convierte entidades User a UserDTO. + * @param mapperUser convertidor que convierte entidades User a UserDTO. * @param validatorUser validador que valida los datos de usuario. */ @Autowired @@ -48,28 +54,6 @@ public SUser(RUser repositoryUser, MUser mapperUser, VUserI validatorUser) { this.validatorUser = validatorUser; } - /** - * Obtiene todos los usuarios de la base de datos y los convierte a DTOs. - * * - * Este método consulta todos los usuarios almacenados en la base de datos utilizando el - * repositorio `repositoryUser`. Si la lista de usuarios está vacía, lanza una excepción. - * Los usuarios obtenidos se mapean a objetos `UserDTO` utilizando el objeto `mapperUser`. - * - * @return una lista de objetos {@link UserDTO} que representan a todos los usuarios. - * @throws Exception si no se encuentran usuarios en la base de datos. - * - * @see UserDTO - */ - @Override - public List<UserDTO> getAllUsers() throws Exception { - return Optional.of(repositoryUser.findAll()) - .filter(users -> !users.isEmpty()) - .map(users -> users.stream() - .map(mapperUser::toDTO) - .collect(Collectors.toList())) - .orElseThrow(() -> new Exception("No existe ningún usuario")); - } - /** * Crea un nuevo usuario en la base de datos a partir de un objeto {@link UserDTO}. * * @@ -79,25 +63,28 @@ public List<UserDTO> getAllUsers() throws Exception { * se guarda en la base de datos. Si el usuario ya existe o si las validaciones fallan, se lanza una * excepción correspondiente. * * + * * @param userDTO el objeto {@link UserDTO} que contiene los datos del nuevo usuario. * @return un objeto {@link UserDTO} que representa al usuario creado. - * @throws Exception si el usuario ya existe en la base de datos (por email) o si hay un error de validación. + * @throws IllegalStateException si el usuario ya existe en la base de datos (por email). + * @throws ValidationException si el usuario tiene un error de validación. * @throws ValidationException si las validaciones de los campos del usuario no son exitosas. - * * + * * @see UserDTO * @see ValidationException */ @Override - public UserDTO CreateUser(UserDTO userDTO) throws Exception { + @CachePut(value = "users", key = "#userDTO.id") + public UserDTO CreateUser(UserDTO userDTO) throws ValidationException, IllegalStateException { return Optional.of(userDTO) .filter(dto -> !repositoryUser.existsByEmail(dto.getEmail())) .map(ValidDTO -> { - validatorUser.validateName(userDTO.getName()); - validatorUser.validateLastname(userDTO.getLastname()); - validatorUser.validateEmail(userDTO.getEmail()); - validatorUser.validatePassword(userDTO.getPassword()); - validatorUser.validateRole(userDTO.getRole()); - if (validatorUser.isValid()) { + validatorUser.validateName(ValidDTO.getName()); + validatorUser.validateLastname(ValidDTO.getLastname()); + validatorUser.validateEmail(ValidDTO.getEmail()); + validatorUser.validatePassword(ValidDTO.getPassword()); + validatorUser.validateRole(ValidDTO.getRole()); + if (!validatorUser.isValid()) { throw new ValidationException(validatorUser.getErrors().toString()); } validatorUser.Reset(); @@ -111,7 +98,30 @@ public UserDTO CreateUser(UserDTO userDTO) throws Exception { .map(mapperUser::toEntity) .map(repositoryUser::save) .map(mapperUser::toDTO) - .orElseThrow(() -> new Exception("El Usuario ya existe")); + .orElseThrow(() -> new IllegalStateException("El Usuario ya existe")); + } + + + /** + * Obtiene todos los usuarios de la base de datos y los convierte a DTOs. + * * + * Este método consulta todos los usuarios almacenados en la base de datos utilizando el + * repositorio `repositoryUser`. Si la lista de usuarios está vacía, lanza una excepción. + * Los usuarios obtenidos se mapean a objetos `UserDTO` utilizando el objeto `mapperUser`. + * + * @return una lista de objetos {@link UserDTO} que representan a todos los usuarios. + * @throws Exception si no se encuentran usuarios en la base de datos. + * @see UserDTO + */ + @Override + @Cacheable(value = "users", key = "'all_users'") + public List<UserDTO> getAllUsers() throws Exception { + return Optional.of(repositoryUser.findAll()) + .filter(users -> !users.isEmpty()) + .map(users -> users.stream() + .map(mapperUser::toDTO) + .collect(Collectors.toList())) + .orElseThrow(() -> new Exception("No existe ningún usuario")); } /** @@ -124,18 +134,18 @@ public UserDTO CreateUser(UserDTO userDTO) throws Exception { * * @param id el ID del usuario que se desea obtener. El ID debe ser una cadena que representa un {@link ObjectId}. * @return un objeto {@link UserDTO} que representa al usuario encontrado. - * @throws Exception si el usuario no existe en la base de datos o si el ID no es válido. + * @throws Exception si el usuario no existe en la base de datos o si el ID no es válido. * @throws ValidationException si el ID proporcionado no es válido según las reglas de validación. - * * @see UserDTO * @see ValidationException */ @Override + @Cacheable(value = "users", key = "'id_'+#id") public UserDTO getUserById(String id) throws Exception { return Optional.of(id) .map(ValidId -> { - validatorUser.validateId(id); - if (validatorUser.isValid()) { + validatorUser.validateId(ValidId); + if (!validatorUser.isValid()) { throw new ValidationException(validatorUser.getErrors().toString()); } validatorUser.Reset(); @@ -159,18 +169,18 @@ public UserDTO getUserById(String id) throws Exception { * * @param email el correo electrónico del usuario que se desea obtener. * @return un objeto {@link UserDTO} que representa al usuario encontrado. - * @throws Exception si el usuario no existe en la base de datos o si el correo no es válido. + * @throws Exception si el usuario no existe en la base de datos o si el correo no es válido. * @throws ValidationException si el correo electrónico proporcionado no es válido según las reglas de validación. - * * @see UserDTO * @see ValidationException */ @Override + @Cacheable(value = "users", key = "'email_'+#email") public UserDTO getUserByEmail(String email) throws Exception { return Optional.of(email) .map(ValidEmail -> { validatorUser.validateEmail(email); - if (validatorUser.isValid()) { + if (!validatorUser.isValid()) { throw new ValidationException(validatorUser.getErrors().toString()); } validatorUser.Reset(); @@ -194,18 +204,18 @@ public UserDTO getUserByEmail(String email) throws Exception { * * @param name el nombre del usuario que se desea obtener. * @return una lista de objetos {@link UserDTO} que representan a los usuarios encontrados. - * @throws Exception si no existen usuarios con el nombre proporcionado o si el nombre no es válido. + * @throws Exception si no existen usuarios con el nombre proporcionado o si el nombre no es válido. * @throws ValidationException si el nombre proporcionado no es válido según las reglas de validación. - * * @see UserDTO * @see ValidationException */ @Override + @Cacheable(value = "users", key = "'name_'+#name") public List<UserDTO> getUsersByName(String name) throws Exception { return Optional.of(name) .map(ValidName -> { validatorUser.validateName(ValidName); - if (validatorUser.isValid()) { + if (!validatorUser.isValid()) { throw new ValidationException(validatorUser.getErrors().toString()); } validatorUser.Reset(); @@ -230,18 +240,18 @@ public List<UserDTO> getUsersByName(String name) throws Exception { * * @param lastname el apellido del usuario o los usuarios que se desea obtener. * @return una lista de objetos {@link UserDTO} que representan a los usuarios encontrados. - * @throws Exception si no existen usuarios con el apellido proporcionado o si el apellido no es válido. + * @throws Exception si no existen usuarios con el apellido proporcionado o si el apellido no es válido. * @throws ValidationException si el apellido proporcionado no es válido según las reglas de validación. - * * @see UserDTO * @see ValidationException */ @Override + @Cacheable(value = "users", key = "'lastname_'+#lastname") public List<UserDTO> getUsersByLastname(String lastname) throws Exception { return Optional.of(lastname) .map(ValidLastname -> { validatorUser.validateLastname(ValidLastname); - if (validatorUser.isValid()) { + if (!validatorUser.isValid()) { throw new ValidationException(validatorUser.getErrors().toString()); } validatorUser.Reset(); @@ -266,18 +276,18 @@ public List<UserDTO> getUsersByLastname(String lastname) throws Exception { * * @param role el rol del usuario o los usuarios que se desea obtener. * @return una lista de objetos {@link UserDTO} que representan a los usuarios encontrados. - * @throws Exception si no existen usuarios con el rol proporcionado o si el rol no es válido. + * @throws Exception si no existen usuarios con el rol proporcionado o si el rol no es válido. * @throws ValidationException si el rol proporcionado no es válido según las reglas de validación. - * * @see UserDTO * @see ValidationException */ @Override + @Cacheable(value = "users", key = "'role:'+#role") public List<UserDTO> getUsersByRole(String role) throws Exception { return Optional.of(role) .map(ValidRole -> { validatorUser.validateRole(ValidRole); - if (validatorUser.isValid()) { + if (!validatorUser.isValid()) { throw new ValidationException(validatorUser.getErrors().toString()); } validatorUser.Reset(); @@ -291,6 +301,28 @@ public List<UserDTO> getUsersByRole(String role) throws Exception { .orElseThrow(() -> new Exception("No existe ningún usuario con el rol")); } + /** + * Obtiene todos los roles de usuario de la base de datos. + * * + * Este método consulta todos los roles de usuario almacenados en la base de datos utilizando el + * enum `Role`. Si la lista de roles de usuario está vacía, lanza una excepción. + * + * @return una lista de objetos {@link String} que representan a todos los roles de usuario. + * @throws Exception si no se encuentra ningún rol de usuario en la base de datos. + * @see UserRole + */ + @Override + @Cacheable(value = "users", key = "'all_rols'") + public List<String> getAllRoles() throws Exception { + return Optional.of( + Arrays.stream(UserRole.values()) + .map(Enum::name) + .collect(Collectors.toList()) + ) + .filter(roles -> !roles.isEmpty()) + .orElseThrow(() -> new Exception("No existe ningún rol")); + } + /** * Actualiza la información de un usuario en la base de datos. * * @@ -299,16 +331,16 @@ public List<UserDTO> getUsersByRole(String role) throws Exception { * valores proporcionados en el objeto {@link UserDTO}. Luego, se validan los nuevos valores antes de * guardar el usuario actualizado. Si alguno de los valores es inválido, se lanza una excepción de validación. * - * @param id el identificador del usuario a actualizar. + * @param id el identificador del usuario a actualizar. * @param updatedUser el objeto {@link UserDTO} que contiene los nuevos valores para el usuario. * @return un objeto {@link UserDTO} con la información actualizada del usuario. - * @throws Exception si el usuario no se pudo actualizar debido a algún error general. + * @throws Exception si el usuario no se pudo actualizar debido a algún error general. * @throws ValidationException si alguno de los campos del usuario proporcionado no es válido según las reglas de validación. - * * @see UserDTO * @see ValidationException */ @Override + @CachePut(value = "users", key = "'id:'+#id") public UserDTO UpdateUser(String id, UserDTO updatedUser) throws Exception { UserDTO existingUser = getUserById(id); return Optional.of(updatedUser) @@ -349,10 +381,10 @@ public UserDTO UpdateUser(String id, UserDTO updatedUser) throws Exception { * @param id el identificador del usuario que se desea eliminar. * @return un mensaje indicando que el usuario ha sido eliminado correctamente. * @throws Exception si el usuario no existe o si ocurre un error durante el proceso de eliminación. - * * @see UserDTO */ @Override + @CacheEvict(value = "users", key = "'id_'+#id") public String DeleteUser(String id) throws Exception { return Optional.of(getUserById(id)) .map(user -> { @@ -373,7 +405,6 @@ public String DeleteUser(String id) throws Exception { * @param username el correo electrónico del usuario que se desea cargar. * @return un objeto {@link UserDetails} que contiene la información del usuario cargado. * @throws UsernameNotFoundException si no se encuentra un usuario con el correo electrónico proporcionado. - * * @see UserDetails * @see UsernameNotFoundException */ diff --git a/src/main/java/smartpot/com/api/Users/Model/DAO/Service/SUserI.java b/src/main/java/smartpot/com/api/Users/Service/SUserI.java similarity index 87% rename from src/main/java/smartpot/com/api/Users/Model/DAO/Service/SUserI.java rename to src/main/java/smartpot/com/api/Users/Service/SUserI.java index 6c50ece..a5c30ac 100644 --- a/src/main/java/smartpot/com/api/Users/Model/DAO/Service/SUserI.java +++ b/src/main/java/smartpot/com/api/Users/Service/SUserI.java @@ -1,5 +1,6 @@ -package smartpot.com.api.Users.Model.DAO.Service; +package smartpot.com.api.Users.Service; +import jakarta.validation.ValidationException; import org.springframework.security.core.userdetails.UserDetailsService; import smartpot.com.api.Users.Model.DTO.UserDTO; @@ -14,14 +15,6 @@ * correo electrónico y rol. */ public interface SUserI extends UserDetailsService { - /** - * Obtiene todos los usuarios registrados en el sistema. - * - * @return una lista de objetos {@link UserDTO} que representan a todos los usuarios. - * @throws Exception si ocurre un error al obtener los usuarios. - */ - List<UserDTO> getAllUsers() throws Exception; - /** * Crea un nuevo usuario en el sistema. * @@ -29,7 +22,15 @@ public interface SUserI extends UserDetailsService { * @return el objeto {@link UserDTO} del usuario creado. * @throws Exception si el usuario ya existe o si ocurre un error durante la creación. */ - UserDTO CreateUser(UserDTO userDTO) throws Exception; + UserDTO CreateUser(UserDTO userDTO) throws ValidationException, IllegalStateException; + + /** + * Obtiene todos los usuarios registrados en el sistema. + * + * @return una lista de objetos {@link UserDTO} que representan a todos los usuarios. + * @throws Exception si ocurre un error al obtener los usuarios. + */ + List<UserDTO> getAllUsers() throws Exception; /** * Obtiene un usuario por su ID. @@ -76,10 +77,18 @@ public interface SUserI extends UserDetailsService { */ List<UserDTO> getUsersByRole(String role) throws Exception; + /** + * Obtiene todos los roles de usuario registrados en el sistema. + * + * @return una lista de objetos {@link String} que representan a todos los roles de usuario. + * @throws Exception si ocurre un error al obtener los roles de usuario. + */ + List<String> getAllRoles() throws Exception; + /** * Actualiza los datos de un usuario. * - * @param id el identificador único del usuario a actualizar. + * @param id el identificador único del usuario a actualizar. * @param updatedUser un objeto {@link UserDTO} con los datos actualizados del usuario. * @return el objeto {@link UserDTO} con los datos del usuario actualizado. * @throws Exception si no se encuentra el usuario o si ocurre un error durante la actualización. @@ -94,4 +103,5 @@ public interface SUserI extends UserDetailsService { * @throws Exception si no se encuentra el usuario o si ocurre un error durante la eliminación. */ String DeleteUser(String id) throws Exception; + } diff --git a/src/main/java/smartpot/com/api/Users/Validation/UserRegex.java b/src/main/java/smartpot/com/api/Users/Validator/UserRegex.java similarity index 98% rename from src/main/java/smartpot/com/api/Users/Validation/UserRegex.java rename to src/main/java/smartpot/com/api/Users/Validator/UserRegex.java index 4030d45..60a5624 100644 --- a/src/main/java/smartpot/com/api/Users/Validation/UserRegex.java +++ b/src/main/java/smartpot/com/api/Users/Validator/UserRegex.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Users.Validation; +package smartpot.com.api.Users.Validator; /** * Contiene las expresiones regulares para la validación de los campos de usuario. diff --git a/src/main/java/smartpot/com/api/Users/Validation/VUser.java b/src/main/java/smartpot/com/api/Users/Validator/VUser.java similarity index 81% rename from src/main/java/smartpot/com/api/Users/Validation/VUser.java rename to src/main/java/smartpot/com/api/Users/Validator/VUser.java index 4c6202a..4b7c83a 100644 --- a/src/main/java/smartpot/com/api/Users/Validation/VUser.java +++ b/src/main/java/smartpot/com/api/Users/Validator/VUser.java @@ -1,14 +1,17 @@ -package smartpot.com.api.Users.Validation; +package smartpot.com.api.Users.Validator; import org.bson.types.ObjectId; import org.springframework.stereotype.Component; -import smartpot.com.api.Users.Model.Entity.Role; +import smartpot.com.api.Users.Model.Entity.UserRole; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import static smartpot.com.api.Users.Validation.UserRegex.*; +import static smartpot.com.api.Users.Validator.UserRegex.*; /** * Clase de validación para los usuarios. @@ -23,10 +26,14 @@ @Component public class VUser implements VUserI { - /** Indica si la validación fue exitosa. */ + /** + * Indica si la validación fue exitosa. + */ public boolean valid; - /** Lista de errores de validación. */ + /** + * Lista de errores de validación. + */ public List<String> errors; /** @@ -45,7 +52,7 @@ public VUser() { */ @Override public boolean isValid() { - return !valid; + return valid; } /** @@ -55,7 +62,9 @@ public boolean isValid() { */ @Override public List<String> getErrors() { - return errors; + List<String> currentErrors = errors; + Reset(); + return currentErrors; } /** @@ -163,32 +172,15 @@ public void validateRole(String role) { if (role == null || role.isEmpty()) { errors.add("El rol no puede estar vacío"); valid = false; - } else { - boolean isValidRole = false; - for (String validRole : getRoleNames()) { - if (validRole.matches(role)) { - isValidRole = true; - break; - } - } - - if (!isValidRole) { - errors.add("El Rol debe ser uno de los siguientes: " + String.join(", ", getRoleNames())); - valid = false; - } + return; } - } - /** - * Obtiene la lista de nombres de roles definidos en el sistema. - * - * @return Una lista con los nombres de los roles. - */ - private List<String> getRoleNames() { - List<String> roleNames = new ArrayList<>(); - for (Role role : Role.values()) { - roleNames.add(role.name()); + Set<String> validRoles = Arrays.stream(UserRole.values()) + .map(Enum::name).collect(Collectors.toSet()); + + if (!validRoles.contains(role)) { + errors.add("El Rol debe ser uno de los siguientes: " + String.join(", ", validRoles)); + valid = false; } - return roleNames; } } diff --git a/src/main/java/smartpot/com/api/Users/Validation/VUserI.java b/src/main/java/smartpot/com/api/Users/Validator/VUserI.java similarity index 98% rename from src/main/java/smartpot/com/api/Users/Validation/VUserI.java rename to src/main/java/smartpot/com/api/Users/Validator/VUserI.java index e276c34..0bc285a 100644 --- a/src/main/java/smartpot/com/api/Users/Validation/VUserI.java +++ b/src/main/java/smartpot/com/api/Users/Validator/VUserI.java @@ -1,4 +1,4 @@ -package smartpot.com.api.Users.Validation; +package smartpot.com.api.Users.Validator; import java.util.List; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4cfd854..9eba63c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,6 +14,28 @@ application.security.jwt.secret-key=${SECURITY_JWT_SECRET_KEY} application.security.jwt.expiration=${SECURITY_JWT_EXPIRATION} application.security.public.routes=${SECURITY_PUBLIC_ROUTES} +# Conexión de Redis +spring.data.redis.host=${CACHE_HOST} +spring.data.redis.port=${CACHE_PORT} +spring.data.redis.database=${CACHE_DB} +spring.data.redis.username=${CACHE_USERNAME} +spring.data.redis.password=${CACHE_PASSWORD} +spring.data.redis.timeout=${CACHE_TIMEOUT} +spring.data.redis.lettuce.pool.max-active=${CACHE_LETTUCE_POOL_MAX_ACTIVE} +spring.data.redis.lettuce.pool.max-wait=${CACHE_LETTUCE_POOL_MAX_WAIT} +spring.data.redis.lettuce.pool.max-idle=${CACHE_LETTUCE_POOL_MAX_IDLE} +spring.data.redis.lettuce.pool.min-idle=${CACHE_LETTUCE_POOL_MIN_IDLE} + +# Cache Config +spring.cache.type=${CACHE_TYPE} +spring.cache.redis.time-to-live=${CACHE_TIME_TO_LIVE} +spring.cache.redis.cache-null-values=${CACHE_NULL_VALUES} + +# Configuración de Rate Limiting +rate.limiting.max-requests=${RATE_LIMITING_MAX_REQUESTS} +rate.limiting.time-window=${RATE_LIMITING_TIME_WINDOW} +rate.limiting.public-routes=${RATE_LIMITING_PUBLIC_ROUTES} + # Swagger Config springdoc.api-docs.enabled=true springdoc.swagger-ui.enabled=true @@ -25,6 +47,14 @@ springdoc.swagger-ui.default-model-rendering=example springdoc.swagger-ui.default-model-expand-depth=1 springdoc.swagger-ui.doc-expansion=list +# Email Credentials +spring.mail.host=${MAIL_HOST} +spring.mail.port=${MAIL_PORT} +spring.mail.username=${MAIL_USERNAME} +spring.mail.password=${MAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=${MAIL_PROPERTIES_SMTP_AUTH} +spring.mail.properties.mail.smtp.starttls.enable=${MAIL_PROPERTIES_SMTP_STARTTLS_ENABLE} + # Http Headers http.header.cors.allowedOrigins=${HEADER_CORS_ALLOWED_ORIGINS} diff --git a/src/test/java/smartpot/com/api/SmartPotApiApplicationTest.java b/src/test/java/smartpot/com/api/SmartPotApiApplicationTest.java new file mode 100644 index 0000000..3723940 --- /dev/null +++ b/src/test/java/smartpot/com/api/SmartPotApiApplicationTest.java @@ -0,0 +1,12 @@ +package smartpot.com.api; + +import org.junit.jupiter.api.Test; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(locations = "classpath:/application.properties") +class SmartPotApiApplicationTest { + + @Test + void contextLoads() { + } +} \ No newline at end of file diff --git a/src/test/java/smartpot/com/api/SmartPotApiApplicationTests.java b/src/test/java/smartpot/com/api/SmartPotApiApplicationTests.java deleted file mode 100644 index 7b75231..0000000 --- a/src/test/java/smartpot/com/api/SmartPotApiApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package smartpot.com.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SmartPotApiApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/smartpot/com/api/Users/Controller/UserControllerTest.java b/src/test/java/smartpot/com/api/Users/Controller/UserControllerTest.java new file mode 100644 index 0000000..c1afed7 --- /dev/null +++ b/src/test/java/smartpot/com/api/Users/Controller/UserControllerTest.java @@ -0,0 +1,54 @@ +package smartpot.com.api.Users.Controller; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UserControllerTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void createUser() { + } + + @Test + void getAllUsers() { + } + + @Test + void getUserById() { + } + + @Test + void getUsersByEmail() { + } + + @Test + void getUsersByName() { + } + + @Test + void getUsersByLastname() { + } + + @Test + void getUsersByRole() { + } + + @Test + void updateUser() { + } + + @Test + void deleteUser() { + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..8f3e38c --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,44 @@ +# Configuración APP +spring.application.name=SmartPot-API-Test +server.port=8091 +application.title=SmartPot-API-Test +application.description=Entorno de Prueba para API de SmartPot +application.version=1.0.0 +application.author=SmartPot Developers +# Conexión de MongoDB +spring.data.mongodb.uri=mongo://localhost:27017/smartpot +# Security Config +application.security.jwt.secret-key=mySuperSecretKey +application.security.jwt.expiration=4102444800 +application.security.public.routes=/** +# Swagger Config +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true +springdoc.swagger-ui.path=/ +springdoc.swagger-ui.url=/v3/api-docs +springdoc.swagger-ui.display-operation-id=false +springdoc.swagger-ui.display-request-duration=true +springdoc.swagger-ui.default-model-rendering=example +springdoc.swagger-ui.default-model-expand-depth=1 +springdoc.swagger-ui.doc-expansion=list +# Http Headers +http.header.cors.allowedOrigins=* +# Config TOMCAT +server.tomcat.connection-timeout=100000000 +# Logs +# Activar logs de seguridad para JWT +logging.level.org.springframework.security=DEBUG +logging.level.smartpot.com.api.Security.jwt=DEBUG +# Habilitar logs de headers HTTP y filtros +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.web.filter=DEBUG +# Logs de MongoDB +logging.level.org.springframework.data.mongodb=DEBUG +logging.level.com.mongodb=DEBUG +# Logs de Swagger +logging.level.org.springdoc=DEBUG +logging.level.io.swagger=DEBUG +# Logs generales de Spring Boot +logging.level.org.springframework=DEBUG +logging.level.root=DEBUG +