├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ ├── java │ │ └── dev │ │ │ └── marioszocs │ │ │ └── hotelreservationapi │ │ │ ├── utils │ │ │ └── AppUtils.java │ │ │ ├── entity │ │ │ ├── ValidTypesOfHotelsEnum.java │ │ │ ├── BaseEntity.java │ │ │ ├── Reservation.java │ │ │ ├── Hotel.java │ │ │ └── AuditableEntity.java │ │ │ ├── dto │ │ │ ├── IdEntity.java │ │ │ └── SuccessEntity.java │ │ │ ├── constants │ │ │ ├── AppConstants.java │ │ │ └── ErrorMessages.java │ │ │ ├── repository │ │ │ ├── ReservationRepository.java │ │ │ └── HotelRepository.java │ │ │ ├── HotelReservationApiApplication.java │ │ │ ├── exception │ │ │ ├── InvalidRequestException.java │ │ │ ├── HotelApiExceptionHandler.java │ │ │ └── ApiErrorMessage.java │ │ │ ├── ServletInitializer.java │ │ │ ├── config │ │ │ └── OpenApiConfig.java │ │ │ ├── service │ │ │ ├── ReservationService.java │ │ │ └── HotelService.java │ │ │ ├── validator │ │ │ ├── PageNumberAndSizeValidator.java │ │ │ ├── ReservationValidator.java │ │ │ ├── HotelValidator.java │ │ │ └── BaseValidator.java │ │ │ ├── controller │ │ │ ├── ReservationController.java │ │ │ └── HotelController.java │ │ │ ├── bootstrap │ │ │ └── HotelsAndReservationsLoader.java │ │ │ └── serviceImp │ │ │ ├── ReservationServiceImp.java │ │ │ └── HotelServiceImp.java │ └── resources │ │ ├── application-h2.properties │ │ ├── application-mysql.properties │ │ └── application.properties └── test │ └── java │ ├── resources │ └── application.properties │ └── dev │ └── marioszocs │ └── hotelreservationapi │ ├── HotelReservationApiApplicationTests.java │ ├── validator │ ├── ReservationValidatorTest.java │ └── HotelValidatorTest.java │ └── serviceImp │ └── HotelServiceImpMockitoTest.java ├── .gitignore ├── pom.xml ├── mvnw.cmd ├── README.md └── mvnw /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marioszocs/spring-boot-hotel-reservation-api/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/utils/AppUtils.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.utils; 2 | 3 | public class AppUtils { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/entity/ValidTypesOfHotelsEnum.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.entity; 2 | 3 | public enum ValidTypesOfHotelsEnum { 4 | DELUXE, LUXURY, SUITE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/dto/IdEntity.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.dto; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class IdEntity { 7 | private Integer id; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/dto/SuccessEntity.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.dto; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class SuccessEntity { 7 | private boolean success; 8 | } 9 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/test/java/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1 2 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 3 | spring.jpa.hibernate.ddl-auto=create-drop 4 | spring.jpa.show-sql=true 5 | logging.level.dev.marioszocs.hotelreservationapi=debug 6 | spring.datasource.driver-class-name=org.h2.Driver -------------------------------------------------------------------------------- /src/test/java/dev/marioszocs/hotelreservationapi/HotelReservationApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class HotelReservationApiApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/application-h2.properties: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # SPRING DATASOURCE SETTINGS for H2 3 | ############################################################ 4 | spring.datasource.driver-class-name=org.h2.Driver 5 | spring.datasource.url=jdbc:h2:mem:testh2db 6 | spring.datasource.username=sa 7 | spring.datasource.password= 8 | spring.jpa.hibernate.ddl-auto=create-drop 9 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/constants/AppConstants.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.constants; 2 | 3 | public class AppConstants { 4 | 5 | public static final String DEFAULT_PAGE_NUMBER = "0"; 6 | 7 | public static final String DEFAULT_PAGE_SIZE = "3"; 8 | 9 | public static final String MAX_PAGE_SIZE = "10"; 10 | 11 | public static final String DEFAULT_SORTING_PARAM = "name"; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/repository/ReservationRepository.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.repository; 2 | 3 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface ReservationRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/HotelReservationApiApplication.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class HotelReservationApiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(HotelReservationApiApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/exception/InvalidRequestException.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.exception; 2 | 3 | import java.io.Serial; 4 | 5 | /** 6 | * Invalid Request Exceptions for the Hotel Reservation API 7 | */ 8 | public class InvalidRequestException extends RuntimeException { 9 | 10 | @Serial 11 | private static final long serialVersionUID = 2178386706175989636L; 12 | 13 | public InvalidRequestException(String message) { 14 | super(message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/dev/marioszocs/hotelreservationapi/validator/ReservationValidatorTest.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.validator; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class ReservationValidatorTest { 8 | 9 | @Test 10 | void validateReservationPOST() { 11 | } 12 | 13 | @Test 14 | void validateId() { 15 | } 16 | 17 | @Test 18 | void validateDates() { 19 | } 20 | 21 | @Test 22 | void validateGuest() { 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(HotelReservationApiApplication.class); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.config; 2 | 3 | import io.swagger.v3.oas.models.OpenAPI; 4 | import io.swagger.v3.oas.models.info.Info; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class OpenApiConfig { 10 | 11 | @Bean 12 | public OpenAPI customOpenAPI() { 13 | return new OpenAPI() 14 | .info(new Info().title("Hotel Reservation API") 15 | .description("API to manage hotel reservations") 16 | .version("v1")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import jakarta.persistence.*; 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Use @MappedSuperclass to indicate that although this is not an Entity in itself, 11 | * i.e. we don't want to store it in a separate table, but the 3 fields shown here 12 | * should appear in the tables of the child classes 13 | */ 14 | @MappedSuperclass 15 | @Getter 16 | @Setter 17 | public abstract class BaseEntity implements Serializable { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.AUTO) 21 | @Column(updatable = false) 22 | protected Integer id; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application-mysql.properties: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # SPRING DATASOURCE SETTINGS for MySQL 3 | ############################################################ 4 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 5 | spring.datasource.url=jdbc:mysql://localhost:3306/hotelreservationapi?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false 6 | spring.datasource.username=root 7 | spring.datasource.password=mysql123 8 | 9 | ############################################################ 10 | # SPRING JPA SETTINGS 11 | ############################################################ 12 | # Hibernate ddl auto (create, create-drop, validate, update) 13 | spring.jpa.hibernate.ddl-auto=create-drop 14 | spring.jpa.database=mysql 15 | spring.jpa.show-sql=false -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/service/ReservationService.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.service; 2 | 3 | import dev.marioszocs.hotelreservationapi.dto.IdEntity; 4 | import dev.marioszocs.hotelreservationapi.dto.SuccessEntity; 5 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 6 | 7 | import java.util.List; 8 | 9 | public interface ReservationService { 10 | List getAllReservations(); 11 | Reservation getReservation(Integer id); 12 | IdEntity saveReservation(Reservation reservations); 13 | SuccessEntity deleteReservation(Integer id); 14 | boolean validateHotelExistenceById(Integer id); 15 | boolean dateIsBefore(String date1, String date2); 16 | boolean reservationOverlaps(Reservation reservations); 17 | boolean validateReservationExistence(Integer id); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/repository/HotelRepository.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.repository; 2 | 3 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface HotelRepository extends JpaRepository { 13 | 14 | @Query(value = "SELECT * FROM hotel WHERE hotel.available_from >= ?1 AND hotel.available_to <= ?2 AND hotel.ID NOT IN " + 15 | "(SELECT hotel_id FROM reservation WHERE (check_in >= ?1 OR check_out <= ?2))", nativeQuery = true) 16 | List findAllBetweenDates(@Param("dateFrom") String dateFrom, @Param("dateTo") String dateTo); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/service/HotelService.java: -------------------------------------------------------------------------------- 1 | 2 | package dev.marioszocs.hotelreservationapi.service; 3 | 4 | import dev.marioszocs.hotelreservationapi.dto.IdEntity; 5 | import dev.marioszocs.hotelreservationapi.dto.SuccessEntity; 6 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 7 | 8 | import java.util.List; 9 | 10 | public interface HotelService { 11 | 12 | List getHotelPagedList(Integer pageNo, Integer pageSize, String sortBy); // Pagination 13 | 14 | List getAllHotels(); 15 | 16 | Hotel getHotel(Integer id); 17 | 18 | List getAvailable(String dateFrom, String dateTo); 19 | 20 | IdEntity saveHotel(Hotel hotel); 21 | 22 | SuccessEntity deleteHotel(Integer id); 23 | 24 | SuccessEntity patchHotel(Hotel hotel); 25 | 26 | void doesReservationOverlap(Hotel hotel); 27 | 28 | boolean validateHotelExistenceById(Integer id); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/validator/PageNumberAndSizeValidator.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.validator; 2 | 3 | import dev.marioszocs.hotelreservationapi.constants.AppConstants; 4 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 5 | 6 | import static dev.marioszocs.hotelreservationapi.constants.ErrorMessages.*; 7 | 8 | public class PageNumberAndSizeValidator { 9 | 10 | public static void validatePageNumberAndSize(int page, int size) { 11 | if (page < 0) { 12 | throw new InvalidRequestException(PAGE_NUMBER_CANNOT_BE_LESS_THAN_ZERO); 13 | } 14 | 15 | if (size < 0) { 16 | throw new InvalidRequestException(SIZE_NUMBER_CANNOT_BE_LESS_THAN_ZERO); 17 | } 18 | 19 | if (size > Integer.parseInt(AppConstants.MAX_PAGE_SIZE)) { 20 | throw new InvalidRequestException(MAX_PAGE_SIZE_EXCEPTION); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/entity/Reservation.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.entity; 2 | 3 | import lombok.*; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.Table; 8 | import jakarta.validation.constraints.Max; 9 | import jakarta.validation.constraints.Min; 10 | 11 | 12 | @Getter 13 | @Setter 14 | @Entity 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @Table(name = "reservation") 19 | public class Reservation extends AuditableEntity { 20 | 21 | @Column(name = "hotel_id", nullable = false) 22 | private Integer hotelId; 23 | 24 | @Column(name = "check_in", nullable = false) 25 | private String checkIn; 26 | 27 | @Column(name = "check_out", nullable = false) 28 | private String checkOut; 29 | 30 | @Min(1) 31 | @Max(8) 32 | @Column(nullable = false) 33 | private Integer guests; 34 | 35 | @Column 36 | private boolean status; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # GENERAL APPLICATION SETTINGS 3 | ############################################################ 4 | spring.application.name=hotel-reservation-api 5 | server.port=8080 6 | spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER 7 | # Swagger UI path: http://localhost:8080/swagger-ui/index.html 8 | 9 | 10 | 11 | ############################################################ 12 | # SPRING LOGGING SETTINGS 13 | ############################################################ 14 | # Write logs to the current directory 15 | logging.file.path=. 16 | logging.file.name=hotel-reservation-api.log 17 | logging.level.dev.marioszocs.hotelreservationapi=debug 18 | 19 | 20 | 21 | 22 | ############################################################ 23 | # DEFAULT SPRING DATASOURCE SETTINGS for H2 24 | ############################################################ 25 | spring.datasource.driver-class-name=org.h2.Driver 26 | spring.datasource.url=jdbc:h2:mem:defaulth2db 27 | spring.datasource.username=sa 28 | spring.datasource.password= 29 | spring.jpa.hibernate.ddl-auto=create-drop 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/exception/HotelApiExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.exception; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | /** 10 | * Exception Handler for the Hotel Reservation API 11 | */ 12 | @Slf4j 13 | @ControllerAdvice 14 | public class HotelApiExceptionHandler { 15 | 16 | /** 17 | * Exception handler for invalid requests. 18 | * 19 | * @param e InvalidRequestException 20 | */ 21 | @ExceptionHandler(value = InvalidRequestException.class) 22 | public ResponseEntity handleInvalidRequest(InvalidRequestException e) { 23 | return handleBadRequest(e); 24 | } 25 | 26 | private ResponseEntity handleBadRequest(Exception e) { 27 | log.error(HttpStatus.BAD_REQUEST.getReasonPhrase(), e); // get full stacktrace 28 | return new ResponseEntity<>(new ApiErrorMessage(HttpStatus.BAD_REQUEST, e.getMessage()), HttpStatus.BAD_REQUEST); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/exception/ApiErrorMessage.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.exception; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import org.springframework.http.HttpStatus; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | /** 11 | * Model for the Error Messages 12 | */ 13 | @Data 14 | @AllArgsConstructor 15 | public class ApiErrorMessage { 16 | private HttpStatus status; // The status property holds the operation call status 17 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") 18 | private LocalDateTime timestamp; // The timestamp property holds the date-time instance when the error happened. 19 | private String message; // The message property holds a user-friendly message about the error. 20 | 21 | private ApiErrorMessage() { 22 | timestamp = LocalDateTime.now(); 23 | } 24 | 25 | ApiErrorMessage(HttpStatus status) { 26 | this(); 27 | this.status = status; 28 | } 29 | 30 | ApiErrorMessage(HttpStatus status, String message) { 31 | this(); 32 | this.status = status; 33 | this.message = message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/entity/Hotel.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.entity; 2 | 3 | import lombok.*; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.Table; 8 | import jakarta.validation.constraints.NotBlank; 9 | import jakarta.validation.constraints.NotEmpty; 10 | import jakarta.validation.constraints.Size; 11 | 12 | /** 13 | * Hotel Object 14 | */ 15 | 16 | @Entity 17 | @Builder 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @Table(name = "hotel") 23 | public class Hotel extends AuditableEntity { 24 | 25 | @NotBlank(message = "Hotel name is mandatory") 26 | @Size(min = 3, max = 40, message = "Name must be at least 3 characters long") 27 | @Column() 28 | private String name; 29 | 30 | //@NotEmpty 31 | @Column 32 | private ValidTypesOfHotelsEnum type = ValidTypesOfHotelsEnum.DELUXE; 33 | 34 | @NotBlank(message = "Description is mandatory") 35 | @Column 36 | private String description; 37 | 38 | @Column(name = "available_from") 39 | private String availableFrom; 40 | 41 | @Column(name = "available_to") 42 | private String availableTo; 43 | 44 | @Column(nullable = false) 45 | private boolean status; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/validator/ReservationValidator.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.validator; 2 | 3 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 4 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import static dev.marioszocs.hotelreservationapi.constants.ErrorMessages.INVALID_GUESTS; 8 | 9 | /** 10 | * Reservation Validator that ensures user inputs are correctly formatted 11 | */ 12 | @Slf4j 13 | public class ReservationValidator extends BaseValidator { 14 | 15 | /** 16 | * Validator for the Reservation POST call 17 | * 18 | * @param reservation 19 | */ 20 | public static void validateReservationPOST(Reservation reservation) { 21 | validateDates(reservation.getCheckIn(), reservation.getCheckOut()); 22 | validateGuest(reservation.getGuests()); 23 | } 24 | 25 | /** 26 | * Validator for the guest number 27 | * 28 | * @param guests 29 | */ 30 | public static void validateGuest(Integer guests) { 31 | if (guests == null || guests <= 0) { 32 | log.error("Invalid guests number: '{}', guests must be a non-zero number.", guests); 33 | throw new InvalidRequestException(INVALID_GUESTS); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/entity/AuditableEntity.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.data.annotation.CreatedDate; 7 | import org.springframework.data.annotation.LastModifiedDate; 8 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 9 | 10 | import jakarta.persistence.Column; 11 | import jakarta.persistence.EntityListeners; 12 | import jakarta.persistence.MappedSuperclass; 13 | import java.io.Serializable; 14 | import java.time.LocalDateTime; 15 | 16 | /** 17 | * This class adds support for entity listener. 18 | * It will track the creation date and modification date of the entity extending this class. 19 | */ 20 | @JsonIgnoreProperties(value = {"createdDate", "lastModifiedDate"}) 21 | @MappedSuperclass 22 | @Getter 23 | @Setter 24 | @EntityListeners(AuditingEntityListener.class) 25 | public abstract class AuditableEntity extends BaseEntity implements Serializable { 26 | 27 | @CreatedDate 28 | @Column(name = "created_date", columnDefinition = "TIMESTAMP", updatable = false) 29 | protected LocalDateTime createdDate; 30 | 31 | @LastModifiedDate 32 | @Column(name = "last_modified_date",columnDefinition = "TIMESTAMP") // if (..., nullable = false) than org.hibernate.PropertyValueException: not-null property references a null or transient value : dev.marioszocs.hotelreservationapi.entity.Hotel.lastModifiedDate 33 | private LocalDateTime lastModifiedDate; 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/validator/HotelValidator.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.validator; 2 | 3 | import dev.marioszocs.hotelreservationapi.constants.ErrorMessages; 4 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 5 | import dev.marioszocs.hotelreservationapi.entity.ValidTypesOfHotelsEnum; 6 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.util.Arrays; 11 | 12 | @Slf4j 13 | public class HotelValidator extends BaseValidator { 14 | 15 | /** 16 | * Validator for the Hotel POST call 17 | * 18 | * @param hotel 19 | */ 20 | public static void validateHotelPOST(Hotel hotel) { 21 | validateName(hotel.getName()); 22 | validateType(hotel.getType()); 23 | validateDates(hotel.getAvailableFrom(), hotel.getAvailableTo()); 24 | } 25 | 26 | /** 27 | * Validator for the Hotel PATCH call 28 | * 29 | * @param hotel 30 | */ 31 | public static void validateHotelPATCH(Hotel hotel) { 32 | validateId(hotel.getId()); 33 | validateName(hotel.getName()); 34 | validateType(hotel.getType()); 35 | validateDates(hotel.getAvailableFrom(), hotel.getAvailableTo()); 36 | } 37 | 38 | /** 39 | * Validator for the Hotel name 40 | * 41 | * @param name 42 | */ 43 | public static void validateName(String name) { 44 | if (!StringUtils.hasText(name)) { 45 | log.error("Hotel name cannot be null..."); 46 | throw new InvalidRequestException(ErrorMessages.INVALID_NAME); 47 | } 48 | } 49 | 50 | /** 51 | * Validator for the Hotel type 52 | * 53 | * @param type 54 | */ 55 | public static void validateType(ValidTypesOfHotelsEnum type) { 56 | if (type == null || !Arrays.asList("DELUXE", "LUXURY", "SUITE").contains(type.toString())) { 57 | log.error("The type parameter: '{}' is invalid, must be one of the following [DELUXE, LUXURY, SUITE]", type); 58 | throw new InvalidRequestException(ErrorMessages.INVALID_TYPE); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/constants/ErrorMessages.java: -------------------------------------------------------------------------------- 1 | 2 | package dev.marioszocs.hotelreservationapi.constants; 3 | 4 | /** 5 | * Collection of Error Messages for Invalid Requests 6 | */ 7 | public final class ErrorMessages { 8 | public static final String INVALID_NAME = "Invalid Name: Name cannot be null."; 9 | public static final String INVALID_TYPE = "Invalid Type: Must be one of the following [DELUXE, LUXURY, SUITE]."; 10 | public static final String INVALID_DATE = "Invalid date: Please input dates in 'yyyy-MM-dd' format."; 11 | public static final String INVALID_DATE_ORDER = "Invalid date: Start date must be before end date."; 12 | public static final String INVALID_RESERVATION_DATES = "Invalid Reservation Dates: The check in and/or check out dates are outside the available dates."; 13 | public static final String INVALID_DATE_OVERLAP = "Invalid Date: The entered date overlaps with an already registered reservation."; 14 | public static final String INVALID_DATE_NULL_VALUES = "Invalid Date: If a date is entered, both the 'startDate' and the 'endDate' must be provided."; 15 | public static final String EMPTY_HOTEL_DATES = "Invalid Hotel: A reservation can not be made as the user specified hotel does not have available dates."; 16 | public static final String INVALID_DATE_CHANGE_NULL = "Invalid Date Change: Hotel contains a reservation, therefore null hotel dates cannot be entered."; 17 | public static final String INVALID_HOTEL_ID = "Invalid Hotel ID: Hotel ID can not be null"; 18 | public static final String INVALID_HOTEL_IN_RESERVATION = "Invalid Hotel ID: The Hotel ID does not exist"; 19 | public static final String INVALID_ID_EXISTENCE = "Invalid ID: The id entered does not exist"; 20 | public static final String INVALID_GUESTS = "Invalid Guests: Guests must be a non-zero number"; 21 | public static final String PARSE_ERROR = "Internal Error: A parsing error occurred."; 22 | public static final String INVALID_HOTEL_DELETE = "Invalid Request: Cannot delete hotel as there are active reservations."; 23 | public static final String INVALID_HOTEL_UPDATE = "Invalid Hotel Update: Cannot update user specified hotel as the new dates conflict with an active reservation."; 24 | 25 | // Page number and size validation 26 | public static final String PAGE_NUMBER_CANNOT_BE_LESS_THAN_ZERO = "Page number cannot be less than zero."; 27 | public static final String SIZE_NUMBER_CANNOT_BE_LESS_THAN_ZERO = "Size number cannot be less than zero."; 28 | public static final String MAX_PAGE_SIZE_EXCEPTION = "Page size must not be greater than: " + AppConstants.MAX_PAGE_SIZE; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/controller/ReservationController.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.controller; 2 | 3 | import dev.marioszocs.hotelreservationapi.dto.IdEntity; 4 | import dev.marioszocs.hotelreservationapi.dto.SuccessEntity; 5 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 6 | import dev.marioszocs.hotelreservationapi.service.ReservationService; 7 | import dev.marioszocs.hotelreservationapi.validator.ReservationValidator; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Reservation Controller containing endpoints of Reservation related API calls 16 | */ 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | @RestController 20 | @RequestMapping("/api/v1") 21 | public class ReservationController { 22 | private final ReservationService reservationService; 23 | 24 | /** 25 | * End point to get all reservations. 26 | * 27 | * @return list of Reservations 28 | */ 29 | @GetMapping(value = "/reservations", produces = "application/json") 30 | public List getReservationList(){ 31 | log.info("Get all reservations..."); 32 | return reservationService.getAllReservations(); 33 | } 34 | //jUnit, integrationtest, mockito, hibernate, lombok, jpa, swagger 35 | /** 36 | * End point to get user specified reservation. 37 | * 38 | * @param id Integer 39 | * @return Reservation object 40 | */ 41 | @GetMapping(value = "/reservation/{id}", produces = "application/json") 42 | public Reservation getReservation(@PathVariable Integer id){ 43 | ReservationValidator.validateId(id); 44 | log.info("Get a user specified reservation with id = {}", id); 45 | return reservationService.getReservation(id); 46 | } 47 | 48 | /** 49 | * End point to update user specified Reservation 50 | * 51 | * @param reservation 52 | * @return 53 | */ 54 | @PostMapping(value = "/reservation", produces = "application/json") 55 | public IdEntity saveReservation(@RequestBody Reservation reservation){ 56 | ReservationValidator.validateReservationPOST(reservation); 57 | log.info("Save a user specified reservation..."); 58 | return reservationService.saveReservation(reservation); 59 | } 60 | 61 | /** 62 | * End point to delete user specified Reservation. 63 | * 64 | * @param id Integer 65 | * @return successEntity 66 | */ 67 | @DeleteMapping(value = "/reservation/{id}", produces = "application/json") 68 | public SuccessEntity deleteReservation(@PathVariable Integer id){ 69 | ReservationValidator.validateId(id); 70 | log.info("Delete a user specified reservation..."); 71 | return reservationService.deleteReservation(id); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/validator/BaseValidator.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.validator; 2 | 3 | import dev.marioszocs.hotelreservationapi.constants.ErrorMessages; 4 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.text.DateFormat; 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | 11 | import static dev.marioszocs.hotelreservationapi.constants.ErrorMessages.*; 12 | 13 | /** 14 | * Base Validator that ensures user inputs are correctly formatted 15 | */ 16 | @Slf4j 17 | public class BaseValidator { 18 | 19 | private static final String VALID_DATE_FORMAT = "yyyy-MM-dd"; 20 | private static final DateFormat simpleDateFormat = new SimpleDateFormat(VALID_DATE_FORMAT); 21 | 22 | /** 23 | * Validator for the Hotel id 24 | * 25 | * @param id 26 | */ 27 | public static void validateId(Integer id) { 28 | if (id == 0) { 29 | throw new InvalidRequestException(ErrorMessages.INVALID_HOTEL_ID); 30 | } 31 | } 32 | 33 | /** 34 | * Validator for date format (yyyy-MM-dd) 35 | * 36 | * @param date 37 | * @return 38 | */ 39 | public static boolean validateDateFormat(String date) { 40 | simpleDateFormat.setLenient(false); 41 | try { 42 | simpleDateFormat.parse(date); 43 | } catch (ParseException e) { 44 | log.error("Invalid date format: '{}', please input dates in 'yyyy-MM-dd' format.", date); 45 | throw new InvalidRequestException(INVALID_DATE); 46 | // return false; 47 | } 48 | return true; 49 | } 50 | 51 | /** 52 | * Validator for 'startDate' and 'endDate' 53 | * 54 | * @param startDate 55 | * @param endDate 56 | */ 57 | public static void validateDates(String startDate, String endDate) { 58 | // Checks to see if one of the dates are null and throws an exception if only one date has been entered. 59 | if (startDate == null || endDate == null) { 60 | throw new InvalidRequestException(INVALID_DATE_NULL_VALUES); 61 | } 62 | if (validateDateFormat(startDate) && validateDateFormat(endDate)) { 63 | try { 64 | if (simpleDateFormat.parse(startDate).after(simpleDateFormat.parse(endDate))) { 65 | log.error("Start date: '{}' mus be before end date: '{}'.", startDate, endDate); 66 | throw new InvalidRequestException(INVALID_DATE_ORDER); 67 | } 68 | } catch (ParseException e) { 69 | log.debug("Invalid date comparison."); 70 | } 71 | } else { 72 | log.error("Invalid date: Please input dates in 'yyyy-MM-dd' format."); 73 | throw new InvalidRequestException(INVALID_DATE); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/dev/marioszocs/hotelreservationapi/serviceImp/HotelServiceImpMockitoTest.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.serviceImp; 2 | 3 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 4 | import dev.marioszocs.hotelreservationapi.entity.ValidTypesOfHotelsEnum; 5 | import dev.marioszocs.hotelreservationapi.repository.HotelRepository; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertAll; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.mockito.Mockito.*; 19 | 20 | class HotelServiceImpMockitoTest { 21 | @Mock 22 | HotelRepository hotelRepository; 23 | 24 | @InjectMocks 25 | HotelServiceImp hotelServiceImp; 26 | 27 | static List hotelList = new ArrayList<>(); 28 | 29 | @BeforeEach 30 | public void initMocks() { 31 | MockitoAnnotations.openMocks(this); 32 | } 33 | 34 | @BeforeAll 35 | public static void loadHotelList() { 36 | Hotel hotel = Hotel.builder() 37 | .name("Hilton Hotel") 38 | .description("5* Hotel...") 39 | .availableFrom("2023-01-01") 40 | .availableTo("2023-12-31") 41 | .type(ValidTypesOfHotelsEnum.DELUXE) 42 | .status(true) 43 | .build(); 44 | 45 | Hotel hotel2 = Hotel.builder() 46 | .name("Four Season Hotel") 47 | .description("5* Hotel...") 48 | .availableFrom("2023-01-01") 49 | .availableTo("2023-12-31") 50 | .type(ValidTypesOfHotelsEnum.LUXURY) 51 | .status(true) 52 | .build(); 53 | 54 | hotelList.add(hotel); 55 | hotelList.add(hotel2); 56 | } 57 | 58 | @Test 59 | void getAllHotelsTest_Valid() { 60 | 61 | // when - action or behaviour that we are going test 62 | when(hotelRepository.findAll()).thenReturn(hotelList); 63 | 64 | assertEquals(2, hotelServiceImp.getAllHotels().size()); 65 | 66 | verify(hotelRepository, times(1)).findAll(); 67 | } 68 | 69 | 70 | @Test 71 | void saveHotelTest_Valid() { 72 | Hotel hotel3 = Hotel.builder() 73 | .name("Hilton Hotel") 74 | .description("5* Hotel...") 75 | .availableFrom("2023-01-01") 76 | .availableTo("2023-12-31") 77 | .type(ValidTypesOfHotelsEnum.DELUXE) 78 | .status(true) 79 | .build(); 80 | 81 | when(hotelRepository.save(hotel3)).thenReturn(hotel3); 82 | 83 | // then - verify the result or output using assert statements 84 | assertAll( 85 | () -> assertEquals("Hilton Hotel", hotel3.getName()), 86 | () -> assertEquals("5* Hotel...", hotel3.getDescription()), 87 | () -> assertEquals(ValidTypesOfHotelsEnum.DELUXE, hotel3.getType()) 88 | ); 89 | } 90 | 91 | 92 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project Specific 2 | /build/www/** 3 | /src/test/javascript/coverage/ 4 | /src/test/javascript/PhantomJS*/ 5 | 6 | # Node 7 | /node/ 8 | node_tmp/ 9 | node_modules/ 10 | npm-debug.log.* 11 | 12 | # SASS 13 | .sass-cache/ 14 | 15 | # STS 16 | .apt_generated 17 | .classpath 18 | .project 19 | .settings 20 | .springBeans 21 | .sts4-cache 22 | 23 | # Eclipse 24 | .metadata 25 | bin/ 26 | tmp/ 27 | *.tmp 28 | *.bak 29 | *.swp 30 | *~.nib 31 | local.properties 32 | .settings/ 33 | .loadpath 34 | .recommenders 35 | 36 | # External tool builders 37 | .externalToolBuilders/ 38 | 39 | # Locally stored "Eclipse launch configurations" 40 | *.launch 41 | 42 | # PyDev specific (Python IDE for Eclipse) 43 | *.pydevproject 44 | 45 | # CDT-specific (C/C++ Development Tooling) 46 | .cproject 47 | 48 | # CDT- autotools 49 | .autotools 50 | 51 | # Java annotation processor (APT) 52 | .factorypath 53 | 54 | # PDT-specific (PHP Development Tools) 55 | .buildpath 56 | 57 | # sbteclipse plugin 58 | .target 59 | 60 | # Tern plugin 61 | .tern-project 62 | 63 | # TeXlipse plugin 64 | .texlipse 65 | 66 | # Code Recommenders 67 | .recommenders/ 68 | 69 | # Annotation Processing 70 | .apt_generated/ 71 | 72 | # Scala IDE specific (Scala & Java development for Eclipse) 73 | .cache-main 74 | .scala_dependencies 75 | .worksheet 76 | 77 | # External tool builders 78 | .externalToolBuilders/** 79 | 80 | # Intellij 81 | .idea/ 82 | *.iml 83 | *.iws 84 | *.ipr 85 | *.ids 86 | 87 | # Visual Studio Code 88 | .vscode/ 89 | 90 | # Maven 91 | /log/ 92 | */target/** 93 | target/** 94 | pom.xml.tag 95 | pom.xml.releaseBackup 96 | pom.xml.versionsBackup 97 | pom.xml.next 98 | release.properties 99 | dependency-reduced-pom.xml 100 | buildNumber.properties 101 | .mvn/timing.properties 102 | .mvn/wrapper/maven-wrapper.jar 103 | 104 | # Gradle 105 | .gradle/ 106 | /build/ 107 | 108 | # Package Files 109 | *.jar 110 | *.war 111 | *.nar 112 | *.ear 113 | *.zip 114 | *.tar.gz 115 | *.rar 116 | *.db 117 | 118 | # Windows - thumbnail cache files 119 | Thumbs.db 120 | ehthumbs.db 121 | ehthumbs_vista.db 122 | 123 | # Windows - Dump file 124 | *.stackdump 125 | 126 | # Windows - Folder config file 127 | Desktop.ini 128 | 129 | # Windows - Recycle Bin used on file shares 130 | $RECYCLE.BIN/ 131 | 132 | # Windows - Installer files 133 | *.cab 134 | *.msi 135 | *.msix 136 | *.msm 137 | *.msp 138 | 139 | # Windows - Shortcuts 140 | *.lnk 141 | 142 | # Mac OSX 143 | .DS_Store 144 | .svn 145 | 146 | # Thumbnails 147 | ._* 148 | 149 | # Files that might appear on external disk 150 | .Spotlight-V100 151 | .Trashes 152 | 153 | # Directories 154 | /bin/ 155 | /deploy/ 156 | 157 | # Logs 158 | *.log 159 | *.log.* 160 | *.gz 161 | 162 | # Others 163 | *.class 164 | *.*~ 165 | *~ 166 | .merge_file* 167 | 168 | # Gradle Wrapper 169 | !gradle/wrapper/gradle-wrapper.jar 170 | 171 | # Maven Wrapper 172 | !.mvn/wrapper/maven-wrapper.jar 173 | 174 | # ESLint 175 | .eslintcache 176 | /.idea/ 177 | 178 | # Git 179 | *.orig 180 | 181 | # Created by git when using merge tools for conflicts 182 | *.BACKUP.* 183 | *.BASE.* 184 | *.LOCAL.* 185 | *.REMOTE.* 186 | *_BACKUP_*.txt 187 | *_BASE_*.txt 188 | *_LOCAL_*.txt 189 | *_REMOTE_*.txt -------------------------------------------------------------------------------- /src/test/java/dev/marioszocs/hotelreservationapi/validator/HotelValidatorTest.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.validator; 2 | 3 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 4 | import dev.marioszocs.hotelreservationapi.entity.ValidTypesOfHotelsEnum; 5 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** 11 | * Testing the HotelValidator class... 12 | */ 13 | class HotelValidatorTest { 14 | 15 | @Test 16 | void validateHotelPOST_ValidCase() { 17 | Hotel hotel = Hotel.builder() 18 | .name("Hilton Hotel") 19 | .description("5* Hotel...") 20 | .availableFrom("2023-01-01") 21 | .availableTo("2023-12-31") 22 | .type(ValidTypesOfHotelsEnum.DELUXE) 23 | .status(true) 24 | .build(); 25 | Assertions.assertDoesNotThrow(() -> HotelValidator.validateHotelPOST(hotel)); 26 | } 27 | 28 | @Test 29 | void validateHotelPOST_InvalidCase() { 30 | Hotel hotel = Hotel.builder() 31 | .name(" ") 32 | .description("5* Hotel...") 33 | .availableFrom("2023-01-01") 34 | .availableTo("2023-12-31") 35 | .type(ValidTypesOfHotelsEnum.DELUXE) 36 | .status(true) 37 | .build(); 38 | Assertions.assertThrows(InvalidRequestException.class, () -> HotelValidator.validateHotelPOST(hotel)); 39 | } 40 | 41 | @Test 42 | void validateHotelPATCH_ValidCase() { 43 | Hotel hotel = Hotel.builder() 44 | .name("Hilton Hotel") 45 | .description("5* Hotel...") 46 | .availableFrom("2023-01-01") 47 | .availableTo("2023-12-31") 48 | .type(ValidTypesOfHotelsEnum.DELUXE) 49 | .status(true) 50 | .build(); 51 | Assertions.assertDoesNotThrow(() -> HotelValidator.validateHotelPOST(hotel)); 52 | } 53 | 54 | @Test 55 | void validateId_ValidCase() { 56 | Integer id = 1; 57 | Assertions.assertDoesNotThrow(() -> HotelValidator.validateId(id)); 58 | } 59 | 60 | @Test 61 | void validateId_InvalidCase1() { 62 | Integer id = null; 63 | Assertions.assertThrows(NullPointerException.class, () -> HotelValidator.validateId(id)); 64 | } 65 | 66 | @Test 67 | void validateName_ValidCase() { 68 | String name = "Hilton Hotel"; 69 | Assertions.assertDoesNotThrow(() -> HotelValidator.validateName(name)); 70 | } 71 | 72 | @DisplayName("Invalid Hotel name testing...") 73 | @Test 74 | void validateName_InvalidCase() { 75 | String name = null; 76 | Assertions.assertThrows(InvalidRequestException.class, () -> HotelValidator.validateName(name)); 77 | } 78 | 79 | @Test 80 | void validateDates_ValidCase1() { 81 | String from = "2023-01-01"; 82 | String to = "2024-01-01"; 83 | Assertions.assertDoesNotThrow(() -> HotelValidator.validateDates(from, to)); 84 | } 85 | 86 | @Test 87 | void validateDates_InvalidCase1() { 88 | String from = "2023-01-01"; 89 | String to = "2022-01-01"; 90 | Assertions.assertThrows(InvalidRequestException.class, () -> HotelValidator.validateDates(from, to)); 91 | } 92 | 93 | @Test 94 | void validateDates_InvalidCase2() { 95 | String from = "Hello"; 96 | String to = "2022-01-01"; 97 | Assertions.assertThrows(InvalidRequestException.class, () -> HotelValidator.validateDates(from, to)); 98 | } 99 | 100 | @Test 101 | void validateDateFormat_ValidCase() { 102 | String validDate = "2023-01-01"; 103 | Assertions.assertDoesNotThrow(() -> HotelValidator.validateDateFormat(validDate)); 104 | } 105 | 106 | @Test 107 | void validateDateFormat_InvalidCase() { 108 | String invalidDate = "2023/01/01"; 109 | Assertions.assertThrows(InvalidRequestException.class, () -> HotelValidator.validateDateFormat(invalidDate)); 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/bootstrap/HotelsAndReservationsLoader.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.bootstrap; 2 | 3 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 4 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 5 | import dev.marioszocs.hotelreservationapi.entity.ValidTypesOfHotelsEnum; 6 | import dev.marioszocs.hotelreservationapi.repository.HotelRepository; 7 | import dev.marioszocs.hotelreservationapi.repository.ReservationRepository; 8 | import lombok.AllArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.boot.CommandLineRunner; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Slf4j 14 | @Component 15 | @AllArgsConstructor 16 | public class HotelsAndReservationsLoader implements CommandLineRunner { 17 | 18 | private final HotelRepository hotelRepository; 19 | private final ReservationRepository reservationRepository; 20 | 21 | @Override 22 | public void run(String... args) throws Exception { 23 | 24 | if (hotelRepository.count() == 0) { 25 | log.info("Loading data from Hotels..."); 26 | loadHotelObject(); 27 | } 28 | 29 | if (reservationRepository.count() == 0) { 30 | log.info("Loading data from Reservations..."); 31 | loadReservationObject(); 32 | } 33 | 34 | 35 | } 36 | 37 | private void loadReservationObject() { 38 | Reservation r1 = Reservation.builder() 39 | .hotelId(1) 40 | .checkIn("2019-01-01") 41 | .checkOut("2019-12-31") 42 | .guests(2) 43 | .build(); 44 | 45 | Reservation r2 = Reservation.builder() 46 | .hotelId(2) 47 | .checkIn("2020-01-01") 48 | .checkOut("2020-12-31") 49 | .guests(3) 50 | .build(); 51 | 52 | Reservation r3 = Reservation.builder() 53 | .hotelId(3) 54 | .checkIn("2021-01-01") 55 | .checkOut("2021-12-31") 56 | .guests(4) 57 | .build(); 58 | 59 | Reservation r4 = Reservation.builder() 60 | .hotelId(4) 61 | .checkIn("2022-01-01") 62 | .checkOut("2022-12-31") 63 | .guests(5) 64 | .build(); 65 | 66 | Reservation r5 = Reservation.builder() 67 | .hotelId(5) 68 | .checkIn("2023-01-01") 69 | .checkOut("2023-12-31") 70 | .guests(6) 71 | .build(); 72 | 73 | reservationRepository.save(r1); 74 | reservationRepository.save(r2); 75 | reservationRepository.save(r3); 76 | reservationRepository.save(r4); 77 | reservationRepository.save(r5); 78 | 79 | log.info("Loaded Reservations: " + reservationRepository.count()); 80 | } 81 | 82 | private void loadHotelObject() { 83 | Hotel h1 = Hotel.builder() 84 | .name("Falkensteiner Hotel Bratislava") 85 | .type(ValidTypesOfHotelsEnum.SUITE) 86 | .description("Falkensteiner") 87 | .availableFrom("2019-01-01") 88 | .availableTo("2019-12-31") 89 | .status(true) 90 | .build(); 91 | 92 | Hotel h2 = Hotel.builder() 93 | .name("Park Inn by Radisson Danube Bratislava") 94 | .type(ValidTypesOfHotelsEnum.SUITE) 95 | .description("Park Inn") 96 | .availableFrom("2020-01-01") 97 | .availableTo("2020-12-31") 98 | .status(true) 99 | .build(); 100 | 101 | Hotel h3 = Hotel.builder() 102 | .name("Radisson Blu Carlton Hotel Bratislava") 103 | .type(ValidTypesOfHotelsEnum.DELUXE) 104 | .description("Radisson Blu Carlton") 105 | .availableFrom("2021-01-01") 106 | .availableTo("2021-12-31") 107 | .status(true) 108 | .build(); 109 | 110 | Hotel h4 = Hotel.builder() 111 | .name("Boutique Hotel Bratislava") 112 | .type(ValidTypesOfHotelsEnum.SUITE) 113 | .description("Boutique Hotel") 114 | .availableFrom("2022-01-01") 115 | .availableTo("2022-12-31") 116 | .status(true) 117 | .build(); 118 | 119 | Hotel h5 = Hotel.builder() 120 | .name("Sheraton Bratislava Hotel") 121 | .type(ValidTypesOfHotelsEnum.LUXURY) 122 | .description("Sheraton") 123 | .availableFrom("2023-01-01") 124 | .availableTo("2023-12-31") 125 | .status(true) 126 | .build(); 127 | 128 | hotelRepository.save(h1); 129 | hotelRepository.save(h2); 130 | hotelRepository.save(h3); 131 | hotelRepository.save(h4); 132 | hotelRepository.save(h5); 133 | 134 | log.info("Loaded Hotels: " + hotelRepository.count()); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/controller/HotelController.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.controller; 2 | 3 | import dev.marioszocs.hotelreservationapi.constants.AppConstants; 4 | import dev.marioszocs.hotelreservationapi.dto.IdEntity; 5 | import dev.marioszocs.hotelreservationapi.dto.SuccessEntity; 6 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 7 | import dev.marioszocs.hotelreservationapi.service.HotelService; 8 | import dev.marioszocs.hotelreservationapi.validator.HotelValidator; 9 | import dev.marioszocs.hotelreservationapi.validator.PageNumberAndSizeValidator; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import jakarta.validation.Valid; 17 | import java.util.List; 18 | 19 | /** 20 | * Hotel Controller containing endpoints of Hotel related API calls 21 | */ 22 | @Slf4j 23 | @RequiredArgsConstructor 24 | @RestController 25 | @RequestMapping("/api/v1") 26 | public class HotelController { 27 | private final HotelService hotelService; 28 | 29 | /** 30 | * End point to get all Hotels in the database 31 | * 32 | * @return list of Hotels 33 | */ 34 | @GetMapping(value = "/hotels", produces = "application/json") 35 | public ResponseEntity> getHotelList(){ 36 | log.info("Get all: {} hotels from database", hotelService.getAllHotels().size()); 37 | return ResponseEntity.ok(hotelService.getAllHotels()); 38 | } 39 | 40 | /** 41 | * End point to get Hotel paged list 42 | * 43 | * @param pageNumber 44 | * @param pageSize 45 | * @param sortBy 46 | * @return 47 | */ 48 | @GetMapping(value = "/hotelPagedList", produces = "application/json") 49 | public ResponseEntity> getPagedHotelList( 50 | @RequestParam(name = "pageNumber", required = false, defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) Integer pageNumber, 51 | @RequestParam(name = "pageSize", required = false, defaultValue = AppConstants.DEFAULT_PAGE_SIZE) Integer pageSize, 52 | @RequestParam(name = "sortBy", required = false, defaultValue = AppConstants.DEFAULT_SORTING_PARAM) String sortBy) { 53 | 54 | PageNumberAndSizeValidator.validatePageNumberAndSize(pageNumber, pageSize); 55 | List hotelPagedList = hotelService.getHotelPagedList(pageNumber, pageSize, sortBy); 56 | 57 | log.info("Return Hotel paged list with pageNumber: {}, pageSize: {} and sortBy: {}.", pageNumber, pageSize, sortBy); 58 | 59 | return new ResponseEntity<>(hotelPagedList, HttpStatus.OK); 60 | } 61 | 62 | /** 63 | * End point to get user specified Hotel 64 | * 65 | * @param id Integer 66 | * @return 67 | */ 68 | @GetMapping(value = "/hotel/{id}", produces = "application/json") 69 | public Hotel getHotel(@PathVariable Integer id) { 70 | HotelValidator.validateId(id); 71 | log.info("Get hotel by id = {}", id); 72 | return hotelService.getHotel(id); 73 | } 74 | 75 | /** 76 | * End point to get list of Hotels available between user specified dates 77 | * 78 | * @param from String 79 | * @param to String 80 | * @return list of Hotels 81 | */ 82 | @GetMapping(value = "/hotels/availabilitySearch", produces = "application/json") 83 | public List getHotel(@RequestParam("dateFrom") String from, @RequestParam("dateTo") String to){ 84 | HotelValidator.validateDates(from, to); 85 | log.info("Get all Hotels available between dates from: {} to: {}", from, to); 86 | return hotelService.getAvailable(from, to); 87 | } 88 | 89 | /** 90 | * End point to update user specified Hotel. 91 | * 92 | * @param hotel Hotel 93 | * @return successEntity 94 | */ 95 | @PatchMapping(value = "/hotel", produces = "application/json") 96 | public SuccessEntity patchHotel(@RequestBody @Valid Hotel hotel){ 97 | HotelValidator.validateHotelPATCH(hotel); 98 | log.info("Update Hotel with name: {}", hotel.getName()); 99 | return hotelService.patchHotel(hotel); 100 | } 101 | 102 | /** 103 | * End point to save a user specified hotel 104 | * 105 | * @param hotel Hotel 106 | * @return idEntity 107 | */ 108 | @PostMapping(value = "/hotel", produces = "application/json") 109 | public IdEntity saveHotel(@RequestBody @Valid Hotel hotel){ 110 | HotelValidator.validateHotelPOST(hotel); 111 | log.info("Save a user specified hotel with name: {}", hotel.getName()); 112 | return hotelService.saveHotel(hotel); 113 | } 114 | 115 | /** 116 | * End point to delete a user specified hotel 117 | * 118 | * @param id Integer 119 | * @return SuccessEntity 120 | */ 121 | @DeleteMapping(value = "/hotel/{id}", produces = "application/json") 122 | public SuccessEntity deleteHotel(@PathVariable Integer id){ 123 | HotelValidator.validateId(id); 124 | log.info("Delete a user specified hotel with id = {}", id); 125 | return hotelService.deleteHotel(id); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.1 9 | 10 | 11 | 12 | dev.marioszocs 13 | hotel-reservation-api 14 | 0.0.1-SNAPSHOT 15 | Hotel Reservation API 16 | 17 | The Hotel Reservation API is a Spring Boot application designed to manage hotel reservations. 18 | It provides RESTful endpoints for creating, updating, retrieving, and deleting hotel and reservation data. 19 | The application uses Spring Data JPA for database interactions and supports validation using Spring Boot Starter 20 | Validation. 21 | It includes unit tests with Mockito and JUnit to ensure the reliability of the service. 22 | 23 | 24 | 25 | 21 26 | 21 27 | UTF-8 28 | 21 29 | 8.0.33 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-dependencies 38 | 3.4.1 39 | pom 40 | import 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-web 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-tomcat 55 | provided 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-data-jpa 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-validation 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-test 69 | test 70 | 71 | 72 | 73 | org.projectlombok 74 | lombok 75 | true 76 | 77 | 78 | 79 | mysql 80 | mysql-connector-java 81 | ${mysql.version} 82 | runtime 83 | 84 | 85 | 86 | com.h2database 87 | h2 88 | 89 | runtime 90 | 91 | 92 | 93 | org.junit.jupiter 94 | junit-jupiter-engine 95 | test 96 | 97 | 98 | 99 | org.springdoc 100 | springdoc-openapi-starter-webmvc-ui 101 | 2.7.0 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-maven-plugin 111 | 112 | 113 | 114 | org.projectlombok 115 | lombok 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | h2 127 | 128 | 129 | com.h2database 130 | h2 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/serviceImp/ReservationServiceImp.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.serviceImp; 2 | 3 | import dev.marioszocs.hotelreservationapi.constants.ErrorMessages; 4 | import dev.marioszocs.hotelreservationapi.dto.IdEntity; 5 | import dev.marioszocs.hotelreservationapi.dto.SuccessEntity; 6 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 7 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 8 | import dev.marioszocs.hotelreservationapi.repository.HotelRepository; 9 | import dev.marioszocs.hotelreservationapi.repository.ReservationRepository; 10 | import dev.marioszocs.hotelreservationapi.service.ReservationService; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Service; 14 | 15 | import jakarta.transaction.Transactional; 16 | import java.text.ParseException; 17 | import java.text.SimpleDateFormat; 18 | import java.util.List; 19 | 20 | /** 21 | * Reservation Service tha performs operations regarding Reservation API Calls 22 | */ 23 | @Slf4j 24 | @Transactional 25 | @RequiredArgsConstructor 26 | @Service 27 | public class ReservationServiceImp implements ReservationService { 28 | private final ReservationRepository reservationRepository; 29 | private final HotelRepository hotelRepository; 30 | 31 | /** 32 | * Returns all existing Reservation objects in the database 33 | * @return 34 | */ 35 | @Override 36 | public List getAllReservations() { 37 | return reservationRepository.findAll(); 38 | } 39 | 40 | /** 41 | * Finds a user specified Reservation in the database 42 | * @param id 43 | * @return 44 | */ 45 | @Override 46 | public Reservation getReservation(Integer id) { 47 | validateReservationExistence(id); 48 | return reservationRepository.findById(id).get(); 49 | } 50 | 51 | /** 52 | * Saves a user created Reservation object to the database 53 | * @param reservations 54 | * @return 55 | */ 56 | @Override 57 | public IdEntity saveReservation(Reservation reservations) { 58 | Integer reservationsInventoryId = reservations.getHotelId(); 59 | 60 | //boolean to determine if the Reservation is valid through the existence of the inventory ID. 61 | //if the inventory ID exists, then continue 62 | if (validateHotelExistenceById(reservationsInventoryId)) { 63 | //boolean to determine if there is already a pre-existing reservation that overlaps with the users 64 | if (reservationOverlaps(reservations)) { 65 | throw new InvalidRequestException(ErrorMessages.INVALID_DATE_OVERLAP); 66 | } 67 | //determine if the dates are out of the Inventory's bounds //TODO: getById is deprecated 68 | if (dateIsBefore(hotelRepository.getById(reservations.getHotelId()).getAvailableFrom(), reservations.getCheckIn()) && dateIsBefore(reservations.getCheckOut(), hotelRepository.getById(reservations.getHotelId()).getAvailableTo())) { 69 | reservations = reservationRepository.save(reservations); 70 | IdEntity idEntity = new IdEntity(); 71 | idEntity.setId(reservations.getId()); 72 | return idEntity; 73 | } else { 74 | throw new InvalidRequestException(ErrorMessages.INVALID_RESERVATION_DATES); 75 | } 76 | } else { 77 | //Throw error if the Inventory ID does not exist 78 | throw new InvalidRequestException(ErrorMessages.INVALID_HOTEL_IN_RESERVATION); 79 | } 80 | } 81 | 82 | /** 83 | * Deletes a user specified Reservation object from the database 84 | * 85 | * @param id 86 | * @return 87 | */ 88 | @Override 89 | public SuccessEntity deleteReservation(Integer id) { 90 | validateReservationExistence(id); 91 | reservationRepository.deleteById(id); 92 | SuccessEntity successEntity = new SuccessEntity(); 93 | successEntity.setSuccess(!reservationRepository.existsById(id)); 94 | return successEntity; 95 | } 96 | 97 | /** 98 | * Checks to existene of a Hotel object in the database 99 | * @param id 100 | * @return 101 | */ 102 | @Override 103 | public boolean validateHotelExistenceById(Integer id) { 104 | if (!hotelRepository.existsById(id)) { 105 | throw new InvalidRequestException(ErrorMessages.INVALID_ID_EXISTENCE); 106 | } else if (hotelRepository.getById(id).getAvailableFrom() == null && hotelRepository.getById(id).getAvailableTo() == null) { //TODO getById is deprecated 107 | //Checks if the inventory has available to and available from dates, if not then throw an error as a reservation cannot be made. 108 | throw new InvalidRequestException(ErrorMessages.EMPTY_HOTEL_DATES); 109 | } else { 110 | return true; 111 | } 112 | } 113 | 114 | /** 115 | * Checks the chronological order of user specified dates 116 | * 117 | * @param date1 118 | * @param date2 119 | * @return 120 | */ 121 | @Override 122 | public boolean dateIsBefore(String date1, String date2) { 123 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); 124 | try { 125 | return simpleDateFormat.parse(date1).before(simpleDateFormat.parse(date2)); 126 | } catch (ParseException e) { 127 | throw new InvalidRequestException(ErrorMessages.PARSE_ERROR); 128 | } 129 | } 130 | 131 | /** 132 | * Checks to see if a user specified Reservation overlaps with a pre-existing Reservation in the database 133 | * 134 | * @param reservations 135 | * @return 136 | */ 137 | @Override 138 | public boolean reservationOverlaps(Reservation reservations) { 139 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); 140 | 141 | return reservationRepository.findAll().stream().anyMatch(dataBaseRes -> { 142 | //If two reservations have the same inventory id, then compare their check in and checkout dates 143 | if (dataBaseRes.getHotelId() == reservations.getHotelId()) { 144 | try { 145 | int checkInBeforeDbCheckOut = sdf.parse(reservations.getCheckIn()).compareTo(sdf.parse(dataBaseRes.getCheckOut())); 146 | int checkOutBeforeDbCheckIn = sdf.parse(reservations.getCheckOut()).compareTo(sdf.parse(dataBaseRes.getCheckIn())); 147 | log.debug("check in int " + checkInBeforeDbCheckOut); 148 | log.debug("check out int " + checkOutBeforeDbCheckIn); 149 | if (checkInBeforeDbCheckOut == 0 || checkOutBeforeDbCheckIn == 0) { 150 | return true; 151 | } else { 152 | return checkInBeforeDbCheckOut != checkOutBeforeDbCheckIn; 153 | } 154 | } catch (ParseException e) { 155 | throw new InvalidRequestException(ErrorMessages.PARSE_ERROR); 156 | } 157 | } else { 158 | return false; 159 | } 160 | 161 | }); 162 | } 163 | 164 | /** 165 | * Checks the existence of a user specified Reservation object in the database 166 | * 167 | * @param id 168 | * @return 169 | */ 170 | @Override 171 | public boolean validateReservationExistence(Integer id) { 172 | if(!reservationRepository.existsById(id)){ 173 | throw new InvalidRequestException(ErrorMessages.INVALID_ID_EXISTENCE); 174 | } else { 175 | return true; 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/dev/marioszocs/hotelreservationapi/serviceImp/HotelServiceImp.java: -------------------------------------------------------------------------------- 1 | package dev.marioszocs.hotelreservationapi.serviceImp; 2 | 3 | import dev.marioszocs.hotelreservationapi.constants.ErrorMessages; 4 | import dev.marioszocs.hotelreservationapi.dto.IdEntity; 5 | import dev.marioszocs.hotelreservationapi.dto.SuccessEntity; 6 | import dev.marioszocs.hotelreservationapi.entity.Hotel; 7 | import dev.marioszocs.hotelreservationapi.entity.Reservation; 8 | import dev.marioszocs.hotelreservationapi.exception.InvalidRequestException; 9 | import dev.marioszocs.hotelreservationapi.repository.HotelRepository; 10 | import dev.marioszocs.hotelreservationapi.repository.ReservationRepository; 11 | import dev.marioszocs.hotelreservationapi.service.HotelService; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.PageRequest; 16 | import org.springframework.data.domain.Pageable; 17 | import org.springframework.data.domain.Sort; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.util.StringUtils; 20 | 21 | import jakarta.transaction.Transactional; 22 | import jakarta.validation.Valid; 23 | import java.text.ParseException; 24 | import java.text.SimpleDateFormat; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | /** 29 | * Hotel Service that preforms operations regarding Hotel API Calls 30 | */ 31 | @Slf4j 32 | @Service 33 | @RequiredArgsConstructor 34 | @Transactional 35 | public class HotelServiceImp implements HotelService { 36 | 37 | private final HotelRepository hotelRepository; 38 | private final ReservationRepository reservationRepository; 39 | 40 | /** 41 | * Return all existing Hotel objects in the database 42 | * 43 | * @return List 44 | */ 45 | @Override 46 | public List getAllHotels() { 47 | return hotelRepository.findAll(); 48 | } 49 | 50 | /** 51 | * Return existing Hotel with pagination 52 | * 53 | * @param pageNo 54 | * @param pageSize 55 | * @param sortBy 56 | * @return 57 | */ 58 | @Override 59 | public List getHotelPagedList(Integer pageNo, Integer pageSize, String sortBy) { 60 | 61 | Pageable paging = PageRequest.of(pageNo, pageSize, Sort.Direction.ASC, sortBy); 62 | Page pagedResult = hotelRepository.findAll(paging); 63 | 64 | if (pagedResult.hasContent()) { 65 | return pagedResult.getContent(); 66 | } else { 67 | return new ArrayList<>(); 68 | } 69 | } 70 | 71 | /** 72 | * Returns a user specified Hotel item through the Hotel id 73 | * 74 | * @param id 75 | * @return Hotel 76 | */ 77 | @Override 78 | public Hotel getHotel(Integer id) { 79 | validateHotelExistenceById(id); 80 | return hotelRepository.findById(id).get(); 81 | 82 | // Ot without validateHotelExistenceById(id); 83 | // Optional hotel = hotelRepository.findById(id); 84 | // return hotel.isPresent() ? hotel.get() : null; 85 | } 86 | 87 | /** 88 | * Returns all Hotel objects in the database that are available in between user specified dates 89 | * 90 | * @param dateFrom 91 | * @param dateTo 92 | * @return 93 | */ 94 | @Override 95 | public List getAvailable(String dateFrom, String dateTo) { 96 | return hotelRepository.findAllBetweenDates(dateFrom, dateTo); 97 | } 98 | 99 | /** 100 | * Saves a user specified Hotel object to the database 101 | * 102 | * @param hotel 103 | * @return 104 | */ 105 | @Override 106 | public IdEntity saveHotel(@Valid Hotel hotel) { 107 | //If dates are empty strings make them null values so that they can be accepted by the database 108 | if ((!StringUtils.hasText(hotel.getAvailableFrom())) && (!(StringUtils.hasText(hotel.getAvailableTo())))) { 109 | hotel.setAvailableFrom(null); 110 | hotel.setAvailableTo(null); 111 | } 112 | hotel = hotelRepository.save(hotel); 113 | IdEntity idEntity = new IdEntity(); 114 | idEntity.setId(hotel.getId()); 115 | return idEntity; 116 | } 117 | 118 | /** 119 | * Deletes a user specified Hotel object from the database 120 | * 121 | * @param id 122 | * @return 123 | */ 124 | @Override 125 | public SuccessEntity deleteHotel(Integer id) { 126 | validateHotelExistenceById(id); 127 | if (reservationRepository.findAll().stream() 128 | .anyMatch(reservations -> reservations.getHotelId().equals(id))) { 129 | throw new InvalidRequestException(ErrorMessages.INVALID_HOTEL_DELETE); 130 | } 131 | SuccessEntity successEntity = new SuccessEntity(); 132 | hotelRepository.deleteById(id); 133 | successEntity.setSuccess(!hotelRepository.existsById(id)); 134 | return successEntity; 135 | } 136 | 137 | /** 138 | * Updates a pre-existing Hotel object in the database 139 | * 140 | * @param hotel 141 | * @return 142 | */ 143 | @Override 144 | public SuccessEntity patchHotel(Hotel hotel) { 145 | validateHotelExistenceById(hotel.getId()); 146 | doesReservationOverlap(hotel); 147 | SuccessEntity successEntity = new SuccessEntity(); 148 | hotel = hotelRepository.save(hotel); 149 | successEntity.setSuccess(hotelRepository.existsById(hotel.getId())); 150 | return successEntity; 151 | } 152 | 153 | /** 154 | * Checks to see if a reservation date overlaps with the inventory dates 155 | * 156 | * @param hotel 157 | */ 158 | @Override 159 | public void doesReservationOverlap(Hotel hotel) { 160 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); 161 | String availTo = hotel.getAvailableTo(); 162 | String availFrom = hotel.getAvailableFrom(); 163 | Integer hotelId = hotel.getId(); 164 | List matchingReservationList = reservationRepository.findAll().stream().filter(reservations -> { 165 | if (reservations.getHotelId() == hotelId) { 166 | try { 167 | //Checks to see if the user dates are null, if so throw an error as it conflicts with a reservation 168 | if (!StringUtils.hasText(availTo) && !StringUtils.hasText(availFrom)) { 169 | throw new InvalidRequestException(ErrorMessages.INVALID_DATE_CHANGE_NULL); 170 | } 171 | //should return 1 or 0 if there is no overlap, should return -1 if there is an overlap 172 | int checkInBeforeAvailFrom = sdf.parse(reservations.getCheckIn()).compareTo(sdf.parse(availFrom)); 173 | //should return -1 or 0 if there is no overlap, should return 1 if there is an overlap 174 | int checkOutBeforeAvailTo = sdf.parse(reservations.getCheckOut()).compareTo(sdf.parse(availTo)); 175 | if ((checkInBeforeAvailFrom < 0) || (checkOutBeforeAvailTo > 0)) { 176 | return true; 177 | } 178 | 179 | } catch (ParseException e) { 180 | throw new InvalidRequestException(ErrorMessages.PARSE_ERROR); 181 | } 182 | } 183 | return false; 184 | }).toList(); 185 | 186 | if (matchingReservationList.size() != 0) { 187 | throw new InvalidRequestException(ErrorMessages.INVALID_HOTEL_UPDATE); 188 | } 189 | } 190 | 191 | /** 192 | * Checks the existence of a Hotel object in the database 193 | * 194 | * @param id 195 | * @return 196 | */ 197 | @Override 198 | public boolean validateHotelExistenceById(Integer id) { 199 | if (!hotelRepository.existsById(id)) { 200 | log.error("Invalid ID: The entered id = {} does not exist.", id); 201 | throw new InvalidRequestException(ErrorMessages.INVALID_ID_EXISTENCE); 202 | } else { 203 | return true; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Hotel Reservation API 3 | 4 | This project provides a **RESTful API** for hotel reservations. It enables users to create hotels, make reservations, search for availability, and manage existing data. The project is implemented using **Spring Boot**, with a focus on clean architecture and proper validations. 5 | 6 | --- 7 | 8 | ## Features 9 | - **Hotel Management**: 10 | - Create, update, and delete hotels. 11 | - Search for hotels by availability or retrieve all existing hotels. 12 | - Each hotel has attributes such as name, type (e.g., DELUXE, LUXURY, SUITE), availability dates, and status. 13 | 14 | - **Reservation Management**: 15 | - Make reservations for a specific hotel, including check-in and check-out dates, number of guests, and status. 16 | - View or delete existing reservations. 17 | - Ensures logical validations, such as no overlapping reservations for the same hotel. 18 | 19 | - **Validations**: 20 | - Prevent overlapping reservations for the same hotel. 21 | - Ensure valid date ranges and proper input formats (e.g., `YYYY-MM-DD`). 22 | - Disallow deletion of hotels with active reservations. 23 | 24 | --- 25 | 26 | ## Explore REST APIs 27 | 28 | ### Hotel APIs 29 | 30 | ![image](https://github.com/user-attachments/assets/cb224507-8968-441a-a1d5-600c1271c401) 31 | 32 | 33 | 34 | | Method | Endpoint | Description | 35 | | ------ | -------- | ----------- | 36 | | POST | `/api/v1/hotel` | Create a new hotel. | 37 | | PATCH | `/api/v1/hotel` | Update an existing hotel. | 38 | | GET | `/api/v1/hotels` | Retrieve all hotels. | 39 | | GET | `/api/v1/hotels/availabilitySearch?dateFrom={from}&dateTo={to}` | Get available hotels for a date range. | 40 | | GET | `/api/v1/hotel/{id}` | Retrieve a specific hotel by ID. | 41 | | DELETE | `/api/v1/hotel/{id}` | Delete a hotel by ID. | 42 | 43 | #### Example Request: Create a New Hotel 44 | **Request Body:** 45 | ```json 46 | { 47 | "name": "Luxury Inn", 48 | "type": "LUXURY", 49 | "description": "A luxurious hotel in the city center.", 50 | "availableFrom": "2025-01-01", 51 | "availableTo": "2025-12-31", 52 | "status": true 53 | } 54 | ``` 55 | **Response:** 56 | ```json 57 | { 58 | "id": 1, 59 | "name": "Luxury Inn", 60 | "type": "LUXURY", 61 | "description": "A luxurious hotel in the city center.", 62 | "availableFrom": "2025-01-01", 63 | "availableTo": "2025-12-31", 64 | "status": true 65 | } 66 | ``` 67 | 68 | --- 69 | 70 | ### Reservation APIs 71 | 72 | ![image](https://github.com/user-attachments/assets/d51a7e5d-481c-46ed-8bda-a31314e212ff) 73 | 74 | 75 | 76 | | Method | Endpoint | Description | 77 | | ------ | -------- | ----------- | 78 | | POST | `/api/v1/reservation` | Create a reservation for a hotel. | 79 | | GET | `/api/v1/reservations` | Retrieve all reservations. | 80 | | GET | `/api/v1/reservation/{id}` | Retrieve a reservation by ID. | 81 | | DELETE | `/api/v1/reservation/{id}` | Delete a reservation by ID. | 82 | 83 | #### Example Request: Create a Reservation 84 | **Request Body:** 85 | ```json 86 | { 87 | "hotelId": 1, 88 | "checkIn": "2025-03-15", 89 | "checkOut": "2025-03-20", 90 | "guests": 2, 91 | "status": true 92 | } 93 | ``` 94 | **Response:** 95 | ```json 96 | { 97 | "id": 101, 98 | "hotelId": 1, 99 | "checkIn": "2025-03-15", 100 | "checkOut": "2025-03-20", 101 | "guests": 2, 102 | "status": true 103 | } 104 | ``` 105 | 106 | 107 | 108 | --- 109 | 110 | ## Database Structure 111 | The API uses a **MySQL database** with the following tables: 112 | 113 | ### `hotel` 114 | | Column | Type | Description | 115 | | --------------- | ---------- | ----------------------------------- | 116 | | `id` | Integer | Primary key. | 117 | | `name` | String | Hotel name. | 118 | | `type` | Enum | `DELUXE`, `LUXURY`, or `SUITE`. | 119 | | `description` | String | Description of the hotel. | 120 | | `availableFrom` | Date | Start date of availability. | 121 | | `availableTo` | Date | End date of availability. | 122 | | `status` | Boolean | Availability status. | 123 | 124 | ### `reservation` 125 | | Column | Type | Description | 126 | | ------------ | ------- | ------------------------------------ | 127 | | `id` | Integer | Primary key. | 128 | | `hotelId` | Integer | Foreign key referencing `hotel`. | 129 | | `checkIn` | Date | Check-in date. | 130 | | `checkOut` | Date | Check-out date. | 131 | | `guests` | Integer | Number of guests. | 132 | | `status` | Boolean | Reservation status. | 133 | 134 | ### Example SQL Script to Set Up the Schema 135 | ```sql 136 | CREATE TABLE hotel ( 137 | id INT AUTO_INCREMENT PRIMARY KEY, 138 | name VARCHAR(255) NOT NULL, 139 | type ENUM('DELUXE', 'LUXURY', 'SUITE') NOT NULL, 140 | description TEXT, 141 | availableFrom DATE NOT NULL, 142 | availableTo DATE NOT NULL, 143 | status BOOLEAN NOT NULL 144 | ); 145 | 146 | CREATE TABLE reservation ( 147 | id INT AUTO_INCREMENT PRIMARY KEY, 148 | hotelId INT NOT NULL, 149 | checkIn DATE NOT NULL, 150 | checkOut DATE NOT NULL, 151 | guests INT NOT NULL, 152 | status BOOLEAN NOT NULL, 153 | FOREIGN KEY (hotelId) REFERENCES hotel(id) 154 | ); 155 | 156 | INSERT INTO hotel (name, type, description, availableFrom, availableTo, status) VALUES 157 | ('Luxury Inn', 'LUXURY', 'A luxurious hotel in the city center.', '2025-01-01', '2025-12-31', true), 158 | ('City Center Suites', 'DELUXE', 'Conveniently located in the heart of the city.', '2025-01-01', '2025-12-31', true); 159 | ``` 160 | 161 | --- 162 | 163 | ## Tech Stack 164 | - **Backend**: Java 21, Spring Boot 3, Hibernate, JPA 165 | - **Database**: MySQL (runtime), H2 (testing) 166 | - **API Documentation**: Swagger UI 167 | - **Testing**: JUnit 5, Mockito 168 | - **Utilities**: Lombok, Maven 169 | 170 | --- 171 | 172 | ## Project Structure 173 | ``` 174 | marioszocs-spring-boot-hotel-reservation-api/ 175 | ├── README.md 176 | ├── mvnw 177 | ├── mvnw.cmd 178 | ├── pom.xml 179 | ├── src/ 180 | │ ├── main/ 181 | │ │ ├── java/ 182 | │ │ │ └── dev/marioszocs/hotelreservationapi/ 183 | │ │ │ ├── HotelReservationApiApplication.java 184 | │ │ │ ├── ServletInitializer.java 185 | │ │ │ ├── bootstrap/ 186 | │ │ │ │ └── HotelsAndReservationsLoader.java 187 | │ │ │ ├── config/ 188 | │ │ │ │ └── OpenApiConfig.java 189 | │ │ │ ├── constants/ 190 | │ │ │ │ ├── AppConstants.java 191 | │ │ │ │ └── ErrorMessages.java 192 | │ │ │ ├── controller/ 193 | │ │ │ │ ├── HotelController.java 194 | │ │ │ │ └── ReservationController.java 195 | │ │ │ ├── entity/ 196 | │ │ │ │ ├── Hotel.java 197 | │ │ │ │ ├── Reservation.java 198 | │ │ │ │ └── ValidTypesOfHotelsEnum.java 199 | │ │ │ ├── repository/ 200 | │ │ │ │ ├── HotelRepository.java 201 | │ │ │ │ └── ReservationRepository.java 202 | │ │ │ ├── service/ 203 | │ │ │ │ ├── HotelService.java 204 | │ │ │ │ └── ReservationService.java 205 | │ │ │ ├── serviceImp/ 206 | │ │ │ │ ├── HotelServiceImp.java 207 | │ │ │ │ └── ReservationServiceImp.java 208 | │ │ │ ├── utils/ 209 | │ │ │ │ └── AppUtils.java 210 | │ │ │ └── validator/ 211 | │ │ │ ├── HotelValidator.java 212 | │ │ │ └── ReservationValidator.java 213 | │ │ └── resources/ 214 | │ │ ├── application-h2.properties 215 | │ │ ├── application-mysql.properties 216 | │ │ └── application.properties 217 | ├── .mvn/ 218 | │ └── wrapper/ 219 | │ └── maven-wrapper.properties 220 | ``` 221 | 222 | --- 223 | 224 | ## Testing 225 | - Includes **unit tests** (with Mockito) and **integration tests** (using H2 in-memory database). 226 | - Coverage: 227 | - **93%** of classes. 228 | - **94%** of methods. 229 | - **85%** of lines. 230 | - Test scenarios: 231 | - API endpoint responses. 232 | - Validation of inputs and error handling. 233 | - Business logic for reservations and hotel availability. 234 | 235 | --- 236 | 237 | ## Installation 238 | 239 | ### Prerequisites 240 | - Java 21 241 | - Maven 242 | - MySQL (if not using the H2 database) 243 | 244 | ### Steps 245 | 1. Clone the repository: 246 | ```bash 247 | git clone https://github.com/your-username/hotel-reservation-api.git 248 | cd hotel-reservation-api 249 | ``` 250 | 2. Configure the database in `src/main/resources/application.properties`: 251 | ```properties 252 | spring.datasource.url=jdbc:mysql://localhost:3306/hotel_reservation_db 253 | spring.datasource.username=your-username 254 | spring.datasource.password=your-password 255 | ``` 256 | 3. Build the project: 257 | ```bash 258 | mvn clean install 259 | ``` 260 | 4. Run the application: 261 | ```bash 262 | mvn spring-boot:run 263 | ``` 264 | 265 | --- 266 | 267 | ## Contribution 268 | Contributions are welcome! Feel free to fork the repository and submit a pull request. 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | --------------------------------------------------------------------------------