Spring Boot 3 - Buenas prácticas y protección: Aula 2
This commit is contained in:
parent
bb4bfe3772
commit
e9fbc012c2
@ -2,8 +2,8 @@ package med.voll.api.controller;
|
|||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import med.voll.api.direccion.DatosDireccion;
|
import med.voll.api.domain.direccion.DatosDireccion;
|
||||||
import med.voll.api.medico.*;
|
import med.voll.api.domain.medico.*;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@ -50,7 +50,7 @@ public class MedicoController {
|
|||||||
@PutMapping
|
@PutMapping
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<DatosRespuestaMedico> actualizarMedico(
|
public ResponseEntity<DatosRespuestaMedico> actualizarMedico(
|
||||||
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
|
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
|
||||||
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
|
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
|
||||||
medico.actualizarDatos(datosActualizarMedico);
|
medico.actualizarDatos(datosActualizarMedico);
|
||||||
return ResponseEntity.ok(new DatosRespuestaMedico(
|
return ResponseEntity.ok(new DatosRespuestaMedico(
|
||||||
|
@ -2,8 +2,8 @@ package med.voll.api.controller;
|
|||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import med.voll.api.direccion.DatosDireccion;
|
import med.voll.api.domain.direccion.DatosDireccion;
|
||||||
import med.voll.api.paciente.*;
|
import med.voll.api.domain.paciente.*;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@ -25,8 +25,8 @@ public class PacienteController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<DatosRespuestaPaciente> registrarPaciente(
|
public ResponseEntity<DatosRespuestaPaciente> registrarPaciente(
|
||||||
@RequestBody @Valid DatosRegistroPaciente datosRegistroPaciente,
|
@RequestBody @Valid DatosRegistroPaciente datosRegistroPaciente,
|
||||||
UriComponentsBuilder uriComponentsBuilder) {
|
UriComponentsBuilder uriComponentsBuilder) {
|
||||||
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
|
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
|
||||||
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
|
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
|
||||||
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),
|
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package med.voll.api.direccion;
|
package med.voll.api.domain.direccion;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package med.voll.api.direccion;
|
package med.voll.api.domain.direccion;
|
||||||
|
|
||||||
import jakarta.persistence.Embeddable;
|
import jakarta.persistence.Embeddable;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
@ -1,7 +1,7 @@
|
|||||||
package med.voll.api.medico;
|
package med.voll.api.domain.medico;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
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) {
|
public record DatosActualizarMedico(@NotNull Long id, String nombre, String documento, DatosDireccion direccion) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package med.voll.api.medico;
|
package med.voll.api.domain.medico;
|
||||||
|
|
||||||
public record DatosListadoMedicos(
|
public record DatosListadoMedicos(
|
||||||
Long id,
|
Long id,
|
@ -1,11 +1,11 @@
|
|||||||
package med.voll.api.medico;
|
package med.voll.api.domain.medico;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import med.voll.api.direccion.DatosDireccion;
|
import med.voll.api.domain.direccion.DatosDireccion;
|
||||||
|
|
||||||
public record DatosRegistroMedico(
|
public record DatosRegistroMedico(
|
||||||
@NotBlank String nombre,
|
@NotBlank String nombre,
|
@ -1,7 +1,7 @@
|
|||||||
package med.voll.api.medico;
|
package med.voll.api.domain.medico;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
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,
|
public record DatosRespuestaMedico(@NotNull Long id, String nombre,
|
||||||
String email, String telefono, String documento,
|
String email, String telefono, String documento,
|
@ -1,4 +1,4 @@
|
|||||||
package med.voll.api.medico;
|
package med.voll.api.domain.medico;
|
||||||
|
|
||||||
public enum Especialidad {
|
public enum Especialidad {
|
||||||
ORTOPEDIA,
|
ORTOPEDIA,
|
@ -1,11 +1,11 @@
|
|||||||
package med.voll.api.medico;
|
package med.voll.api.domain.medico;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import med.voll.api.direccion.Direccion;
|
import med.voll.api.domain.direccion.Direccion;
|
||||||
|
|
||||||
@Table(name="medicos")
|
@Table(name="medicos")
|
||||||
@Entity(name="Medico")
|
@Entity(name="Medico")
|
@ -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.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
@ -1,7 +1,7 @@
|
|||||||
package med.voll.api.paciente;
|
package med.voll.api.domain.paciente;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
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) {
|
public record DatosActualizarPaciente(@NotNull Long id, String nombre, String documento, DatosDireccion direccion) {
|
||||||
|
|
@ -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) {
|
public record DatosListadoPacientes(Long id, String nombre, String documento, String email) {
|
||||||
|
|
@ -1,11 +1,11 @@
|
|||||||
package med.voll.api.paciente;
|
package med.voll.api.domain.paciente;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import med.voll.api.direccion.DatosDireccion;
|
import med.voll.api.domain.direccion.DatosDireccion;
|
||||||
|
|
||||||
public record DatosRegistroPaciente(
|
public record DatosRegistroPaciente(
|
||||||
@NotBlank String nombre,
|
@NotBlank String nombre,
|
@ -1,7 +1,7 @@
|
|||||||
package med.voll.api.paciente;
|
package med.voll.api.domain.paciente;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
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,
|
public record DatosRespuestaPaciente(@NotNull Long id, String nombre,
|
||||||
String email, String telefono, String documento,
|
String email, String telefono, String documento,
|
@ -1,12 +1,11 @@
|
|||||||
package med.voll.api.paciente;
|
package med.voll.api.domain.paciente;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import med.voll.api.direccion.Direccion;
|
import med.voll.api.domain.direccion.Direccion;
|
||||||
import med.voll.api.medico.DatosActualizarMedico;
|
|
||||||
|
|
||||||
@Table(name="pacientes")
|
@Table(name="pacientes")
|
||||||
@Entity(name="Paciente")
|
@Entity(name="Paciente")
|
@ -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.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,4 +4,6 @@ spring.datasource.username=alura
|
|||||||
spring.datasource.password=alura
|
spring.datasource.password=alura
|
||||||
|
|
||||||
spring.jpa.show-sql=true
|
spring.jpa.show-sql=true
|
||||||
spring.jpa.properties.hibernate.format_sql=true
|
spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
|
server.error.include-stacktrace=never
|
@ -165,3 +165,142 @@ ejm consulta por código `405 Method Not Allowed`
|
|||||||
https://http.cat/405
|
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) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
@ -51,3 +51,4 @@ primoridiales en programación con Javascript
|
|||||||
- [Persistencia con JPA - Hibernate](./010_spring_boot/jpa_persistencia_hibernate.md)
|
- [Persistencia con JPA - Hibernate](./010_spring_boot/jpa_persistencia_hibernate.md)
|
||||||
- [JPA consultas avanzadas, rendimiento y modelos complejos](./010_spring_boot/jpa_avanzado.md)
|
- [JPA consultas avanzadas, rendimiento y modelos complejos](./010_spring_boot/jpa_avanzado.md)
|
||||||
- [Desarrollo API Rest](./010_spring_boot/spring_boot_1.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)
|
||||||
|
Loading…
Reference in New Issue
Block a user