Spring Boot 3 - Buenas prácticas y protección: Aula 2

This commit is contained in:
devfzn 2023-09-12 14:48:40 -03:00
parent bb4bfe3772
commit e9fbc012c2
Signed by: devfzn
GPG Key ID: E070ECF4A754FDB1
21 changed files with 202 additions and 32 deletions

View File

@ -2,8 +2,8 @@ package med.voll.api.controller;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.medico.*;
import med.voll.api.domain.direccion.DatosDireccion;
import med.voll.api.domain.medico.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@ -50,7 +50,7 @@ public class MedicoController {
@PutMapping
@Transactional
public ResponseEntity<DatosRespuestaMedico> actualizarMedico(
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
medico.actualizarDatos(datosActualizarMedico);
return ResponseEntity.ok(new DatosRespuestaMedico(

View File

@ -2,8 +2,8 @@ package med.voll.api.controller;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.paciente.*;
import med.voll.api.domain.direccion.DatosDireccion;
import med.voll.api.domain.paciente.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@ -25,8 +25,8 @@ public class PacienteController {
@PostMapping
public ResponseEntity<DatosRespuestaPaciente> registrarPaciente(
@RequestBody @Valid DatosRegistroPaciente datosRegistroPaciente,
UriComponentsBuilder uriComponentsBuilder) {
@RequestBody @Valid DatosRegistroPaciente datosRegistroPaciente,
UriComponentsBuilder uriComponentsBuilder) {
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),

View File

@ -1,4 +1,4 @@
package med.voll.api.direccion;
package med.voll.api.domain.direccion;
import jakarta.validation.constraints.NotBlank;

View File

@ -1,4 +1,4 @@
package med.voll.api.direccion;
package med.voll.api.domain.direccion;
import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;

View File

@ -1,7 +1,7 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
import jakarta.validation.constraints.NotNull;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosActualizarMedico(@NotNull Long id, String nombre, String documento, DatosDireccion direccion) {

View File

@ -1,4 +1,4 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
public record DatosListadoMedicos(
Long id,

View File

@ -1,11 +1,11 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRegistroMedico(
@NotBlank String nombre,

View File

@ -1,7 +1,7 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
import jakarta.validation.constraints.NotNull;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRespuestaMedico(@NotNull Long id, String nombre,
String email, String telefono, String documento,

View File

@ -1,4 +1,4 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
public enum Especialidad {
ORTOPEDIA,

View File

@ -1,11 +1,11 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import med.voll.api.direccion.Direccion;
import med.voll.api.domain.direccion.Direccion;
@Table(name="medicos")
@Entity(name="Medico")

View File

@ -1,4 +1,4 @@
package med.voll.api.medico;
package med.voll.api.domain.medico;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

View File

@ -1,7 +1,7 @@
package med.voll.api.paciente;
package med.voll.api.domain.paciente;
import jakarta.validation.constraints.NotNull;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosActualizarPaciente(@NotNull Long id, String nombre, String documento, DatosDireccion direccion) {

View File

@ -1,4 +1,4 @@
package med.voll.api.paciente;
package med.voll.api.domain.paciente;
public record DatosListadoPacientes(Long id, String nombre, String documento, String email) {

View File

@ -1,11 +1,11 @@
package med.voll.api.paciente;
package med.voll.api.domain.paciente;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRegistroPaciente(
@NotBlank String nombre,

View File

@ -1,7 +1,7 @@
package med.voll.api.paciente;
package med.voll.api.domain.paciente;
import jakarta.validation.constraints.NotNull;
import med.voll.api.direccion.DatosDireccion;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRespuestaPaciente(@NotNull Long id, String nombre,
String email, String telefono, String documento,

View File

@ -1,12 +1,11 @@
package med.voll.api.paciente;
package med.voll.api.domain.paciente;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import med.voll.api.direccion.Direccion;
import med.voll.api.medico.DatosActualizarMedico;
import med.voll.api.domain.direccion.Direccion;
@Table(name="pacientes")
@Entity(name="Paciente")

View File

@ -1,4 +1,4 @@
package med.voll.api.paciente;
package med.voll.api.domain.paciente;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

View File

@ -0,0 +1,29 @@
package med.voll.api.infra;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ManejadorDeErrores {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity manejarError404(){
return ResponseEntity.notFound().build();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity manejarError400(MethodArgumentNotValidException e){
var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList();
return ResponseEntity.badRequest().body(errores);
}
private record DatosErrorValidacion(String campo, String error) {
public DatosErrorValidacion(FieldError error) {
this(error.getField(), error.getDefaultMessage());
}
}
}

View File

@ -4,4 +4,6 @@ spring.datasource.username=alura
spring.datasource.password=alura
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.format_sql=true
server.error.include-stacktrace=never

View File

@ -165,3 +165,142 @@ ejm consulta por código `405 Method Not Allowed`
https://http.cat/405
```
## Manejando errores
- Spring boot common
[properties](https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html)
- Spring boot server
[properties](https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.server)
#### Ocultando el stacktrace
[application.properties](./api_rest/api2/src/main/resources/application.properties)
```conf
server.error.include-stacktrace=never
```
Nuevo package [infra](./api_rest/api2/src/main/java/med/voll/api/infra) con nueva
clase
[ManejadorDeErrores](./api_rest/api2/src/main/java/med/voll/api/infra/ManejadorDeErrores.java)
```java
...
@RestControllerAdvice
public class ManejadorDeErrores {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity manejarError404(){
return ResponseEntity.notFound().build();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity manejarError400(MethodArgumentNotValidException e){
var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList();
return ResponseEntity.badRequest().body(errores);
}
private record DatosErrorValidacion(String campo, String error) {
public DatosErrorValidacion(FieldError error) {
this(error.getField(), error.getDefaultMessage());
}
}
}
```
Por defecto, **Bean Validation** devuelve mensajes de error en inglés, sin
embargo, hay una traducción de estos mensajes al español ya implementada en
esta especificación.
En el protocolo HTTP hay un encabezado llamado `Accept-Language`, que sirve para
indicar al servidor el idioma preferido del cliente que activa la solicitud.
Podemos utilizar esta cabecera para indicarle a Spring el idioma deseado, para
que en la integración con Bean Validation pueda buscar mensajes según el idioma
indicado.
En Insomnia, y también en otras herramientas similares, existe una opción
llamada Header en la que podemos incluir cabeceras a enviar en la petición.
Si agregamos el encabezado `Accept-Language` con el valor `es`, los mensajes de
error de **Bean Validation** se devolverán automáticamente en español.
> Nota: Bean Validation solo traduce los mensajes de error a unos pocos idiomas.
### Personalización de mensajes de error
**Bean Validation** tiene un mensaje de error para cada una de sus anotaciones.
P.e. cuando la validación falla en algún atributo anotado con `@NotBlank`, el
mensaje de error será: `must not be blank`.
Estos mensajes de error no se definieron en la aplicación, ya que son mensajes
de error estándar de Bean Validation. Sin embargo, si lo desea, puede
personalizar dichos mensajes.
Una de las formas de personalizar los mensajes de error es agregar el atributo
del mensaje a las anotaciones de validación:
```java
public record DatosCadastroMedico(
@NotBlank(message = "Nombre es obligatorio")
String nombre,
@NotBlank(message = "Email es obligatorio")
@Email(message = "Formato de email es inválido")
String email,
@NotBlank(message = "Teléfono es obligatorio")
String telefono,
@NotBlank(message = "CRM es obligatorio")
@Pattern(regexp = "\\d{4,6}", message = "Formato do CRM es inválido")
String crm,
@NotNull(message = "Especialidad es obligatorio")
Especialidad especialidad,
@NotNull(message = "Datos de dirección son obligatorios")
@Valid DatosDireccion direccion) {}
```
Otra forma es aislar los mensajes en un archivo de propiedades, que debe tener
el nombre `ValidationMessages.properties` y estar creado en el directorio
`src/main/resources`:
```config
nombre.obligatorio=El nombre es obligatorio
email.obligatorio=Correo electrónico requerido
email.invalido=El formato del correo electrónico no es válido
phone.obligatorio=Teléfono requerido
crm.obligatorio=CRM es obligatorio
crm.invalido=El formato CRM no es válido
especialidad.obligatorio=La especialidad es obligatoria
address.obligatorio=Los datos de dirección son obligatorios
```
Y, en las anotaciones, indicar la clave de las propiedades por el propio atributo
message, delimitando con los caracteres `{` y `}`:
```java
public record DatosRegistroMedico(
@NotBlank(message = "{nombre.obligatorio}")
String nombre,
@NotBlank(message = "{email.obligatorio}")
@Email(message = "{email.invalido}")
String email,
@NotBlank(message = "{telefono.obligatorio}")
String telefono,
@NotBlank(message = "{crm.obligatorio}")
@Pattern(regexp = "\\d{4,6}", message = "{crm.invalido}")
String crm,
@NotNull(message = "{especialidad.obligatorio}")
Especialidad especialidad,
@NotNull(message = "{direccion.obligatorio}")
@Valid DatosDireccion direccion) {}
```

View File

@ -51,3 +51,4 @@ primoridiales en programación con Javascript
- [Persistencia con JPA - Hibernate](./010_spring_boot/jpa_persistencia_hibernate.md)
- [JPA consultas avanzadas, rendimiento y modelos complejos](./010_spring_boot/jpa_avanzado.md)
- [Desarrollo API Rest](./010_spring_boot/spring_boot_1.md)
- [Buenas prácticas y protección de API Rest](./010_spring_boot/spring_boot_2.md)