Commit daf6bd79 by Antonio Rueda

Implementación de bloqueos para evitar reservas simultáneas de la misma

habitación.
parent f63846d1
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0" version="24.7.16">
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0" version="24.8.0">
<diagram name="Página-1" id="IpY-njWp8TfkcAYWz6vd">
<mxGraphModel dx="3589" dy="1303" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="2336" math="0" shadow="0">
<mxGraphModel dx="2950" dy="820" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="2336" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
......@@ -49,7 +49,7 @@
<mxCell id="a_C5hD_1tt0fRA46Nat0-17" value="reserva" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
<mxGeometry x="1410" y="204" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="a_C5hD_1tt0fRA46Nat0-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;" parent="1" source="a_C5hD_1tt0fRA46Nat0-10" target="a_C5hD_1tt0fRA46Nat0-1" edge="1">
<mxCell id="a_C5hD_1tt0fRA46Nat0-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=none;endFill=0;startArrow=open;startFill=0;" parent="1" source="a_C5hD_1tt0fRA46Nat0-10" target="a_C5hD_1tt0fRA46Nat0-1" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="a_C5hD_1tt0fRA46Nat0-22" value="cliente" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
......@@ -88,7 +88,7 @@
<mxCell id="a_C5hD_1tt0fRA46Nat0-35" value="cliente" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
<mxGeometry x="790" y="499" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="nwQueBcBUwvZKA8jkqrU-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;startArrow=diamond;startFill=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" target="nwQueBcBUwvZKA8jkqrU-5">
<mxCell id="nwQueBcBUwvZKA8jkqrU-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;startArrow=diamond;startFill=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" target="nwQueBcBUwvZKA8jkqrU-5" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="130" y="344" as="sourcePoint" />
<mxPoint x="130" y="510" as="targetPoint" />
......@@ -97,7 +97,7 @@
</Array>
</mxGeometry>
</mxCell>
<mxCell id="nwQueBcBUwvZKA8jkqrU-5" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;text-decoration:underline;&quot;&gt;&lt;b&gt;direccion:Usuario&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot; style=&quot;border-style:solid;&quot;&gt;&lt;p style=&quot;margin:0px;margin-left:8px;&quot;&gt;nombre = &quot;direccion&quot;&lt;br&gt;email = &quot;direccion@hotelxyz.es&quot;&lt;br&gt;clave =&amp;nbsp;&lt;span style=&quot;background-color: initial;&quot;&gt;&quot;SeCrEtO&quot;&quot;&lt;/span&gt;&lt;/p&gt;" style="verticalAlign=top;align=left;overflow=fill;html=1;whiteSpace=wrap;" vertex="1" parent="1">
<mxCell id="nwQueBcBUwvZKA8jkqrU-5" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;text-decoration:underline;&quot;&gt;&lt;b&gt;direccion:Usuario&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot; style=&quot;border-style:solid;&quot;&gt;&lt;p style=&quot;margin:0px;margin-left:8px;&quot;&gt;nombre = &quot;direccion&quot;&lt;br&gt;email = &quot;direccion@hotelxyz.es&quot;&lt;br&gt;clave =&amp;nbsp;&lt;span style=&quot;background-color: initial;&quot;&gt;&quot;SeCrEtO&quot;&quot;&lt;/span&gt;&lt;/p&gt;" style="verticalAlign=top;align=left;overflow=fill;html=1;whiteSpace=wrap;" parent="1" vertex="1">
<mxGeometry x="50" y="520" width="190" height="80" as="geometry" />
</mxCell>
</root>
......
......@@ -4,16 +4,15 @@ package es.ujaen.dae.reservahoteles.entidades;
import es.ujaen.dae.reservahoteles.excepciones.ReservaNoValida;
import es.ujaen.dae.reservahoteles.excepciones.NoDisponibilidadReserva;
import static es.ujaen.dae.reservahoteles.util.UtilString.normalizar;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import java.time.LocalDate;
import java.util.LinkedList;
......@@ -59,7 +58,11 @@ public class Hotel {
@OneToMany
@JoinColumn(name = "hotel_id")
List<Reserva> reservas;
// Para habilitar bloqueo optimista
@Version
int version;
public Hotel() {
}
......
......@@ -4,9 +4,10 @@ import es.ujaen.dae.reservahoteles.entidades.Hotel;
import es.ujaen.dae.reservahoteles.entidades.Reserva;
import static es.ujaen.dae.reservahoteles.util.UtilString.normalizar;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.PersistenceContext;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
......@@ -21,6 +22,16 @@ public class RepositorioHoteles {
@PersistenceContext
EntityManager em;
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Optional<Hotel> buscarPorId(int id) {
return Optional.ofNullable(em.find(Hotel.class, id));
}
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Optional<Hotel> buscarPorIdBloqueando(int id) {
return Optional.ofNullable(em.find(Hotel.class, id, LockModeType.PESSIMISTIC_WRITE));
}
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public List<Hotel> buscarPorNombreLocalidad(String nombre, String localidad) {
return em.createQuery("select h from Hotel h where " +
......@@ -52,6 +63,10 @@ public class RepositorioHoteles {
return em.merge(hotel);
}
public void comprobarErrores() {
em.flush();
}
public void guardarReserva(Reserva reserva) {
em.persist(reserva);
}
......
......@@ -6,6 +6,7 @@ import es.ujaen.dae.reservahoteles.entidades.Usuario;
import es.ujaen.dae.reservahoteles.entidades.Hotel;
import es.ujaen.dae.reservahoteles.entidades.Reserva;
import es.ujaen.dae.reservahoteles.excepciones.ClienteYaRegistrado;
import es.ujaen.dae.reservahoteles.excepciones.NoDisponibilidadReserva;
import es.ujaen.dae.reservahoteles.repositorios.RepositorioHoteles;
import es.ujaen.dae.reservahoteles.repositorios.RepositorioUsuarios;
import es.ujaen.dae.reservahoteles.util.UtilString;
......@@ -17,11 +18,11 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.PositiveOrZero;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -40,27 +41,19 @@ public class ServicioReservas {
@Autowired
RepositorioHoteles repositorioHoteles;
Map<Integer, Hotel> hoteles;
Map<String, Usuario> clientes;
@Value("${meses-historico}")
int mesesHistorico;
// Cliente especial de dirección
private static final Usuario direccion = new Usuario("direccion", "-", "670343332", "direccion@hotelxyz.es", "SeCrEtO");
private static int nReserva = 1;
public ServicioReservas() {
hoteles = new TreeMap<>();
clientes = new TreeMap<>();
}
public void nuevoHotel(Usuario direccion, @Valid Hotel hotel) {
if (!direccion.nombre().equals("direccion"))
throw new OperacionDeDireccion();
//hoteles.put(hotel.id(), hotel);
repositorioHoteles.guardar(hotel);
}
......@@ -68,25 +61,15 @@ public class ServicioReservas {
// Evitar que se cree un usuario con la cuenta de direccion
if (cliente.email().equals(direccion.email()))
throw new ClienteYaRegistrado();
/*
if (clientes.containsKey(cliente.email()))
throw new ClienteYaRegistrado();
clientes.put(cliente.email(), cliente);
*/
repositorioClientes.guardar(cliente);
}
public Optional<Usuario> login(@Email String email, String clave) {
// Equivalente al código de abajo pero más seguro y compacto
// return Optional.ofNullable(clientes.get(email))
// .filter(cliente -> cliente.clave().equals(clave));
// Caso especial de login de la direccion
if (direccion.email().equals(email) && direccion.clave().equals(clave))
return Optional.of(direccion);
// Usuario cliente = clientes.get(email);
return repositorioClientes.buscar(email).filter(cliente -> cliente.clave().equals(clave));
}
......@@ -97,7 +80,7 @@ public class ServicioReservas {
* @param fechaFin fecha de final de la estancia
* @param numHabSimple número de habitaciones simples solicitadas
* @param numHabDoble número de habitaciones dobles solicitadas
* @return la lista de hoteles candidatos
* @return la lista de hoteles candidatos (sin lista de reservas)
*/
@Transactional
public List<Hotel> buscarHotelesDisponiblesPorLocalidad(@NotBlank String localidad,
......@@ -105,16 +88,11 @@ public class ServicioReservas {
@PositiveOrZero int numHabSimple, @PositiveOrZero int numHabDoble) {
var localidadNorm = UtilString.normalizar(localidad);
// return hoteles.values().stream().filter(h ->
// UtilString.normalizar(h.localidad()).contains(localidadNorm) &&
// h.disponible(fechaInicio, fechaFin, numHabSimple, numHabDoble)
// ).toList();
List<Hotel> hotelesLocalidad = repositorioHoteles.buscarPorLocalidad(localidadNorm);
return hotelesLocalidad.stream().filter(h ->
UtilString.normalizar(h.localidad()).contains(localidadNorm) &&
h.disponible(fechaInicio, fechaFin, numHabSimple, numHabDoble)
).toList();
).map(h -> repositorioHoteles.buscarPorId(h.id()).get()).toList();
}
/**
......@@ -122,23 +100,24 @@ public class ServicioReservas {
* disponibilidad)
* @param nombre el nombre total o parcial del hotel
* @param localidad el nombre total o parcial de la localidad
* @return la lista de hoteles candidatos
* @return la lista de hoteles candidatos (sin lista de reservas)
*/
@Transactional
public List<Hotel> buscarHotel(@NotBlank String nombre, @NotBlank String localidad) {
// var nombreNorm = UtilString.normalizar(nombre);
// var localidadNorm = UtilString.normalizar(localidad);
// return hoteles.values().stream().filter(h ->
// UtilString.normalizar(h.localidad()).contains(localidadNorm) &&
// UtilString.normalizar(h.nombre()).contains(nombreNorm))
// .toList();
List<Hotel> hoteles = repositorioHoteles.buscarPorNombreLocalidad(nombre, localidad);
for (var hotel: hoteles) {
hotel.reservasEntre(LocalDate.MIN, LocalDate.MAX);
}
return hoteles;
return repositorioHoteles.buscarPorNombreLocalidad(nombre, localidad);
}
/**
* Carga las reservas de un hotel
* @param hotel el hotel cuyas lista de reservas se va a cargar
* @return el hotel con las reservas
*/
@Transactional
public Hotel hotelConReservas(Hotel hotel) {
hotel = repositorioHoteles.actualizar(hotel);
// Usar cualquier operación que acceda a las reservas para que se carguen
hotel.disponible(LocalDate.now(), 1, 1);
return hotel;
}
/**
......@@ -150,11 +129,14 @@ public class ServicioReservas {
* @param numHabDoble número de habitaciones dobles solicitadas
* @return true si hay disponibilidad, false en caso contrario
*/
@Transactional
public boolean disponible(Hotel hotel,
@FutureOrPresent LocalDate fechaInicio,
@FutureOrPresent LocalDate fechaFin,
@PositiveOrZero int numHabSimple,
@PositiveOrZero int numHabDoble) {
hotel = repositorioHoteles.actualizar(hotel);
return hotel.disponible(fechaInicio, fechaFin, numHabSimple, numHabDoble);
}
/**
......@@ -167,17 +149,38 @@ public class ServicioReservas {
* @param numHabSimple número de habitaciones simples solicitadas
* @param numHabDoble número de habitaciones dobles solicitadas
* @return la reserva recien creada en caso de éxito
*/
*/
@Transactional
public Reserva reserva(Usuario cliente, Hotel hotel,
LocalDate fechaInicio, LocalDate fechaFin,
@PositiveOrZero int numHabSimple, @PositiveOrZero int numHabDoble) {
// Opción con bloqueo pesimista (no requiere atributo version en Hotel)
//
// hotel = repositorioHoteles.buscarPorIdBloqueando(hotel.id()).get();
// var reserva = new Reserva(cliente, fechaInicio, fechaFin, numHabSimple, numHabDoble);
//
// hotel.nuevaReserva(reserva);
// repositorioHoteles.guardarReserva(reserva);
// Opción con bloqueo optimista (requiere el atributo version en Hotel)
var reserva = new Reserva(cliente, fechaInicio, fechaFin, numHabSimple, numHabDoble);
repositorioHoteles.guardarReserva(reserva);
hotel.nuevaReserva(reserva);
repositorioHoteles.actualizar(hotel);
boolean reservado = false;
while (!reservado) {
try {
hotel = repositorioHoteles.buscarPorId(hotel.id()).get();
hotel.nuevaReserva(reserva);
repositorioHoteles.guardarReserva(reserva);
repositorioHoteles.comprobarErrores();
reservado = true;
}
catch(OptimisticLockingFailureException e) {
}
}
// No hace falta guardar explícitamente el hotel porque está conectado con la transacción
return reserva;
}
......@@ -191,9 +194,5 @@ public class ServicioReservas {
idHoteles.stream()
.map(id -> repositorioHoteles.buscarPorId(id).get())
.forEach(hotel -> hotel.eliminarReservasAnteriores(fechaLimite));
//for (var hotel: hoteles.values()) {
// hotel.eliminarReservasAnteriores(fechaLimite);
//}
}
}
......@@ -8,6 +8,7 @@ import es.ujaen.dae.reservahoteles.excepciones.NoDisponibilidadReserva;
import jakarta.validation.ConstraintViolationException;
import java.time.LocalDate;
import java.util.List;
import java.util.logging.Logger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.jupiter.api.Test;
......@@ -128,5 +129,39 @@ public class TestServicioReservas {
// Reservar una simple y otra doble: no hay disponibilidad para fechas solapadas con la anterior reserva
assertThatThrownBy(()-> servicio.reserva(cliente, hotel, LocalDate.now().plusDays(5), LocalDate.now().plusDays(8), 1, 2))
.isInstanceOf(NoDisponibilidadReserva.class);
}
@Test
@DirtiesContext
void testReservaHotelesConcurrente() {
var direccion = servicio.login("direccion@hotelxyz.es", "SeCrEtO").get();
servicio.nuevoHotel(direccion, new Hotel("Bed and Breakfast Almería", "Almería", "Almería", "04001", 2, 2, 60, 100));
servicio.nuevoCliente(new Usuario("Pedro", "Jaén Jaén", "611203025", "pjaen@gmail.com", "miClAvE"));
servicio.nuevoCliente(new Usuario("Juan", "Granada Granada", "611213126", "jgranada@gmail.com", "miClAvE"));
var hotel = servicio.buscarHotel("bed and breakfast", "almeria").get(0);
var cliente1 = servicio.login("pjaen@gmail.com", "miClAvE").get();
var cliente2 = servicio.login("jgranada@gmail.com", "miClAvE").get();
// Reservar 2 habitaciones dobles
new Thread(()-> {
try {
servicio.reserva(cliente1, hotel, LocalDate.now().plusDays(7), LocalDate.now().plusDays(10), 0, 2);
}
catch(NoDisponibilidadReserva e) {
Logger.getLogger(servicio.getClass().getName()).warning("Reserva de cliente 1 sin disponibilidad");
}
}).start();
try {
servicio.reserva(cliente2, hotel, LocalDate.now().plusDays(5), LocalDate.now().plusDays(12), 0, 2);
}
catch(NoDisponibilidadReserva e) {
Logger.getLogger(servicio.getClass().getName()).warning("Reserva de cliente 2 sin disponibilidad");
}
var hotelConReservas = servicio.hotelConReservas(servicio.buscarHotel("bed and breakfast", "almeria").get(0));
assertThat(hotelConReservas.reservasEntre(LocalDate.MIN, LocalDate.MAX)).singleElement();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment