Skip to content

4 Stay Management Implementation

4 Stay Management Implementation

Goal

  • Implement stay management service.

Screenshot 2024-07-09 at 19.37.22

Model Creation

Let’s recap the data model of the backend service first. For stay management, we need a stay table, a stay reserved date table, and a stay image table.

  1. Open your staybooking project in Intellij and go to the com.eve.staybooking.model package. Create a new class named Stay.
package com.eve.staybooking.model;

public class Stay {
}
  1. Add some private fields to the Stay.class. As you can see in the IDE, the StayReservedDate class is not available.
package com.eve.staybooking.model;

public class Stay {
    private Long id;
    private String name;
    private String description;
    private String address;
    private int guestNumber;
    private User host;
    private List<StayReservedDate> reservedDates;
}

Screenshot 2024-07-10 at 18.33.54

  1. Under the same com.eve.staybooking.model package, create the StayReservedDate.class.
package com.eve.staybooking.model;

public class StayReservedDate {
}
  1. For the three columns in the StayReservedDate table, we choose to create the composite primary key based on stay_id and date. So create another class called StayReservedDateKey under the com.eve.staybooking.model package.
package com.eve.staybooking.model;

public class StayReservedDateKey {
}
  1. Add both stay_id and date as the private field of StayReservedDateKey.class.
package com.eve.staybooking.model;

import java.time.LocalDate;

public class StayReservedDateKey {
    private Long stay_id;
    private LocalDate date;
}
  1. Add the public getters, setters, and constructors to StayReservedDateKey class.
package com.eve.staybooking.model;

import java.time.LocalDate;

public class StayReservedDateKey {
    private Long stay_id;
    private LocalDate date;

    public StayReservedDateKey() {}

    public StayReservedDateKey(Long stay_id, LocalDate date) {
        this.stay_id = stay_id;
        this.date = date;
    }

    public Long getStay_id() {
        return stay_id;
    }

    public StayReservedDateKey setStay_id(Long stay_id) {
        this.stay_id = stay_id;
        return this;
    }

    public LocalDate getDate() {
        return date;
    }

    public StayReservedDateKey setDate(LocalDate date) {
        this.date = date;
        return this;
    }
}
  1. Since we’re going to use StayReservedDateKey as the primary key of the StayReservedDate table, we need to mark it as @Embeddable, add a serialVersionUID and implement both hashCode() and equals().
package com.eve.staybooking.model;

import jakarta.persistence.Embeddable;

import java.io.Serializable;
import java.time.LocalDate;
import java.util.Objects;

@Embeddable
public class StayReservedDateKey implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long stay_id;
    private LocalDate date;

    public StayReservedDateKey() {}

    public StayReservedDateKey(Long stay_id, LocalDate date) {
        this.stay_id = stay_id;
        this.date = date;
    }

    public Long getStay_id() {
        return stay_id;
    }

    public StayReservedDateKey setStay_id(Long stay_id) {
        this.stay_id = stay_id;
        return this;
    }

    public LocalDate getDate() {
        return date;
    }

    public StayReservedDateKey setDate(LocalDate date) {
        this.date = date;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StayReservedDateKey that = (StayReservedDateKey) o;
        return stay_id.equals(that.stay_id) && date.equals(that.date);
    }

    @Override
    public int hashCode() {
        return Objects.hash(stay_id, date);
    }
}
  1. Go back to the StayReservedDate.class, add the following code.
package com.eve.staybooking.model;

public class StayReservedDate {
    private StayReservedDateKey id;
    private Stay stay;

    public StayReservedDate() {}

    public StayReservedDate(StayReservedDateKey id, Stay stay) {
        this.id = id;
        this.stay = stay;
    }

    public StayReservedDateKey getId() {
        return id;
    }

    public Stay getStay() {
        return stay;
    }
}

One point for the details

  • Public setters are not added since won’t need them in the code. All the stay reserved date information is read from the database, so we don’t need to update them in the Java code.
  1. Next, annotate the class and private field to make it supported by Hibernate.
package com.eve.staybooking.model;

import jakarta.persistence.*;

import java.io.Serializable;

@Entity
@Table(name = "stay_reserved_date")
public class StayReservedDate implements Serializable {
    private static final long serialVersionUID = 1L;

    @EmbeddedId
    private StayReservedDateKey id;

    @MapsId("stay_id")
    @ManyToOne
    private Stay stay;

    public StayReservedDate() {}

    public StayReservedDate(StayReservedDateKey id, Stay stay) {
        this.id = id;
        this.stay = stay;
    }

    public StayReservedDateKey getId() {
        return id;
    }

    public Stay getStay() {
        return stay;
    }
}
  1. Finally, go back to the Stay class and add necessary getters, setters, and constructors.

    package com.eve.staybooking.model;
    
    import com.fasterxml.jackson.annotation.JsonProperty;
    
    import java.util.List;
    
    public class Stay {
        private Long id;
        private String name;
        private String description;
        private String address;
        private int guestNumber;
        private User host;
        private List<StayReservedDate> reservedDates;
    
        public Stay() {}
    
        private Stay(Builder builder) {
            this.id = builder.id;
            this.name = builder.name;
            this.description = builder.description;
            this.address = builder.address;
            this.guestNumber = builder.guestNumber;
            this.host = builder.host;
            this.reservedDates = builder.reservedDates;
        }
    
        public Long getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public String getDescription() {
            return description;
        }
    
        public String getAddress() {
            return address;
        }
    
        public int getGuestNumber() {
            return guestNumber;
        }
    
        public User getHost() {
            return host;
        }
    
        public List<StayReservedDate> getReservedDates() {
            return reservedDates;
        }
    
        public static class Builder {
    
            @JsonProperty("id")
            private Long id;
    
            @JsonProperty("name")
            private String name;
    
            @JsonProperty("description")
            private String description;
    
            @JsonProperty("address")
            private String address;
    
            @JsonProperty("guest_number")
            private int guestNumber;
    
            @JsonProperty("host")
            private User host;
    
            @JsonProperty("dates")
            private List<StayReservedDate> reservedDates;
    
            public Builder setId(Long id) {
                this.id = id;
                return this;
            }
    
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
    
            public Builder setDescription(String description) {
                this.description = description;
                return this;
            }
    
            public Builder setAddress(String address) {
                this.address = address;
                return this;
            }
    
            public Builder setGuestNumber(int guestNumber) {
                this.guestNumber = guestNumber;
                return this;
            }
    
            public Builder setHost(User host) {
                this.host = host;
                return this;
            }
    
            public Builder setReservedDates(List<StayReservedDate> reservedDates) {
                this.reservedDates = reservedDates;
                return this;
            }
    
            public Stay build() {
                return new Stay(this);
            }
        }
    
    }
    
  2. Add a few Jackson related annotations.

    package com.eve.staybooking.model;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
    
    import java.util.List;
    
    @JsonDeserialize(builder = Stay.Builder.class)
    public class Stay {
        private Long id;
        private String name;
        private String description;
        private String address;
    
        @JsonProperty("guest_number")
        private int guestNumber;
        private User host;
    
        @JsonIgnore
        private List<StayReservedDate> reservedDates;
    
        public Stay() {}
    
        private Stay(Builder builder) {
            this.id = builder.id;
            this.name = builder.name;
            this.description = builder.description;
            this.address = builder.address;
            this.guestNumber = builder.guestNumber;
            this.host = builder.host;
            this.reservedDates = builder.reservedDates;
        }
    
        public Long getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public String getDescription() {
            return description;
        }
    
        public String getAddress() {
            return address;
        }
    
        public int getGuestNumber() {
            return guestNumber;
        }
    
        public User getHost() {
            return host;
        }
    
        public List<StayReservedDate> getReservedDates() {
            return reservedDates;
        }
    
        public static class Builder {
    
            @JsonProperty("id")
            private Long id;
    
            @JsonProperty("name")
            private String name;
    
            @JsonProperty("description")
            private String description;
    
            @JsonProperty("address")
            private String address;
    
            @JsonProperty("guest_number")
            private int guestNumber;
    
            @JsonProperty("host")
            private User host;
    
            @JsonProperty("dates")
            private List<StayReservedDate> reservedDates;
    
            public Builder setId(Long id) {
                this.id = id;
                return this;
            }
    
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
    
            public Builder setDescription(String description) {
                this.description = description;
                return this;
            }
    
            public Builder setAddress(String address) {
                this.address = address;
                return this;
            }
    
            public Builder setGuestNumber(int guestNumber) {
                this.guestNumber = guestNumber;
                return this;
            }
    
            public Builder setHost(User host) {
                this.host = host;
                return this;
            }
    
            public Builder setReservedDates(List<StayReservedDate> reservedDates) {
                this.reservedDates = reservedDates;
                return this;
            }
    
            public Stay build() {
                return new Stay(this);
            }
        }
    
    }
    

    A few explanations for the annotation:

    • @JsonDeserialize makes sure the Jackson library will use the Builder class to convert JSON format data to the Stay object.
    • @JsonProperty makes sure to map guestNumber field to the guest_number key in JSON format data.
    • @JsonIgnore makes sure we don’t return reserved date information when returning the stay information in JSON format because, in our design, the front end doesn’t need to show the details about a stay’s reserved dates.
  3. At last, annotate the Stay class with Hibernate-related annotations.

    package com.eve.staybooking.model;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
    import jakarta.persistence.*;
    
    import java.io.Serializable;
    import java.util.List;
    @Entity
    @Table(name = "stay")
    @JsonDeserialize(builder = Stay.Builder.class)
    public class Stay implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private String name;
        private String description;
        private String address;
    
        @JsonProperty("guest_number")
        private int guestNumber;
    
        @ManyToOne
        @JoinColumn(name = "user_id")
        private User host;
    
        @JsonIgnore
        @OneToMany(mappedBy = "stay", cascade = CascadeType.ALL, fetch=FetchType.LAZY)
        private List<StayReservedDate> reservedDates;
    
        public Stay() {}
    
        private Stay(Builder builder) {
            this.id = builder.id;
            this.name = builder.name;
            this.description = builder.description;
            this.address = builder.address;
            this.guestNumber = builder.guestNumber;
            this.host = builder.host;
            this.reservedDates = builder.reservedDates;
        }
    
        public Long getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public String getDescription() {
            return description;
        }
    
        public String getAddress() {
            return address;
        }
    
        public int getGuestNumber() {
            return guestNumber;
        }
    
        public User getHost() {
            return host;
        }
    
        public List<StayReservedDate> getReservedDates() {
            return reservedDates;
        }
    
        public static class Builder {
    
            @JsonProperty("id")
            private Long id;
    
            @JsonProperty("name")
            private String name;
    
            @JsonProperty("description")
            private String description;
    
            @JsonProperty("address")
            private String address;
    
            @JsonProperty("guest_number")
            private int guestNumber;
    
            @JsonProperty("host")
            private User host;
    
            @JsonProperty("dates")
            private List<StayReservedDate> reservedDates;
    
            public Builder setId(Long id) {
                this.id = id;
                return this;
            }
    
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
    
            public Builder setDescription(String description) {
                this.description = description;
                return this;
            }
    
            public Builder setAddress(String address) {
                this.address = address;
                return this;
            }
    
            public Builder setGuestNumber(int guestNumber) {
                this.guestNumber = guestNumber;
                return this;
            }
    
            public Builder setHost(User host) {
                this.host = host;
                return this;
            }
    
            public Builder setReservedDates(List<StayReservedDate> reservedDates) {
                this.reservedDates = reservedDates;
                return this;
            }
    
            public Stay build() {
                return new Stay(this);
            }
        }
    
    }
    

Define Stay Repository

  1. Go to the com.eve.staybooking.repository package and create a new class StayRepository.
package com.eve.staybooking.repository;

import com.eve.staybooking.model.Stay;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StayRepository extends JpaRepository<Stay, Long> {

}
  1. Let’s check the backend diagram for stay management services. As we mentioned before, common methods like save, deleteById, findById are defined in the JpaRepositry. So we only need to define our method findByHost and findByIdAndHost.
package com.eve.staybooking.repository;

import com.eve.staybooking.model.Stay;
import com.eve.staybooking.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface StayRepository extends JpaRepository<Stay, Long> {
    List<Stay> findByHost(User user);

    Stay findByIdAndHost(Long id, User host);

}

Stay Service Implementation

  1. Go to the com.eve.staybooking.exception package and create a new exception class StayNotExistException.
package com.eve.staybooking.exception;

public class StayNotExistException extends RuntimeException {
    public StayNotExistException(String message) {
        super(message);
    }
}
  1. Go to the com.eve.staybooking.service package and create a new class StayService.
package com.eve.staybooking.service;

import org.springframework.stereotype.Service;

@Service
public class StayService {

}
  1. Add the StayRepository as a private field and create a constructor for initialization.
package com.eve.staybooking.service;

import com.eve.staybooking.repository.StayRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class StayService {
    private StayRepository stayRepository;

    @Autowired
    public StayService(StayRepository stayRepository) {
        this.stayRepository = stayRepository;
    }


}
  1. Implement the methods for stay save, delete by id, list by the user and get by id.
package com.eve.staybooking.service;

import com.eve.staybooking.exception.StayNotExistException;
import com.eve.staybooking.model.Stay;
import com.eve.staybooking.model.User;
import com.eve.staybooking.repository.StayRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class StayService {
    private StayRepository stayRepository;

    @Autowired
    public StayService(StayRepository stayRepository) {
        this.stayRepository = stayRepository;
    }
    public List<Stay> listByUser(String username) {
        return stayRepository.findByHost(new User.Builder().setUsername(username).build());
    }

    public Stay findByIdAndHost(Long stayId, String username) throws StayNotExistException {
        Stay stay = stayRepository.findByIdAndHost(stayId, new User.Builder().setUsername(username).build());
        if (stay == null) {
            throw new StayNotExistException("Stay doesn't exist");
        }
        return stay;
    }

    public void add(Stay stay) {
        stayRepository.save(stay);
    }

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void delete(Long stayId, String username) throws StayNotExistException {
        Stay stay = stayRepository.findByIdAndHost(stayId, new User.Builder().setUsername(username).build());
        if (stay == null) {
            throw new StayNotExistException("Stay doesn't exist");
        }
        stayRepository.deleteById(stayId);
    }


}

Stay Controller Implementation

  1. Go to the com.eve.service.controller package and create a new class StayController.
package com.eve.staybooking.controller;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class StayController {
}
  1. Add the StayService as a private field and create the constructor.
package com.eve.staybooking.controller;

import com.eve.staybooking.service.StayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StayController {
    private StayService stayService;

    @Autowired
    public StayController(StayService stayService) {
        this.stayService = stayService;
    }
}
  1. Implement the methods for all stay management APIs.
package com.eve.staybooking.controller;

import com.eve.staybooking.model.Stay;
import com.eve.staybooking.service.StayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class StayController {
    private StayService stayService;

    @Autowired
    public StayController(StayService stayService) {
        this.stayService = stayService;
    }

    @GetMapping(value = "/stays")
    public List<Stay> listStays(@RequestParam(name = "host") String hostName) {
        return stayService.listByUser(hostName);
    }

    @GetMapping(value = "/stays/id")
    public Stay getStay(
            @RequestParam(name = "stay_id") Long stayId,
            @RequestParam(name = "host") String hostName) {
        return stayService.findByIdAndHost(stayId, hostName);
    }

    @PostMapping("/stays")
    public void addStay(@RequestBody Stay stay) {
        stayService.add(stay);
    }

    @DeleteMapping("/stays")
    public void deleteStay(
            @RequestParam(name = "stay_id") Long stayId,
            @RequestParam(name = "host") String hostName) {
        stayService.delete(stayId, hostName);
    }

}
  1. Go to the CustomExceptionHandler.class and add the function to handle the StayNotExistException.
package com.eve.staybooking.controller;

import com.eve.staybooking.exception.StayNotExistException;
import com.eve.staybooking.exception.UserAlreadyExistException;
import com.eve.staybooking.exception.UserNotExistException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(UserAlreadyExistException.class)
    public final ResponseEntity<String> handleUserAlreadyExistExceptions(Exception ex, WebRequest request){
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.CONFLICT);
    }

    @ExceptionHandler(UserNotExistException.class)
    public final ResponseEntity<String> handleUserNotExistExceptions(Exception ex, WebRequest request){
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.UNAUTHORIZED);
    }

    @ExceptionHandler(StayNotExistException.class)
    public final ResponseEntity<String> handleStayNotExistExceptions(Exception ex, WebRequest request) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

}
  1. Go to the SecurityConfig.class to allow stay management APIs without authentication. We should require users to log in before using stay management APIs, but let’s postpone the implementation to next.
     public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/register/*").permitAll()
                .antMatchers(HttpMethod.POST, "/authenticate/*").permitAll()
                .antMatchers("/stays").permitAll()
                .antMatchers("/stays/*").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf()
                .disable();
    }

    ...
}

Test Your Service

  1. Save all your changes and start your project. Make sure there’s no error in the log.

Screenshot 2024-07-19 at 15.17.45

  1. Use the Stay Upload sample request in the Postman collection to save a new stay to the database. Make sure there’s no error in the response.

Screenshot 2024-07-19 at 16.01.24

  1. Use the List Stays by Host request to verify the stay information can be successfully returned from the backend.

Screenshot 2024-07-19 at 16.27.20

  1. Use the Get Stay by ID request to verify that you can see the stay you’ve just uploaded. Remember to replace the stay_id argument with the real ID returned in the last step.

Screenshot 2024-07-19 at 16.26.33

  1. Finally, use the Delete Stay by ID request to make sure you can delete the stay you’ve just uploaded in stay 2. Again, remember to

Screenshot 2024-07-19 at 16.27.56

  1. replace the stay_id with the real Id returned in step 3.

Screenshot 2024-07-19 at 16.30.48

  1. (Optional) Redo the test process with different arguments to make sure there are no bugs in the backend.