From ae9f784cce68269b78430ea25429b4e7e5028e0b Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 09:13:57 +0200 Subject: [PATCH 01/23] Created skeleton of MVC files and main. Updated build.gradle to include Lombok dependencies. Updated .gitignore with full path to application.yml. --- .gitignore | 2 ++ build.gradle | 2 ++ .../java/com/booleanuk/api/cinema/.gitkeep | 0 .../java/com/booleanuk/api/cinema/Main.java | 11 +++++++++ .../cinema/controller/CustomerController.java | 4 ++++ .../cinema/controller/MovieController.java | 4 ++++ .../controller/ScreeningRepository.java | 4 ++++ .../cinema/controller/TicketController.java | 4 ++++ .../booleanuk/api/cinema/model/Customer.java | 20 ++++++++++++++++ .../com/booleanuk/api/cinema/model/Movie.java | 4 ++++ .../booleanuk/api/cinema/model/Screening.java | 4 ++++ .../booleanuk/api/cinema/model/Ticket.java | 4 ++++ .../cinema/repository/CustomerRepository.java | 7 ++++++ .../cinema/repository/MovieRepository.java | 7 ++++++ .../repository/ScreeningRepository.java | 7 ++++++ .../cinema/repository/TicketRepository.java | 7 ++++++ src/main/resources/application.yml.example | 23 ------------------- 17 files changed, 91 insertions(+), 23 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/.gitkeep create mode 100644 src/main/java/com/booleanuk/api/cinema/Main.java create mode 100644 src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/controller/MovieController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/controller/TicketController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/model/Customer.java create mode 100644 src/main/java/com/booleanuk/api/cinema/model/Movie.java create mode 100644 src/main/java/com/booleanuk/api/cinema/model/Screening.java create mode 100644 src/main/java/com/booleanuk/api/cinema/model/Ticket.java create mode 100644 src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java create mode 100644 src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java delete mode 100644 src/main/resources/application.yml.example diff --git a/.gitignore b/.gitignore index dc01f204..b86b5dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ out/ ### VS Code ### .vscode/ + +src/main/resources/application.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3d7f7607..3d03b1c7 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,8 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/com/booleanuk/api/cinema/.gitkeep b/src/main/java/com/booleanuk/api/cinema/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/booleanuk/api/cinema/Main.java b/src/main/java/com/booleanuk/api/cinema/Main.java new file mode 100644 index 00000000..6151ef1f --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/Main.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.cinema; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + public static void main(String[] args){ + SpringApplication.run(Main.class, args); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java new file mode 100644 index 00000000..9543fffb --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.controller; + +public class CustomerController { +} diff --git a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java new file mode 100644 index 00000000..e1056afd --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.controller; + +public class MovieController { +} diff --git a/src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java new file mode 100644 index 00000000..3d0812a5 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.controller; + +public class ScreeningRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/controller/TicketController.java b/src/main/java/com/booleanuk/api/cinema/controller/TicketController.java new file mode 100644 index 00000000..e753f786 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controller/TicketController.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.controller; + +public class TicketController { +} diff --git a/src/main/java/com/booleanuk/api/cinema/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/model/Customer.java new file mode 100644 index 00000000..1d949d8d --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/model/Customer.java @@ -0,0 +1,20 @@ +package com.booleanuk.api.cinema.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor + +@Entity +@Table(name = "customers") +public class Customer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + +} diff --git a/src/main/java/com/booleanuk/api/cinema/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/model/Movie.java new file mode 100644 index 00000000..4d8a152b --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/model/Movie.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.model; + +public class Movie { +} diff --git a/src/main/java/com/booleanuk/api/cinema/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/model/Screening.java new file mode 100644 index 00000000..5a6b2dde --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/model/Screening.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.model; + +public class Screening { +} diff --git a/src/main/java/com/booleanuk/api/cinema/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/model/Ticket.java new file mode 100644 index 00000000..8118ea54 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/model/Ticket.java @@ -0,0 +1,4 @@ +package com.booleanuk.api.cinema.model; + +public class Ticket { +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java new file mode 100644 index 00000000..5ddf67bc --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.model.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomerRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java new file mode 100644 index 00000000..c689e162 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.model.Movie; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java new file mode 100644 index 00000000..1235edbd --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.model.Screening; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ScreeningRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java b/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java new file mode 100644 index 00000000..ee891c02 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.cinema.repository; + +import com.booleanuk.api.cinema.model.Ticket; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TicketRepository extends JpaRepository { +} diff --git a/src/main/resources/application.yml.example b/src/main/resources/application.yml.example deleted file mode 100644 index 275ec30f..00000000 --- a/src/main/resources/application.yml.example +++ /dev/null @@ -1,23 +0,0 @@ -server: - port: 4000 - error: - include-message: always - include-binding-errors: always - include-stacktrace: never - include-exception: false - -spring: - datasource: - url: jdbc:postgresql://DATABASE_URL:5432/DATABASE_NAME - username: DATABASE_USERNAME - password: DATABASE_PASSWORD - max-active: 3 - max-idle: 3 - jpa: - hibernate: - ddl-auto: update - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: true - show-sql: true From 5d96c1808298a2c7d57d1df00c7642c032dc5901 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 09:27:00 +0200 Subject: [PATCH 02/23] Implemented basic models based on diagram. --- .../booleanuk/api/cinema/model/Customer.java | 17 +++++++++ .../com/booleanuk/api/cinema/model/Movie.java | 36 +++++++++++++++++++ .../booleanuk/api/cinema/model/Screening.java | 36 +++++++++++++++++++ .../booleanuk/api/cinema/model/Ticket.java | 33 +++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/src/main/java/com/booleanuk/api/cinema/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/model/Customer.java index 1d949d8d..057897e0 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Customer.java @@ -1,13 +1,17 @@ package com.booleanuk.api.cinema.model; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.time.LocalDateTime; + @Getter @Setter @NoArgsConstructor +@AllArgsConstructor @Entity @Table(name = "customers") @@ -16,5 +20,18 @@ public class Customer { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + @Column(name = "name") + private String name; + + @Column(name = "email") + private String email; + + @Column(name = "phone") + private String phone; + + @Column(name = "createdAt") + private LocalDateTime createdAt; + @Column(name = "updatedAt") + private LocalDateTime updatedAt; } diff --git a/src/main/java/com/booleanuk/api/cinema/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/model/Movie.java index 4d8a152b..9bd41049 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Movie.java @@ -1,4 +1,40 @@ package com.booleanuk.api.cinema.model; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor + +@Entity +@Table(name = "movies") public class Movie { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(name = "title") + private String title; + + @Column(name = "rating") + private String rating; + + @Column(name = "description") + private String description; + + @Column(name = "runtimeMins") + private String runtimeMins; + + @Column(name = "createdAt") + private LocalDateTime createdAt; + + @Column(name = "updatedAt") + private LocalDateTime updatedAt; } diff --git a/src/main/java/com/booleanuk/api/cinema/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/model/Screening.java index 5a6b2dde..26da2222 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Screening.java @@ -1,4 +1,40 @@ package com.booleanuk.api.cinema.model; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor + +@Entity +@Table(name = "Screening") public class Screening { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(name = "movieId") + int movieId; + + @Column(name = "screenNumber") + int screenNumber; + + @Column(name = "startsAt") + LocalDateTime startsAt; + + @Column(name = "capacity") + int capacity; + + @Column(name = "createdAt") + LocalDateTime createdAt; + + @Column(name = "updatedAt") + LocalDateTime updatedAt; } diff --git a/src/main/java/com/booleanuk/api/cinema/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/model/Ticket.java index 8118ea54..f443e7eb 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Ticket.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Ticket.java @@ -1,4 +1,37 @@ package com.booleanuk.api.cinema.model; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor + +@Entity +@Table(name = "tickets") public class Ticket { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(name = "customerId") + private int customerId; + + @Column(name = "screeningId") + private int screeningId; + + @Column(name = "numberOfSeats") + private int numberOfSeats; + + @Column(name = "createdAt") + private LocalDateTime createdAt; + + @Column(name = "updatedAt") + private LocalDateTime updatedAt; } From 17edd6b018cbdf651f9effdbf2bec460bca6c4c4 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 10:01:17 +0200 Subject: [PATCH 03/23] Implemented basic customer controller with CRUD functionality. --- .../cinema/controller/CustomerController.java | 70 +++++++++++++++++++ .../booleanuk/api/cinema/model/Customer.java | 19 ++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java index 9543fffb..e19a6de3 100644 --- a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java @@ -1,4 +1,74 @@ package com.booleanuk.api.cinema.controller; +import com.booleanuk.api.cinema.model.Customer; +import com.booleanuk.api.cinema.repository.CustomerRepository; +import org.apache.coyote.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; + + +@RestController +@RequestMapping("/customers") public class CustomerController { + + @Autowired + CustomerRepository customerRepository; + + @PostMapping + public ResponseEntity addCustomer(@RequestBody Customer customer) throws ResponseStatusException { + try { + return new ResponseEntity<>(this.customerRepository.save(customer), HttpStatus.CREATED); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when creating a customer: " + e.getMessage()); + } + } + + @GetMapping + public ResponseEntity> getAllCustomers() { + return new ResponseEntity<>(this.customerRepository.findAll(), HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) throws ResponseStatusException { + return ResponseEntity.ok(findCustomerById(id)); + } + + @PutMapping("/{id}") + public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @RequestBody Customer customer) throws ResponseStatusException { + Customer customerToUpdate = findCustomerById(id); + + try { + customerToUpdate.setName(customer.getName()); + customerToUpdate.setPhone(customer.getPhone()); + customerToUpdate.setEmail(customer.getEmail()); + customerToUpdate.setUpdatedAt(LocalDateTime.now()); + + return new ResponseEntity<>(this.customerRepository.save(customerToUpdate), HttpStatus.CREATED); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update customer: " + e.getMessage()); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ + Customer customerToDelete = findCustomerById(id); + try { + this.customerRepository.delete(customerToDelete); + return ResponseEntity.ok(customerToDelete); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to delete customer: " + e.getMessage()); + } + } + + + private Customer findCustomerById(int id){ + return this.customerRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Customer with provided ID was not found")); + } + } diff --git a/src/main/java/com/booleanuk/api/cinema/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/model/Customer.java index 057897e0..16b6828c 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Customer.java @@ -1,7 +1,6 @@ package com.booleanuk.api.cinema.model; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,11 +10,17 @@ @Getter @Setter @NoArgsConstructor -@AllArgsConstructor @Entity @Table(name = "customers") public class Customer { + + public Customer (String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @@ -34,4 +39,14 @@ public class Customer { @Column(name = "updatedAt") private LocalDateTime updatedAt; + + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } } From 092733a90c18c7ab43fa42a0153ecb765e74b819 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 10:17:00 +0200 Subject: [PATCH 04/23] Implemented basic movie controller with CRUD functionality. Set all fields in customer and movie to not nullable. --- .../cinema/controller/MovieController.java | 59 +++++++++++++++++++ .../booleanuk/api/cinema/model/Customer.java | 10 ++-- .../com/booleanuk/api/cinema/model/Movie.java | 21 +++++-- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java index e1056afd..07ab47ca 100644 --- a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java @@ -1,4 +1,63 @@ package com.booleanuk.api.cinema.controller; +import com.booleanuk.api.cinema.model.Movie; +import com.booleanuk.api.cinema.repository.MovieRepository; +import org.apache.coyote.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping public class MovieController { + MovieRepository movieRepository; + + @PostMapping + public ResponseEntity addMovie(@RequestBody Movie movie) throws ResponseStatusException { + try { + return new ResponseEntity<>(this.movieRepository.save(movie), HttpStatus.CREATED); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to create a movie: " + e.getMessage()); + } + } + + @GetMapping + public ResponseEntity> getAllMovies() throws ResponseStatusException { + return ResponseEntity.ok(this.movieRepository.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getMovieById(@PathVariable (name = "id") int id) throws ResponseStatusException { + return ResponseEntity.ok(findMovieById(id)); + } + + @PutMapping("/{id}") + public ResponseEntity updateMovie(@PathVariable (name = "id") int id, @RequestBody Movie movie) throws ResponseStatusException { + Movie movieToUpdate = findMovieById(id); + try { + movieToUpdate.setTitle(movie.getTitle()); + movieToUpdate.setRating(movie.getRating()); + movieToUpdate.setDescription(movie.getDescription()); + movieToUpdate.setRuntimeMins(movie.getRuntimeMins()); + movieToUpdate.setUpdatedAt(LocalDateTime.now()); + return new ResponseEntity<>(this.movieRepository.save(movieToUpdate), HttpStatus.CREATED); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update movie: " + e.getMessage()); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) throws ResponseStatusException { + Movie movieToDelete = findMovieById(id); + this.movieRepository.delete(movieToDelete); + return ResponseEntity.ok(movieToDelete); + } + + private Movie findMovieById(int id) throws ResponseStatusException { + return this.movieRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Movie with the provided ID does not exist.")); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/model/Customer.java index 16b6828c..f213c88e 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Customer.java @@ -25,19 +25,19 @@ public Customer (String name, String email, String phone) { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - @Column(name = "name") + @Column(name = "name", nullable = false) private String name; - @Column(name = "email") + @Column(name = "email", nullable = false) private String email; - @Column(name = "phone") + @Column(name = "phone", nullable = false) private String phone; - @Column(name = "createdAt") + @Column(name = "createdAt", nullable = false) private LocalDateTime createdAt; - @Column(name = "updatedAt") + @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; diff --git a/src/main/java/com/booleanuk/api/cinema/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/model/Movie.java index 9bd41049..28c41612 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Movie.java @@ -20,21 +20,30 @@ public class Movie { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - @Column(name = "title") + @Column(name = "title", nullable = false) private String title; - @Column(name = "rating") + @Column(name = "rating", nullable = false) private String rating; - @Column(name = "description") + @Column(name = "description", nullable = false) private String description; - @Column(name = "runtimeMins") + @Column(name = "runtimeMins", nullable = false) private String runtimeMins; - @Column(name = "createdAt") + @Column(name = "createdAt", nullable = false) private LocalDateTime createdAt; - @Column(name = "updatedAt") + @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } } From 91d3864fee1f4c334762bf5e4d4ee0167d7bfdb3 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 10:18:16 +0200 Subject: [PATCH 05/23] Added actual requestmapping. --- .../com/booleanuk/api/cinema/controller/MovieController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java index 07ab47ca..6f1e7175 100644 --- a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java @@ -2,7 +2,6 @@ import com.booleanuk.api.cinema.model.Movie; import com.booleanuk.api.cinema.repository.MovieRepository; -import org.apache.coyote.Response; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -12,7 +11,7 @@ import java.util.List; @RestController -@RequestMapping +@RequestMapping("/movies") public class MovieController { MovieRepository movieRepository; From 18886e642557fc139fba216658dc7907b884830e Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 10:20:31 +0200 Subject: [PATCH 06/23] Added @autowired. --- .../com/booleanuk/api/cinema/controller/MovieController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java index 6f1e7175..01bc302d 100644 --- a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java @@ -2,6 +2,7 @@ import com.booleanuk.api.cinema.model.Movie; import com.booleanuk.api.cinema.repository.MovieRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -13,6 +14,8 @@ @RestController @RequestMapping("/movies") public class MovieController { + + @Autowired MovieRepository movieRepository; @PostMapping From 2c5ad17a14f59590856a1326f0a132db50ed872f Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 10:47:45 +0200 Subject: [PATCH 07/23] Updated customer and movie to use OffsetDateTime instead of LocalDateTime to adhere to api spec. --- .../cinema/controller/CustomerController.java | 5 ++--- .../cinema/controller/MovieController.java | 3 ++- .../booleanuk/api/cinema/model/Customer.java | 10 ++++----- .../com/booleanuk/api/cinema/model/Movie.java | 21 ++++++++++++------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java index e19a6de3..08992701 100644 --- a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java @@ -2,14 +2,13 @@ import com.booleanuk.api.cinema.model.Customer; import com.booleanuk.api.cinema.repository.CustomerRepository; -import org.apache.coyote.Response; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @@ -47,7 +46,7 @@ public ResponseEntity updateCustomer(@PathVariable (name = "id") int i customerToUpdate.setName(customer.getName()); customerToUpdate.setPhone(customer.getPhone()); customerToUpdate.setEmail(customer.getEmail()); - customerToUpdate.setUpdatedAt(LocalDateTime.now()); + customerToUpdate.setUpdatedAt(OffsetDateTime.now()); return new ResponseEntity<>(this.customerRepository.save(customerToUpdate), HttpStatus.CREATED); } catch (Exception e) { diff --git a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java index 01bc302d..334f0618 100644 --- a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java @@ -9,6 +9,7 @@ import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @RestController @@ -45,7 +46,7 @@ public ResponseEntity updateMovie(@PathVariable (name = "id") int id, @Re movieToUpdate.setRating(movie.getRating()); movieToUpdate.setDescription(movie.getDescription()); movieToUpdate.setRuntimeMins(movie.getRuntimeMins()); - movieToUpdate.setUpdatedAt(LocalDateTime.now()); + movieToUpdate.setUpdatedAt(OffsetDateTime.now()); return new ResponseEntity<>(this.movieRepository.save(movieToUpdate), HttpStatus.CREATED); } catch (Exception e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update movie: " + e.getMessage()); diff --git a/src/main/java/com/booleanuk/api/cinema/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/model/Customer.java index f213c88e..eff7a5f8 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Customer.java @@ -5,7 +5,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; @Getter @Setter @@ -35,10 +35,10 @@ public Customer (String name, String email, String phone) { private String phone; @Column(name = "createdAt", nullable = false) - private LocalDateTime createdAt; + private OffsetDateTime createdAt; @Column(name = "updatedAt", nullable = false) - private LocalDateTime updatedAt; + private OffsetDateTime updatedAt; @PrePersist @@ -46,7 +46,7 @@ private void onCreate() { /* This method is called before the entity manager saves the entity to the database. */ - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } } diff --git a/src/main/java/com/booleanuk/api/cinema/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/model/Movie.java index 28c41612..3aa03ead 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Movie.java @@ -1,21 +1,26 @@ package com.booleanuk.api.cinema.model; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDateTime; +import java.time.OffsetDateTime; @Getter @Setter @NoArgsConstructor -@AllArgsConstructor @Entity @Table(name = "movies") public class Movie { + public Movie (String title, String rating, String description, int runtimeMins) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @@ -30,20 +35,20 @@ public class Movie { private String description; @Column(name = "runtimeMins", nullable = false) - private String runtimeMins; + private int runtimeMins; @Column(name = "createdAt", nullable = false) - private LocalDateTime createdAt; + private OffsetDateTime createdAt; @Column(name = "updatedAt", nullable = false) - private LocalDateTime updatedAt; + private OffsetDateTime updatedAt; @PrePersist private void onCreate() { /* This method is called before the entity manager saves the entity to the database. */ - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = OffsetDateTime.now(); + this.updatedAt = OffsetDateTime.now(); } } From 2a9c8c3fbdc4d9a410f88e6e6ec4ed6a4420c8fc Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 11:19:15 +0200 Subject: [PATCH 08/23] Implemented screening functionality satisfying core requirements. Added some extra functionality for fun. --- .../controller/ScreeningController.java | 73 +++++++++++++++++++ .../booleanuk/api/cinema/model/Screening.java | 39 +++++++--- .../api/cinema/model/ScreeningDTO.java | 29 ++++++++ 3 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java diff --git a/src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java b/src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java new file mode 100644 index 00000000..2179351c --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java @@ -0,0 +1,73 @@ +package com.booleanuk.api.cinema.controller; + +import com.booleanuk.api.cinema.model.Screening; +import com.booleanuk.api.cinema.model.ScreeningDTO; +import com.booleanuk.api.cinema.repository.ScreeningRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@RestController +@RequestMapping("/screenings") +public class ScreeningController { + @Autowired + ScreeningRepository screeningRepository; + + @PostMapping + public ResponseEntity addScreening(@RequestBody ScreeningDTO screeningDTO) throws ResponseStatusException { + try { + return new ResponseEntity<>(this.screeningRepository.save(convertFromDTO(screeningDTO)), HttpStatus.CREATED); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to create a screening: " + e.getMessage()); + } + } + + @GetMapping + public ResponseEntity> getAllScreenings() throws ResponseStatusException { + return ResponseEntity.ok(this.screeningRepository.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getScreeningById(@PathVariable (name = "id") int id) throws ResponseStatusException { + return ResponseEntity.ok(findScreeningById(id)); + } + + @PutMapping("/{id}") + public ResponseEntity updateScreening(@PathVariable (name = "id") int id, @RequestBody ScreeningDTO screeningDTO) throws ResponseStatusException { + Screening screeningToUpdate = findScreeningById(id); + try { + Screening convertedScreening = convertFromDTO(screeningDTO); + screeningToUpdate.setScreenNumber(convertedScreening.getScreenNumber()); + screeningToUpdate.setCapacity(convertedScreening.getCapacity()); + screeningToUpdate.setStartsAt(convertedScreening.getStartsAt()); + screeningToUpdate.setUpdatedAt(LocalDateTime.now()); + return new ResponseEntity<>(this.screeningRepository.save(screeningToUpdate), HttpStatus.CREATED); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update screening: " + e.getMessage()); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteScreening(@PathVariable (name = "id") int id) throws ResponseStatusException { + Screening screeningToDelete = findScreeningById(id); + this.screeningRepository.delete(screeningToDelete); + return ResponseEntity.ok(screeningToDelete); + } + + private Screening findScreeningById(int id) throws ResponseStatusException { + return this.screeningRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Screening with the provided ID does not exist.")); + } + + private Screening convertFromDTO(ScreeningDTO screeningDTO) { + String formattedDate = screeningDTO.getStartsAt().replace(" ", "T"); + OffsetDateTime startsAt = OffsetDateTime.parse(formattedDate, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + return new Screening(screeningDTO.getScreenNumber(), screeningDTO.getCapacity(), startsAt); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/model/Screening.java index 26da2222..003664c8 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/model/Screening.java @@ -7,6 +7,8 @@ import lombok.Setter; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; @Getter @Setter @@ -16,25 +18,40 @@ @Entity @Table(name = "Screening") public class Screening { + public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { + this.screenNumber = screenNumber; + this.capacity = capacity; + this.startsAt = startsAt; + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(name = "movieId") - int movieId; + private int movieId; + + @Column(name = "screenNumber", nullable = false) + private int screenNumber; - @Column(name = "screenNumber") - int screenNumber; + @Column(name = "startsAt", nullable = false) + private OffsetDateTime startsAt; - @Column(name = "startsAt") - LocalDateTime startsAt; + @Column(name = "capacity", nullable = false) + private int capacity; - @Column(name = "capacity") - int capacity; + @Column(name = "createdAt", nullable = false) + private LocalDateTime createdAt; - @Column(name = "createdAt") - LocalDateTime createdAt; + @Column(name = "updatedAt", nullable = false) + private LocalDateTime updatedAt; - @Column(name = "updatedAt") - LocalDateTime updatedAt; + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java b/src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java new file mode 100644 index 00000000..bacfdcca --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java @@ -0,0 +1,29 @@ +package com.booleanuk.api.cinema.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor + +public class ScreeningDTO { + /* + DTO to accept time format specified in api spec. + Used as a middle man to convert to the desired time format when storing in database. + */ + + public ScreeningDTO (int screenNumber, int capacity, String startsAt) { + this.screenNumber = screenNumber; + this.capacity = capacity; + this.startsAt = startsAt; + } + + private int screenNumber; + + private String startsAt; + + private int capacity; + +} From d14dc3e3e91bf30ab1d3cc7b039c9d4be7cc8780 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Wed, 11 Sep 2024 11:19:31 +0200 Subject: [PATCH 09/23] Implemented screening functionality satisfying core requirements. Added some extra functionality for fun. --- .../booleanuk/api/cinema/controller/ScreeningRepository.java | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java diff --git a/src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java deleted file mode 100644 index 3d0812a5..00000000 --- a/src/main/java/com/booleanuk/api/cinema/controller/ScreeningRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.booleanuk.api.cinema.controller; - -public class ScreeningRepository { -} From 73eecbc55a27ba257f7bfc0aad2964507610ae22 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 08:10:12 +0200 Subject: [PATCH 10/23] Implemented foundation for database relationships. --- .../cinema/{ => customer}/model/Customer.java | 3 +- .../booleanuk/api/cinema/model/Ticket.java | 37 ----------------- .../api/cinema/{ => movie}/model/Movie.java | 12 +++++- .../{ => screening}/model/Screening.java | 20 ++++++--- .../api/cinema/ticket/model/Ticket.java | 41 +++++++++++++++++++ 5 files changed, 67 insertions(+), 46 deletions(-) rename src/main/java/com/booleanuk/api/cinema/{ => customer}/model/Customer.java (95%) delete mode 100644 src/main/java/com/booleanuk/api/cinema/model/Ticket.java rename src/main/java/com/booleanuk/api/cinema/{ => movie}/model/Movie.java (73%) rename src/main/java/com/booleanuk/api/cinema/{ => screening}/model/Screening.java (67%) create mode 100644 src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java diff --git a/src/main/java/com/booleanuk/api/cinema/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java similarity index 95% rename from src/main/java/com/booleanuk/api/cinema/model/Customer.java rename to src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java index eff7a5f8..2a92c09e 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java @@ -1,4 +1,4 @@ -package com.booleanuk.api.cinema.model; +package com.booleanuk.api.cinema.customer.model; import jakarta.persistence.*; import lombok.Getter; @@ -40,7 +40,6 @@ public Customer (String name, String email, String phone) { @Column(name = "updatedAt", nullable = false) private OffsetDateTime updatedAt; - @PrePersist private void onCreate() { /* diff --git a/src/main/java/com/booleanuk/api/cinema/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/model/Ticket.java deleted file mode 100644 index f443e7eb..00000000 --- a/src/main/java/com/booleanuk/api/cinema/model/Ticket.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.booleanuk.api.cinema.model; - -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor - -@Entity -@Table(name = "tickets") -public class Ticket { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; - - @Column(name = "customerId") - private int customerId; - - @Column(name = "screeningId") - private int screeningId; - - @Column(name = "numberOfSeats") - private int numberOfSeats; - - @Column(name = "createdAt") - private LocalDateTime createdAt; - - @Column(name = "updatedAt") - private LocalDateTime updatedAt; -} diff --git a/src/main/java/com/booleanuk/api/cinema/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java similarity index 73% rename from src/main/java/com/booleanuk/api/cinema/model/Movie.java rename to src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java index 3aa03ead..8db7448f 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java @@ -1,11 +1,14 @@ -package com.booleanuk.api.cinema.model; +package com.booleanuk.api.cinema.movie.model; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.time.OffsetDateTime; +import java.util.List; @Getter @Setter @@ -14,11 +17,12 @@ @Entity @Table(name = "movies") public class Movie { - public Movie (String title, String rating, String description, int runtimeMins) { + public Movie (String title, String rating, String description, int runtimeMins, List movieScreenings) { this.title = title; this.rating = rating; this.description = description; this.runtimeMins = runtimeMins; + this.movieScreenings = movieScreenings; } @Id @@ -43,6 +47,10 @@ public Movie (String title, String rating, String description, int runtimeMins) @Column(name = "updatedAt", nullable = false) private OffsetDateTime updatedAt; + @OneToMany(mappedBy = "movies", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnoreProperties("movies") + private List movieScreenings; + @PrePersist private void onCreate() { /* diff --git a/src/main/java/com/booleanuk/api/cinema/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java similarity index 67% rename from src/main/java/com/booleanuk/api/cinema/model/Screening.java rename to src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java index 003664c8..47984625 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java @@ -1,5 +1,9 @@ -package com.booleanuk.api.cinema.model; +package com.booleanuk.api.cinema.screening.model; +import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.ticket.model.Ticket; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -8,7 +12,7 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; +import java.util.List; @Getter @Setter @@ -16,7 +20,7 @@ @AllArgsConstructor @Entity -@Table(name = "Screening") +@Table(name = "screenings") public class Screening { public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { this.screenNumber = screenNumber; @@ -28,8 +32,10 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - @Column(name = "movieId") - private int movieId; + @ManyToOne + @JsonIncludeProperties(value = {}) + @JoinColumn(name = "movieId") + private Movie movie; @Column(name = "screenNumber", nullable = false) private int screenNumber; @@ -46,6 +52,10 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; + @OneToMany(mappedBy = "screenings", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnoreProperties("screenings") + private List screeningTickets; + @PrePersist private void onCreate() { /* diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java new file mode 100644 index 00000000..87346580 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java @@ -0,0 +1,41 @@ +package com.booleanuk.api.cinema.ticket.model; + +import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor + +@Entity +@Table(name = "tickets") +public class Ticket { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @ManyToOne + @JsonIncludeProperties(value = {}) + @JoinColumn(name = "customerId", nullable = false) + private Customer customer; + + @ManyToOne + @JsonIncludeProperties(value = {}) + @JoinColumn(name = "screeningId", nullable = false) + private Screening screening; + + @Column(name = "numberOfSeats") + private int numberOfSeats; + + @Column(name = "createdAt") + private LocalDateTime createdAt; + + @Column(name = "updatedAt") + private LocalDateTime updatedAt; +} From 2438b5b76173ec2c87454df6c8fdb0e94c481b1d Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 10:19:19 +0200 Subject: [PATCH 11/23] Implemented custom responses. --- .../api/cinema/response/ErrorResponse.java | 25 ++++++++++++++++++ .../cinema/response/ResponseInterface.java | 6 +++++ .../api/cinema/response/SuccessResponse.java | 26 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java create mode 100644 src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java create mode 100644 src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java diff --git a/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java b/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java new file mode 100644 index 00000000..416c099b --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java @@ -0,0 +1,25 @@ +package com.booleanuk.api.cinema.response; + +import java.util.HashMap; +import java.util.Map; + +public class ErrorResponse implements ResponseInterface { + private final String status; + private final Map data; + + public ErrorResponse(String message) { + this.status = "error"; + this.data = new HashMap<>(); + this.data.put("message", message); + } + + @Override + public String getStatus() { + return this.status; + } + + @Override + public Object getData() { + return this.data; + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java b/src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java new file mode 100644 index 00000000..a2d79701 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java @@ -0,0 +1,6 @@ +package com.booleanuk.api.cinema.response; + +public interface ResponseInterface { + String getStatus(); + Object getData(); +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java b/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java new file mode 100644 index 00000000..4d447f4f --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java @@ -0,0 +1,26 @@ +package com.booleanuk.api.cinema.response; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class SuccessResponse implements ResponseInterface { + private String status; + private T data; + + public SuccessResponse(T data) { + this.status = "success"; + this.data = data; + } + + @Override + public String getStatus() { + return status; + } + + @Override + public T getData(){ + return data; + } +} From 275ad47e676400f62cc34012b05f5357f225c4d9 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 11:08:22 +0200 Subject: [PATCH 12/23] Refactored customer, and added new response type. --- .../cinema/controller/CustomerController.java | 73 ------------ .../controller/CustomerController.java | 104 ++++++++++++++++++ .../repository/CustomerRepository.java | 4 +- 3 files changed, 106 insertions(+), 75 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java rename src/main/java/com/booleanuk/api/cinema/{ => customer}/repository/CustomerRepository.java (56%) diff --git a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java deleted file mode 100644 index 08992701..00000000 --- a/src/main/java/com/booleanuk/api/cinema/controller/CustomerController.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.booleanuk.api.cinema.controller; - -import com.booleanuk.api.cinema.model.Customer; -import com.booleanuk.api.cinema.repository.CustomerRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.time.OffsetDateTime; -import java.util.List; - - -@RestController -@RequestMapping("/customers") -public class CustomerController { - - @Autowired - CustomerRepository customerRepository; - - @PostMapping - public ResponseEntity addCustomer(@RequestBody Customer customer) throws ResponseStatusException { - try { - return new ResponseEntity<>(this.customerRepository.save(customer), HttpStatus.CREATED); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when creating a customer: " + e.getMessage()); - } - } - - @GetMapping - public ResponseEntity> getAllCustomers() { - return new ResponseEntity<>(this.customerRepository.findAll(), HttpStatus.OK); - } - - @GetMapping("/{id}") - public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) throws ResponseStatusException { - return ResponseEntity.ok(findCustomerById(id)); - } - - @PutMapping("/{id}") - public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @RequestBody Customer customer) throws ResponseStatusException { - Customer customerToUpdate = findCustomerById(id); - - try { - customerToUpdate.setName(customer.getName()); - customerToUpdate.setPhone(customer.getPhone()); - customerToUpdate.setEmail(customer.getEmail()); - customerToUpdate.setUpdatedAt(OffsetDateTime.now()); - - return new ResponseEntity<>(this.customerRepository.save(customerToUpdate), HttpStatus.CREATED); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update customer: " + e.getMessage()); - } - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ - Customer customerToDelete = findCustomerById(id); - try { - this.customerRepository.delete(customerToDelete); - return ResponseEntity.ok(customerToDelete); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to delete customer: " + e.getMessage()); - } - } - - - private Customer findCustomerById(int id){ - return this.customerRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Customer with provided ID was not found")); - } - -} diff --git a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java new file mode 100644 index 00000000..fe4e499a --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java @@ -0,0 +1,104 @@ +package com.booleanuk.api.cinema.customer.controller; + +import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.customer.repository.CustomerRepository; +import com.booleanuk.api.cinema.response.ErrorResponse; +import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.response.SuccessResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.OffsetDateTime; + +@RestController +@RequestMapping("/customers") +public class CustomerController { + + @Autowired + CustomerRepository customerRepository; + + @PostMapping + public ResponseEntity addCustomer(@RequestBody Customer customer) { + ResponseInterface response; + + try { + Customer newCustomer = this.customerRepository.save(customer); + response = new SuccessResponse<>(newCustomer); + return new ResponseEntity<>(response, HttpStatus.CREATED); + + } catch (Exception e) { + return new ResponseEntity<>(new ErrorResponse("bad request"), HttpStatus.BAD_REQUEST); + } + } + + @GetMapping + public ResponseEntity getAllCustomers() { + ResponseInterface response = new SuccessResponse<>(this.customerRepository.findAll()); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { + ResponseInterface response; + Customer customer = findCustomerById(id); + if (customer == null) { + response = new ErrorResponse("not found"); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + response = new SuccessResponse<>(customer); + return ResponseEntity.ok(response); + } + + @PutMapping("/{id}") + public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @RequestBody Customer customer) { + ResponseInterface response; + Customer customerToUpdate = findCustomerById(id); + + if (customerToUpdate == null) { + response = new ErrorResponse("not found"); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + try { + customerToUpdate.setName(customer.getName()); + customerToUpdate.setPhone(customer.getPhone()); + customerToUpdate.setEmail(customer.getEmail()); + customerToUpdate.setUpdatedAt(OffsetDateTime.now()); + + response = new SuccessResponse<>(this.customerRepository.save(customerToUpdate)); + return new ResponseEntity<>(response, HttpStatus.CREATED); + + } catch (Exception e) { + response = new ErrorResponse("bad request"); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ + ResponseInterface response; + Customer customerToDelete = findCustomerById(id); + + if (customerToDelete == null) { + response = new ErrorResponse("not found"); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + try { + this.customerRepository.delete(customerToDelete); + response = new SuccessResponse<>(customerToDelete); + return ResponseEntity.ok(response); + + } catch (Exception e) { + response = new ErrorResponse("bad request"); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + } + + /* Helper functions */ + private Customer findCustomerById(int id) { + return this.customerRepository.findById(id).orElse(null); + } +} diff --git a/src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java b/src/main/java/com/booleanuk/api/cinema/customer/repository/CustomerRepository.java similarity index 56% rename from src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java rename to src/main/java/com/booleanuk/api/cinema/customer/repository/CustomerRepository.java index 5ddf67bc..4d04e064 100644 --- a/src/main/java/com/booleanuk/api/cinema/repository/CustomerRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/repository/CustomerRepository.java @@ -1,6 +1,6 @@ -package com.booleanuk.api.cinema.repository; +package com.booleanuk.api.cinema.customer.repository; -import com.booleanuk.api.cinema.model.Customer; +import com.booleanuk.api.cinema.customer.model.Customer; import org.springframework.data.jpa.repository.JpaRepository; public interface CustomerRepository extends JpaRepository { From 5307199d2f9c9529eea3b7a80508c0a7310f0b82 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 11:08:56 +0200 Subject: [PATCH 13/23] Implemented responsefactory to abstract the return types in the controllers. --- .../api/cinema/response/ResponseFactory.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java diff --git a/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java b/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java new file mode 100644 index 00000000..e87acf53 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java @@ -0,0 +1,31 @@ +package com.booleanuk.api.cinema.response; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ResponseFactory { + + // Static method to create an HttpStatus OK response + public static ResponseEntity OkSuccessResponse(T data) { + SuccessResponse successResponse = new SuccessResponse<>(data); + return new ResponseEntity<>(successResponse, HttpStatus.OK); + } + + // Static method to create an HttpStatus CREATED response + public static ResponseEntity CreatedSuccessResponse(T data) { + SuccessResponse successResponse = new SuccessResponse<>(data); + return new ResponseEntity<>(successResponse, HttpStatus.CREATED); + } + + // Static method to create a Bad Request response + public static ResponseEntity BadRequestErrorResponse() { + ErrorResponse errorResponse = new ErrorResponse("bad request"); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + // Static method to create a Not Found response + public static ResponseEntity NotFoundErrorResponse() { + ErrorResponse errorResponse = new ErrorResponse("not found"); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } +} From fd07e9451c19b6bfd19934452ae400d0728109b3 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 11:33:25 +0200 Subject: [PATCH 14/23] Updated customercontroller with new return. --- .../controller/CustomerController.java | 74 ++++++++----------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java index fe4e499a..3fdd5d08 100644 --- a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java @@ -2,15 +2,15 @@ import com.booleanuk.api.cinema.customer.model.Customer; import com.booleanuk.api.cinema.customer.repository.CustomerRepository; -import com.booleanuk.api.cinema.response.ErrorResponse; import com.booleanuk.api.cinema.response.ResponseInterface; -import com.booleanuk.api.cinema.response.SuccessResponse; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.OffsetDateTime; +import java.util.Optional; + +import static com.booleanuk.api.cinema.response.ResponseFactory.*; @RestController @RequestMapping("/customers") @@ -21,84 +21,72 @@ public class CustomerController { @PostMapping public ResponseEntity addCustomer(@RequestBody Customer customer) { - ResponseInterface response; - try { Customer newCustomer = this.customerRepository.save(customer); - response = new SuccessResponse<>(newCustomer); - return new ResponseEntity<>(response, HttpStatus.CREATED); + return CreatedSuccessResponse(newCustomer); } catch (Exception e) { - return new ResponseEntity<>(new ErrorResponse("bad request"), HttpStatus.BAD_REQUEST); + return BadRequestErrorResponse(); } } @GetMapping public ResponseEntity getAllCustomers() { - ResponseInterface response = new SuccessResponse<>(this.customerRepository.findAll()); - return new ResponseEntity<>(response, HttpStatus.OK); + return OkSuccessResponse(this.customerRepository.findAll()); } @GetMapping("/{id}") public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { - ResponseInterface response; - Customer customer = findCustomerById(id); - if (customer == null) { - response = new ErrorResponse("not found"); - return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + var customer = findCustomerById(id); + if (customer.isEmpty()) { + return NotFoundErrorResponse(); } - response = new SuccessResponse<>(customer); - return ResponseEntity.ok(response); + return OkSuccessResponse(customer); } @PutMapping("/{id}") public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @RequestBody Customer customer) { - ResponseInterface response; - Customer customerToUpdate = findCustomerById(id); - if (customerToUpdate == null) { - response = new ErrorResponse("not found"); - return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + if (findCustomerById(id).isEmpty()) { + return NotFoundErrorResponse(); } try { - customerToUpdate.setName(customer.getName()); - customerToUpdate.setPhone(customer.getPhone()); - customerToUpdate.setEmail(customer.getEmail()); - customerToUpdate.setUpdatedAt(OffsetDateTime.now()); + Customer customerToUpdate = findCustomerById(id).get(); + updateCustomer(customerToUpdate, customer); + Customer updatedCustomer = this.customerRepository.save(customerToUpdate); - response = new SuccessResponse<>(this.customerRepository.save(customerToUpdate)); - return new ResponseEntity<>(response, HttpStatus.CREATED); + return CreatedSuccessResponse(updatedCustomer); } catch (Exception e) { - response = new ErrorResponse("bad request"); - return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + return BadRequestErrorResponse(); } } @DeleteMapping("/{id}") public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ - ResponseInterface response; - Customer customerToDelete = findCustomerById(id); - - if (customerToDelete == null) { - response = new ErrorResponse("not found"); - return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + if (findCustomerById(id).isEmpty()) { + return NotFoundErrorResponse(); } try { + Customer customerToDelete = findCustomerById(id).get(); this.customerRepository.delete(customerToDelete); - response = new SuccessResponse<>(customerToDelete); - return ResponseEntity.ok(response); - + return OkSuccessResponse(customerToDelete); } catch (Exception e) { - response = new ErrorResponse("bad request"); - return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + return BadRequestErrorResponse(); } } /* Helper functions */ - private Customer findCustomerById(int id) { - return this.customerRepository.findById(id).orElse(null); + private Optional findCustomerById(int id) { + return this.customerRepository.findById(id); + } + + private void updateCustomer(Customer oldCustomer, Customer newCustomer) { + oldCustomer.setName(newCustomer.getName()); + oldCustomer.setPhone(newCustomer.getPhone()); + oldCustomer.setEmail(newCustomer.getEmail()); + oldCustomer.setUpdatedAt(OffsetDateTime.now()); } } From 6fa51c88033d863dcabf1c9df5b4e6d7fa2b53d3 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 11:57:57 +0200 Subject: [PATCH 15/23] Updated moviecontroller to new response format. --- .../cinema/controller/MovieController.java | 66 --------------- .../movie/controller/MovieController.java | 83 +++++++++++++++++++ 2 files changed, 83 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/controller/MovieController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java diff --git a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java deleted file mode 100644 index 334f0618..00000000 --- a/src/main/java/com/booleanuk/api/cinema/controller/MovieController.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.booleanuk.api.cinema.controller; - -import com.booleanuk.api.cinema.model.Movie; -import com.booleanuk.api.cinema.repository.MovieRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.util.List; - -@RestController -@RequestMapping("/movies") -public class MovieController { - - @Autowired - MovieRepository movieRepository; - - @PostMapping - public ResponseEntity addMovie(@RequestBody Movie movie) throws ResponseStatusException { - try { - return new ResponseEntity<>(this.movieRepository.save(movie), HttpStatus.CREATED); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to create a movie: " + e.getMessage()); - } - } - - @GetMapping - public ResponseEntity> getAllMovies() throws ResponseStatusException { - return ResponseEntity.ok(this.movieRepository.findAll()); - } - - @GetMapping("/{id}") - public ResponseEntity getMovieById(@PathVariable (name = "id") int id) throws ResponseStatusException { - return ResponseEntity.ok(findMovieById(id)); - } - - @PutMapping("/{id}") - public ResponseEntity updateMovie(@PathVariable (name = "id") int id, @RequestBody Movie movie) throws ResponseStatusException { - Movie movieToUpdate = findMovieById(id); - try { - movieToUpdate.setTitle(movie.getTitle()); - movieToUpdate.setRating(movie.getRating()); - movieToUpdate.setDescription(movie.getDescription()); - movieToUpdate.setRuntimeMins(movie.getRuntimeMins()); - movieToUpdate.setUpdatedAt(OffsetDateTime.now()); - return new ResponseEntity<>(this.movieRepository.save(movieToUpdate), HttpStatus.CREATED); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update movie: " + e.getMessage()); - } - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) throws ResponseStatusException { - Movie movieToDelete = findMovieById(id); - this.movieRepository.delete(movieToDelete); - return ResponseEntity.ok(movieToDelete); - } - - private Movie findMovieById(int id) throws ResponseStatusException { - return this.movieRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Movie with the provided ID does not exist.")); - } -} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java new file mode 100644 index 00000000..349a525c --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java @@ -0,0 +1,83 @@ +package com.booleanuk.api.cinema.movie.controller; + +import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.movie.repository.MovieRepository; +import com.booleanuk.api.cinema.response.ResponseInterface; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import static com.booleanuk.api.cinema.response.ResponseFactory.*; + +@RestController +@RequestMapping("/movies") +public class MovieController { + + @Autowired + MovieRepository movieRepository; + + @PostMapping + public ResponseEntity addMovie(@RequestBody Movie movie) throws ResponseStatusException { + try { + return CreatedSuccessResponse(this.movieRepository.save(movie)); + } catch (Exception e) { + return BadRequestErrorResponse(); + } + } + + @GetMapping + public ResponseEntity getAllMovies() throws ResponseStatusException { + return OkSuccessResponse(this.movieRepository.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getMovieById(@PathVariable (name = "id") int id) throws ResponseStatusException { + if (findMovieById(id).isEmpty()){ + return NotFoundErrorResponse(); + } + return OkSuccessResponse(findMovieById(id).get()); + } + + + @PutMapping("/{id}") + public ResponseEntity updateMovie(@PathVariable (name = "id") int id, @RequestBody Movie updatedMovie) throws ResponseStatusException { + if (findMovieById(id).isEmpty()) { + return NotFoundErrorResponse(); + } + + try { + Movie movieToUpdate = findMovieById(id).get(); + update(movieToUpdate, updatedMovie); + return CreatedSuccessResponse(this.movieRepository.save(movieToUpdate)); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update movie: " + e.getMessage()); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) throws ResponseStatusException { + if (findMovieById(id).isEmpty()){ + return NotFoundErrorResponse(); + } + Movie movieToDelete = findMovieById(id).get(); + this.movieRepository.delete(movieToDelete); + return OkSuccessResponse(movieToDelete); + } + + private Optional findMovieById(int id) { + return this.movieRepository.findById(id); + } + + private void update(Movie oldMovie, Movie newMovie) { + oldMovie.setTitle(newMovie.getTitle()); + oldMovie.setRating(newMovie.getRating()); + oldMovie.setDescription(newMovie.getDescription()); + oldMovie.setRuntimeMins(newMovie.getRuntimeMins()); + oldMovie.setUpdatedAt(OffsetDateTime.now()); + } +} From 6c28bfe02f0906f6d807261154829c51979ea284 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 12:12:58 +0200 Subject: [PATCH 16/23] Updated screeningcontroller to new response format. --- .../controller/ScreeningController.java | 73 ------------------- .../controller/ScreeningController.java | 46 ++++++++++++ 2 files changed, 46 insertions(+), 73 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java create mode 100644 src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java diff --git a/src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java b/src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java deleted file mode 100644 index 2179351c..00000000 --- a/src/main/java/com/booleanuk/api/cinema/controller/ScreeningController.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.booleanuk.api.cinema.controller; - -import com.booleanuk.api.cinema.model.Screening; -import com.booleanuk.api.cinema.model.ScreeningDTO; -import com.booleanuk.api.cinema.repository.ScreeningRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; - -@RestController -@RequestMapping("/screenings") -public class ScreeningController { - @Autowired - ScreeningRepository screeningRepository; - - @PostMapping - public ResponseEntity addScreening(@RequestBody ScreeningDTO screeningDTO) throws ResponseStatusException { - try { - return new ResponseEntity<>(this.screeningRepository.save(convertFromDTO(screeningDTO)), HttpStatus.CREATED); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to create a screening: " + e.getMessage()); - } - } - - @GetMapping - public ResponseEntity> getAllScreenings() throws ResponseStatusException { - return ResponseEntity.ok(this.screeningRepository.findAll()); - } - - @GetMapping("/{id}") - public ResponseEntity getScreeningById(@PathVariable (name = "id") int id) throws ResponseStatusException { - return ResponseEntity.ok(findScreeningById(id)); - } - - @PutMapping("/{id}") - public ResponseEntity updateScreening(@PathVariable (name = "id") int id, @RequestBody ScreeningDTO screeningDTO) throws ResponseStatusException { - Screening screeningToUpdate = findScreeningById(id); - try { - Screening convertedScreening = convertFromDTO(screeningDTO); - screeningToUpdate.setScreenNumber(convertedScreening.getScreenNumber()); - screeningToUpdate.setCapacity(convertedScreening.getCapacity()); - screeningToUpdate.setStartsAt(convertedScreening.getStartsAt()); - screeningToUpdate.setUpdatedAt(LocalDateTime.now()); - return new ResponseEntity<>(this.screeningRepository.save(screeningToUpdate), HttpStatus.CREATED); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update screening: " + e.getMessage()); - } - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteScreening(@PathVariable (name = "id") int id) throws ResponseStatusException { - Screening screeningToDelete = findScreeningById(id); - this.screeningRepository.delete(screeningToDelete); - return ResponseEntity.ok(screeningToDelete); - } - - private Screening findScreeningById(int id) throws ResponseStatusException { - return this.screeningRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Screening with the provided ID does not exist.")); - } - - private Screening convertFromDTO(ScreeningDTO screeningDTO) { - String formattedDate = screeningDTO.getStartsAt().replace(" ", "T"); - OffsetDateTime startsAt = OffsetDateTime.parse(formattedDate, DateTimeFormatter.ISO_OFFSET_DATE_TIME); - return new Screening(screeningDTO.getScreenNumber(), screeningDTO.getCapacity(), startsAt); - } -} diff --git a/src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java b/src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java new file mode 100644 index 00000000..3ece51fb --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java @@ -0,0 +1,46 @@ +package com.booleanuk.api.cinema.screening.controller; + +import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.screening.model.ScreeningDTO; +import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +import static com.booleanuk.api.cinema.response.ResponseFactory.*; + + + +@RestController +@RequestMapping("/screenings") +public class ScreeningController { + @Autowired + ScreeningRepository screeningRepository; + + @PostMapping + public ResponseEntity addScreening(@RequestBody ScreeningDTO screeningDTO) throws ResponseStatusException { + try { + Screening screening = convertFromDTO(screeningDTO); + return CreatedSuccessResponse(screening); + } catch (Exception e) { + return BadRequestErrorResponse(); + } + } + + @GetMapping + public ResponseEntity getAllScreenings() throws ResponseStatusException { + return OkSuccessResponse(this.screeningRepository.findAll()); + } + + private Screening convertFromDTO(ScreeningDTO screeningDTO) { + String formattedDate = screeningDTO.getStartsAt().replace(" ", "T"); + OffsetDateTime startsAt = OffsetDateTime.parse(formattedDate, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + return new Screening(screeningDTO.getScreenNumber(), screeningDTO.getCapacity(), startsAt); + } +} From a46c65c457a7edbff248a4160a89196daded988c Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 12:18:29 +0200 Subject: [PATCH 17/23] Updated ticketcontroller to new response format. --- .../ticket/controller/TicketController.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java b/src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java new file mode 100644 index 00000000..4e9ba262 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java @@ -0,0 +1,38 @@ +package com.booleanuk.api.cinema.ticket.controller; + +import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.screening.model.ScreeningDTO; +import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; +import com.booleanuk.api.cinema.ticket.model.Ticket; +import com.booleanuk.api.cinema.ticket.repository.TicketRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +import static com.booleanuk.api.cinema.response.ResponseFactory.*; + +@RestController +@RequestMapping("/tickets") +public class TicketController { + @Autowired + TicketRepository ticketRepository; + + @PostMapping + public ResponseEntity bookTicket(@RequestBody Ticket ticket) { + try { + return CreatedSuccessResponse(ticketRepository.save(ticket)); + } catch (Exception e) { + return NotFoundErrorResponse(); + } + } + + @GetMapping + public ResponseEntity getAllTickets() { + return OkSuccessResponse(this.ticketRepository.findAll()); + } +} From 176ee280e13ea126d00fcb18e75f35f8005ffedf Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 12:19:02 +0200 Subject: [PATCH 18/23] Refactoring. --- .../api/cinema/{ => movie}/repository/MovieRepository.java | 4 ++-- .../api/cinema/{ => screening}/model/ScreeningDTO.java | 2 +- .../{ => screening}/repository/ScreeningRepository.java | 4 ++-- .../api/cinema/{ => ticket}/repository/TicketRepository.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/com/booleanuk/api/cinema/{ => movie}/repository/MovieRepository.java (57%) rename src/main/java/com/booleanuk/api/cinema/{ => screening}/model/ScreeningDTO.java (92%) rename src/main/java/com/booleanuk/api/cinema/{ => screening}/repository/ScreeningRepository.java (56%) rename src/main/java/com/booleanuk/api/cinema/{ => ticket}/repository/TicketRepository.java (57%) diff --git a/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java b/src/main/java/com/booleanuk/api/cinema/movie/repository/MovieRepository.java similarity index 57% rename from src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java rename to src/main/java/com/booleanuk/api/cinema/movie/repository/MovieRepository.java index c689e162..55446633 100644 --- a/src/main/java/com/booleanuk/api/cinema/repository/MovieRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/repository/MovieRepository.java @@ -1,6 +1,6 @@ -package com.booleanuk.api.cinema.repository; +package com.booleanuk.api.cinema.movie.repository; -import com.booleanuk.api.cinema.model.Movie; +import com.booleanuk.api.cinema.movie.model.Movie; import org.springframework.data.jpa.repository.JpaRepository; public interface MovieRepository extends JpaRepository { diff --git a/src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java b/src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java similarity index 92% rename from src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java rename to src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java index bacfdcca..7e302adc 100644 --- a/src/main/java/com/booleanuk/api/cinema/model/ScreeningDTO.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java @@ -1,4 +1,4 @@ -package com.booleanuk.api.cinema.model; +package com.booleanuk.api.cinema.screening.model; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java similarity index 56% rename from src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java rename to src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java index 1235edbd..c8f66306 100644 --- a/src/main/java/com/booleanuk/api/cinema/repository/ScreeningRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java @@ -1,6 +1,6 @@ -package com.booleanuk.api.cinema.repository; +package com.booleanuk.api.cinema.screening.repository; -import com.booleanuk.api.cinema.model.Screening; +import com.booleanuk.api.cinema.screening.model.Screening; import org.springframework.data.jpa.repository.JpaRepository; public interface ScreeningRepository extends JpaRepository { diff --git a/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java b/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java similarity index 57% rename from src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java rename to src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java index ee891c02..e2218bdc 100644 --- a/src/main/java/com/booleanuk/api/cinema/repository/TicketRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java @@ -1,6 +1,6 @@ -package com.booleanuk.api.cinema.repository; +package com.booleanuk.api.cinema.ticket.repository; -import com.booleanuk.api.cinema.model.Ticket; +import com.booleanuk.api.cinema.ticket.model.Ticket; import org.springframework.data.jpa.repository.JpaRepository; public interface TicketRepository extends JpaRepository { From 7a47b5e0e1393da233096b380eb20d33a4b97569 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 14:17:04 +0200 Subject: [PATCH 19/23] Refactoring. --- .../cinema/controller/TicketController.java | 4 -- .../controller/CustomerController.java | 59 +++++++++++++++-- .../movie/controller/MovieController.java | 66 +++++++++++++++---- .../api/cinema/movie/model/Movie.java | 2 +- .../controller/ScreeningController.java | 46 ------------- .../api/cinema/screening/model/Screening.java | 2 +- .../repository/ScreeningRepository.java | 4 ++ .../ticket/controller/TicketController.java | 38 ----------- .../ticket/repository/TicketRepository.java | 5 ++ 9 files changed, 120 insertions(+), 106 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/controller/TicketController.java delete mode 100644 src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java delete mode 100644 src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java diff --git a/src/main/java/com/booleanuk/api/cinema/controller/TicketController.java b/src/main/java/com/booleanuk/api/cinema/controller/TicketController.java deleted file mode 100644 index e753f786..00000000 --- a/src/main/java/com/booleanuk/api/cinema/controller/TicketController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.booleanuk.api.cinema.controller; - -public class TicketController { -} diff --git a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java index 3fdd5d08..09601ddb 100644 --- a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java @@ -3,6 +3,10 @@ import com.booleanuk.api.cinema.customer.model.Customer; import com.booleanuk.api.cinema.customer.repository.CustomerRepository; import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; +import com.booleanuk.api.cinema.ticket.model.Ticket; +import com.booleanuk.api.cinema.ticket.repository.TicketRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -19,6 +23,12 @@ public class CustomerController { @Autowired CustomerRepository customerRepository; + @Autowired + TicketRepository ticketRepository; + + @Autowired + ScreeningRepository screeningRepository; + @PostMapping public ResponseEntity addCustomer(@RequestBody Customer customer) { try { @@ -37,11 +47,10 @@ public ResponseEntity getAllCustomers() { @GetMapping("/{id}") public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { - var customer = findCustomerById(id); - if (customer.isEmpty()) { + if (findCustomerById(id).isEmpty()) { return NotFoundErrorResponse(); } - return OkSuccessResponse(customer); + return OkSuccessResponse(findCustomerById(id).get()); } @PutMapping("/{id}") @@ -53,7 +62,7 @@ public ResponseEntity updateCustomer(@PathVariable (name = "i try { Customer customerToUpdate = findCustomerById(id).get(); - updateCustomer(customerToUpdate, customer); + update(customerToUpdate, customer); Customer updatedCustomer = this.customerRepository.save(customerToUpdate); return CreatedSuccessResponse(updatedCustomer); @@ -78,15 +87,55 @@ public ResponseEntity deleteCustomer(@PathVariable (name = "i } } + /* Tickets */ + + @PostMapping("/{customerId}/screenings/{screeningId}") + public ResponseEntity bookTicket(@PathVariable (name = "customerId") int customerId, + @PathVariable (name = "screeningId") int screeningId, + @RequestBody Ticket ticket) { + + if (this.customerRepository.findById(customerId).isEmpty() || this.screeningRepository.findById(screeningId).isEmpty()) { + return NotFoundErrorResponse(); + } + + Customer customer = this.customerRepository.findById(customerId).get(); + Screening screening = this.screeningRepository.findById(screeningId).get(); + + try { + ticket.setCustomer(customer); + ticket.setScreening(screening); + return CreatedSuccessResponse(ticketRepository.save(ticket)); + } catch (Exception e) { + return NotFoundErrorResponse(); + } + } + + @GetMapping("/{customerId}/screenings/{screeningId}") + public ResponseEntity getAllTickets(@PathVariable (name = "customerId") int customerId, + @PathVariable (name = "screeningId") int screeningId) { + + if (this.customerRepository.findById(customerId).isEmpty() || this.screeningRepository.findById(screeningId).isEmpty()) { + return NotFoundErrorResponse(); + } + + Customer customer = this.customerRepository.findById(customerId).get(); + Screening screening = this.screeningRepository.findById(screeningId).get(); + + return OkSuccessResponse(this.ticketRepository.findAllByCustomerAndScreening(customer, screening)); + } + + /* Helper functions */ private Optional findCustomerById(int id) { return this.customerRepository.findById(id); } - private void updateCustomer(Customer oldCustomer, Customer newCustomer) { + private void update(Customer oldCustomer, Customer newCustomer) { oldCustomer.setName(newCustomer.getName()); oldCustomer.setPhone(newCustomer.getPhone()); oldCustomer.setEmail(newCustomer.getEmail()); oldCustomer.setUpdatedAt(OffsetDateTime.now()); } + + } diff --git a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java index 349a525c..c754fdc4 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java @@ -3,6 +3,9 @@ import com.booleanuk.api.cinema.movie.model.Movie; import com.booleanuk.api.cinema.movie.repository.MovieRepository; import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.screening.model.Screening; +import com.booleanuk.api.cinema.screening.model.ScreeningDTO; +import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,7 +13,7 @@ import org.springframework.web.server.ResponseStatusException; import java.time.OffsetDateTime; -import java.util.Optional; +import java.time.format.DateTimeFormatter; import static com.booleanuk.api.cinema.response.ResponseFactory.*; @@ -21,9 +24,13 @@ public class MovieController { @Autowired MovieRepository movieRepository; + @Autowired + ScreeningRepository screeningRepository; + @PostMapping - public ResponseEntity addMovie(@RequestBody Movie movie) throws ResponseStatusException { + public ResponseEntity addMovie(@RequestBody Movie movie) { try { + return CreatedSuccessResponse(this.movieRepository.save(movie)); } catch (Exception e) { return BadRequestErrorResponse(); @@ -36,22 +43,24 @@ public ResponseEntity getAllMovies() throws ResponseStatusExc } @GetMapping("/{id}") - public ResponseEntity getMovieById(@PathVariable (name = "id") int id) throws ResponseStatusException { - if (findMovieById(id).isEmpty()){ + public ResponseEntity getMovieById(@PathVariable (name = "id") int id) { + Movie movie = findMovieById(id); + + if (findMovieById(id) == null){ return NotFoundErrorResponse(); } - return OkSuccessResponse(findMovieById(id).get()); + return OkSuccessResponse(movie); } @PutMapping("/{id}") public ResponseEntity updateMovie(@PathVariable (name = "id") int id, @RequestBody Movie updatedMovie) throws ResponseStatusException { - if (findMovieById(id).isEmpty()) { + Movie movieToUpdate = findMovieById(id); + if (movieToUpdate == null) { return NotFoundErrorResponse(); } try { - Movie movieToUpdate = findMovieById(id).get(); update(movieToUpdate, updatedMovie); return CreatedSuccessResponse(this.movieRepository.save(movieToUpdate)); } catch (Exception e) { @@ -61,16 +70,51 @@ public ResponseEntity updateMovie(@PathVariable (name = "id") @DeleteMapping("/{id}") public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) throws ResponseStatusException { - if (findMovieById(id).isEmpty()){ + Movie movieToDelete = findMovieById(id); + if (movieToDelete == null){ return NotFoundErrorResponse(); } - Movie movieToDelete = findMovieById(id).get(); this.movieRepository.delete(movieToDelete); return OkSuccessResponse(movieToDelete); } - private Optional findMovieById(int id) { - return this.movieRepository.findById(id); + /* Screenings */ + @PostMapping("/{id}/screenings") + public ResponseEntity addScreening(@PathVariable (name = "id") int id, @RequestBody ScreeningDTO screeningDTO) { + Movie movie = findMovieById(id); + if (movie == null) { + return NotFoundErrorResponse(); + } + + try { + Screening screening = convertFromDTO(screeningDTO); + screening.setMovie(movie); + return CreatedSuccessResponse(this.screeningRepository.save(screening)); + } catch (Exception e) { + return BadRequestErrorResponse(); + } + } + + @GetMapping("{id}/screenings") + public ResponseEntity getAllScreenings(@PathVariable (name = "id") int id) { + Movie movie = findMovieById(id); + + if (movie == null) { + return NotFoundErrorResponse(); + } + return OkSuccessResponse(movie.getMovieScreenings()); + } + + + /* Helper functions */ + private Screening convertFromDTO(ScreeningDTO screeningDTO) { + String formattedDate = screeningDTO.getStartsAt().replace(" ", "T"); + OffsetDateTime startsAt = OffsetDateTime.parse(formattedDate, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + return new Screening(screeningDTO.getScreenNumber(), screeningDTO.getCapacity(), startsAt); + } + + private Movie findMovieById(int id) { + return this.movieRepository.findById(id).orElse(null); } private void update(Movie oldMovie, Movie newMovie) { diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java index 8db7448f..6b42f43d 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java @@ -47,7 +47,7 @@ public Movie (String title, String rating, String description, int runtimeMins, @Column(name = "updatedAt", nullable = false) private OffsetDateTime updatedAt; - @OneToMany(mappedBy = "movies", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnoreProperties("movies") private List movieScreenings; diff --git a/src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java b/src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java deleted file mode 100644 index 3ece51fb..00000000 --- a/src/main/java/com/booleanuk/api/cinema/screening/controller/ScreeningController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.booleanuk.api.cinema.screening.controller; - -import com.booleanuk.api.cinema.response.ResponseInterface; -import com.booleanuk.api.cinema.screening.model.Screening; -import com.booleanuk.api.cinema.screening.model.ScreeningDTO; -import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; - -import static com.booleanuk.api.cinema.response.ResponseFactory.*; - - - -@RestController -@RequestMapping("/screenings") -public class ScreeningController { - @Autowired - ScreeningRepository screeningRepository; - - @PostMapping - public ResponseEntity addScreening(@RequestBody ScreeningDTO screeningDTO) throws ResponseStatusException { - try { - Screening screening = convertFromDTO(screeningDTO); - return CreatedSuccessResponse(screening); - } catch (Exception e) { - return BadRequestErrorResponse(); - } - } - - @GetMapping - public ResponseEntity getAllScreenings() throws ResponseStatusException { - return OkSuccessResponse(this.screeningRepository.findAll()); - } - - private Screening convertFromDTO(ScreeningDTO screeningDTO) { - String formattedDate = screeningDTO.getStartsAt().replace(" ", "T"); - OffsetDateTime startsAt = OffsetDateTime.parse(formattedDate, DateTimeFormatter.ISO_OFFSET_DATE_TIME); - return new Screening(screeningDTO.getScreenNumber(), screeningDTO.getCapacity(), startsAt); - } -} diff --git a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java index 47984625..493db43d 100644 --- a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java @@ -52,7 +52,7 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; - @OneToMany(mappedBy = "screenings", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnoreProperties("screenings") private List screeningTickets; diff --git a/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java index c8f66306..de792b60 100644 --- a/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java @@ -1,7 +1,11 @@ package com.booleanuk.api.cinema.screening.repository; +import com.booleanuk.api.cinema.movie.model.Movie; import com.booleanuk.api.cinema.screening.model.Screening; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ScreeningRepository extends JpaRepository { + } diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java b/src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java deleted file mode 100644 index 4e9ba262..00000000 --- a/src/main/java/com/booleanuk/api/cinema/ticket/controller/TicketController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.booleanuk.api.cinema.ticket.controller; - -import com.booleanuk.api.cinema.response.ResponseInterface; -import com.booleanuk.api.cinema.screening.model.Screening; -import com.booleanuk.api.cinema.screening.model.ScreeningDTO; -import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; -import com.booleanuk.api.cinema.ticket.model.Ticket; -import com.booleanuk.api.cinema.ticket.repository.TicketRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; - -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; - -import static com.booleanuk.api.cinema.response.ResponseFactory.*; - -@RestController -@RequestMapping("/tickets") -public class TicketController { - @Autowired - TicketRepository ticketRepository; - - @PostMapping - public ResponseEntity bookTicket(@RequestBody Ticket ticket) { - try { - return CreatedSuccessResponse(ticketRepository.save(ticket)); - } catch (Exception e) { - return NotFoundErrorResponse(); - } - } - - @GetMapping - public ResponseEntity getAllTickets() { - return OkSuccessResponse(this.ticketRepository.findAll()); - } -} diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java b/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java index e2218bdc..7ae4d968 100644 --- a/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/ticket/repository/TicketRepository.java @@ -1,7 +1,12 @@ package com.booleanuk.api.cinema.ticket.repository; +import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.screening.model.Screening; import com.booleanuk.api.cinema.ticket.model.Ticket; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface TicketRepository extends JpaRepository { + public List findAllByCustomerAndScreening(Customer customer, Screening screening); } From 846b2fa15a627c028aac5869e1cc7bc1fef68715 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Thu, 12 Sep 2024 16:27:37 +0200 Subject: [PATCH 20/23] Implemented remaining functionality to satisfy extension criteria. Refactoring remaining tomorrow. --- .../controller/CustomerController.java | 30 ++++++------ .../movie/controller/MovieController.java | 46 ++++++++++++++++--- .../api/cinema/movie/model/Movie.java | 11 +++-- .../api/cinema/movie/model/MovieDTO.java | 38 +++++++++++++++ .../api/cinema/screening/model/Screening.java | 11 +---- .../repository/ScreeningRepository.java | 2 +- .../api/cinema/ticket/model/Ticket.java | 15 ++++-- 7 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java diff --git a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java index 09601ddb..6cbb2dbb 100644 --- a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java @@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.*; import java.time.OffsetDateTime; -import java.util.Optional; import static com.booleanuk.api.cinema.response.ResponseFactory.*; @@ -47,21 +46,21 @@ public ResponseEntity getAllCustomers() { @GetMapping("/{id}") public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { - if (findCustomerById(id).isEmpty()) { + if (this.customerRepository.findById(id).isEmpty()) { return NotFoundErrorResponse(); } - return OkSuccessResponse(findCustomerById(id).get()); + return OkSuccessResponse(this.customerRepository.findById(id).get()); } @PutMapping("/{id}") public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @RequestBody Customer customer) { - if (findCustomerById(id).isEmpty()) { + if (this.customerRepository.findById(id).isEmpty()) { return NotFoundErrorResponse(); } try { - Customer customerToUpdate = findCustomerById(id).get(); + Customer customerToUpdate = this.customerRepository.findById(id).get(); update(customerToUpdate, customer); Customer updatedCustomer = this.customerRepository.save(customerToUpdate); @@ -74,12 +73,12 @@ public ResponseEntity updateCustomer(@PathVariable (name = "i @DeleteMapping("/{id}") public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ - if (findCustomerById(id).isEmpty()) { + if (this.customerRepository.findById(id).isEmpty()) { return NotFoundErrorResponse(); } try { - Customer customerToDelete = findCustomerById(id).get(); + Customer customerToDelete = this.customerRepository.findById(id).get(); this.customerRepository.delete(customerToDelete); return OkSuccessResponse(customerToDelete); } catch (Exception e) { @@ -94,7 +93,11 @@ public ResponseEntity bookTicket(@PathVariable (name = "custo @PathVariable (name = "screeningId") int screeningId, @RequestBody Ticket ticket) { - if (this.customerRepository.findById(customerId).isEmpty() || this.screeningRepository.findById(screeningId).isEmpty()) { + if (this.customerRepository.findById(customerId).isEmpty()) { + return NotFoundErrorResponse(); + } + + if (this.screeningRepository.findById(screeningId).isEmpty()) { return NotFoundErrorResponse(); } @@ -114,7 +117,11 @@ public ResponseEntity bookTicket(@PathVariable (name = "custo public ResponseEntity getAllTickets(@PathVariable (name = "customerId") int customerId, @PathVariable (name = "screeningId") int screeningId) { - if (this.customerRepository.findById(customerId).isEmpty() || this.screeningRepository.findById(screeningId).isEmpty()) { + if (this.customerRepository.findById(customerId).isEmpty()) { + return NotFoundErrorResponse(); + } + + if (this.screeningRepository.findById(screeningId).isEmpty()) { return NotFoundErrorResponse(); } @@ -125,11 +132,6 @@ public ResponseEntity getAllTickets(@PathVariable (name = "cu } - /* Helper functions */ - private Optional findCustomerById(int id) { - return this.customerRepository.findById(id); - } - private void update(Customer oldCustomer, Customer newCustomer) { oldCustomer.setName(newCustomer.getName()); oldCustomer.setPhone(newCustomer.getPhone()); diff --git a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java index c754fdc4..0d040694 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java @@ -1,19 +1,21 @@ package com.booleanuk.api.cinema.movie.controller; import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.movie.model.MovieDTO; import com.booleanuk.api.cinema.movie.repository.MovieRepository; import com.booleanuk.api.cinema.response.ResponseInterface; import com.booleanuk.api.cinema.screening.model.Screening; import com.booleanuk.api.cinema.screening.model.ScreeningDTO; import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; import static com.booleanuk.api.cinema.response.ResponseFactory.*; @@ -28,17 +30,25 @@ public class MovieController { ScreeningRepository screeningRepository; @PostMapping - public ResponseEntity addMovie(@RequestBody Movie movie) { + public ResponseEntity addMovie(@RequestBody MovieDTO movieDTO) { try { + List screenings = convertScreeningDTOList(movieDTO.getScreenings()); - return CreatedSuccessResponse(this.movieRepository.save(movie)); + Movie movie = extractMovieFromMovieDTO(movieDTO); + movie.setScreenings(screenings); + movie = this.movieRepository.save(movie); + + addMovieToScreenings(screenings, movie); + saveScreenings(screenings); + + return CreatedSuccessResponse(movie); } catch (Exception e) { return BadRequestErrorResponse(); } } @GetMapping - public ResponseEntity getAllMovies() throws ResponseStatusException { + public ResponseEntity getAllMovies() { return OkSuccessResponse(this.movieRepository.findAll()); } @@ -64,7 +74,7 @@ public ResponseEntity updateMovie(@PathVariable (name = "id") update(movieToUpdate, updatedMovie); return CreatedSuccessResponse(this.movieRepository.save(movieToUpdate)); } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "An error occurred when attempting to update movie: " + e.getMessage()); + return BadRequestErrorResponse(); } } @@ -102,7 +112,8 @@ public ResponseEntity getAllScreenings(@PathVariable (name = if (movie == null) { return NotFoundErrorResponse(); } - return OkSuccessResponse(movie.getMovieScreenings()); + + return OkSuccessResponse(this.screeningRepository.findScreeningsByMovie(movie)); } @@ -117,6 +128,12 @@ private Movie findMovieById(int id) { return this.movieRepository.findById(id).orElse(null); } + private List convertScreeningDTOList(List screeningDTOList) { + List screenings = new ArrayList<>(); + screeningDTOList.forEach(s -> screenings.add(convertFromDTO(s))); + return screenings; + } + private void update(Movie oldMovie, Movie newMovie) { oldMovie.setTitle(newMovie.getTitle()); oldMovie.setRating(newMovie.getRating()); @@ -124,4 +141,21 @@ private void update(Movie oldMovie, Movie newMovie) { oldMovie.setRuntimeMins(newMovie.getRuntimeMins()); oldMovie.setUpdatedAt(OffsetDateTime.now()); } + + private Movie extractMovieFromMovieDTO(MovieDTO movieDTO){ + Movie movie = new Movie(); + movie.setTitle(movieDTO.getTitle()); + movie.setRating(movieDTO.getRating()); + movie.setDescription(movieDTO.getDescription()); + movie.setRuntimeMins(movieDTO.getRuntimeMins()); + return movie; + } + + private void saveScreenings(List screenings){ + screenings.forEach(s -> this.screeningRepository.save(s)); + } + + private void addMovieToScreenings(List screenings, Movie movie) { + screenings.forEach(s -> s.setMovie(movie)); + } } diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java index 6b42f43d..a6a2f4e6 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java @@ -1,7 +1,7 @@ package com.booleanuk.api.cinema.movie.model; import com.booleanuk.api.cinema.screening.model.Screening; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,12 +17,12 @@ @Entity @Table(name = "movies") public class Movie { - public Movie (String title, String rating, String description, int runtimeMins, List movieScreenings) { + public Movie (String title, String rating, String description, int runtimeMins, List screenings) { this.title = title; this.rating = rating; this.description = description; this.runtimeMins = runtimeMins; - this.movieScreenings = movieScreenings; + this.screenings = screenings; } @Id @@ -47,9 +47,10 @@ public Movie (String title, String rating, String description, int runtimeMins, @Column(name = "updatedAt", nullable = false) private OffsetDateTime updatedAt; + @JsonIgnore @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JsonIgnoreProperties("movies") - private List movieScreenings; + /* Ignore this list in response */ + private List screenings; @PrePersist private void onCreate() { diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java new file mode 100644 index 00000000..132ee097 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java @@ -0,0 +1,38 @@ +package com.booleanuk.api.cinema.movie.model; + +import com.booleanuk.api.cinema.screening.model.ScreeningDTO; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor + +public class MovieDTO { + /* + Another DTO to handle the messed up time format from the API spec for screenings. + */ + public MovieDTO (String title, String rating, String description, int runtimeMins, List screeningsDTO) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = screeningsDTO; + } + + private int id; + + private String title; + + private String rating; + + private String description; + + private int runtimeMins; + + private List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java index 493db43d..02a1678f 100644 --- a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java @@ -1,9 +1,7 @@ package com.booleanuk.api.cinema.screening.model; import com.booleanuk.api.cinema.movie.model.Movie; -import com.booleanuk.api.cinema.ticket.model.Ticket; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,7 +10,6 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.util.List; @Getter @Setter @@ -33,7 +30,7 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { private int id; @ManyToOne - @JsonIncludeProperties(value = {}) + @JsonIgnore @JoinColumn(name = "movieId") private Movie movie; @@ -52,10 +49,6 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JsonIgnoreProperties("screenings") - private List screeningTickets; - @PrePersist private void onCreate() { /* diff --git a/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java index de792b60..ee42c806 100644 --- a/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/repository/ScreeningRepository.java @@ -7,5 +7,5 @@ import java.util.List; public interface ScreeningRepository extends JpaRepository { - + public List findScreeningsByMovie(Movie movie); } diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java index 87346580..625fc911 100644 --- a/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java +++ b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java @@ -2,7 +2,7 @@ import com.booleanuk.api.cinema.customer.model.Customer; import com.booleanuk.api.cinema.screening.model.Screening; -import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; @@ -20,13 +20,13 @@ public class Ticket { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + @JsonIgnore @ManyToOne - @JsonIncludeProperties(value = {}) @JoinColumn(name = "customerId", nullable = false) private Customer customer; + @JsonIgnore @ManyToOne - @JsonIncludeProperties(value = {}) @JoinColumn(name = "screeningId", nullable = false) private Screening screening; @@ -38,4 +38,13 @@ public class Ticket { @Column(name = "updatedAt") private LocalDateTime updatedAt; + + @PrePersist + private void onCreate() { + /* + This method is called before the entity manager saves the entity to the database. + */ + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } } From 6c198cc17a48fe404d4f503608343dd1d4190e0a Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Fri, 13 Sep 2024 12:02:38 +0200 Subject: [PATCH 21/23] Severe refactoring of movie and screenings. Implemented movie DTOs. --- build.gradle | 4 + .../movie/controller/MovieController.java | 179 ++++++++---------- .../api/cinema/movie/model/Movie.java | 16 +- .../api/cinema/movie/model/MovieDTO.java | 38 ---- .../cinema/movie/model/MovieResponseDTO.java | 38 ++++ .../cinema/movie/model/MovieUpdateDTO.java | 38 ++++ .../api/cinema/response/ErrorResponse.java | 2 +- .../{ResponseInterface.java => Response.java} | 2 +- .../api/cinema/response/ResponseFactory.java | 14 +- .../api/cinema/response/SuccessResponse.java | 2 +- .../api/cinema/screening/model/Screening.java | 19 +- .../cinema/screening/model/ScreeningDTO.java | 29 --- 12 files changed, 199 insertions(+), 182 deletions(-) delete mode 100644 src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java create mode 100644 src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java create mode 100644 src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java rename src/main/java/com/booleanuk/api/cinema/response/{ResponseInterface.java => Response.java} (71%) delete mode 100644 src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java diff --git a/build.gradle b/build.gradle index 3d03b1c7..3550f099 100644 --- a/build.gradle +++ b/build.gradle @@ -24,8 +24,12 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // Lombok compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok' + // To use @Valid + implementation 'org.springframework.boot:spring-boot-starter-validation' + } tasks.named('test') { diff --git a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java index 0d040694..2adf3016 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java @@ -1,19 +1,20 @@ package com.booleanuk.api.cinema.movie.controller; import com.booleanuk.api.cinema.movie.model.Movie; -import com.booleanuk.api.cinema.movie.model.MovieDTO; +import com.booleanuk.api.cinema.movie.model.MovieResponseDTO; +import com.booleanuk.api.cinema.movie.model.MovieUpdateDTO; import com.booleanuk.api.cinema.movie.repository.MovieRepository; -import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.response.Response; +import com.booleanuk.api.cinema.response.ResponseFactory; import com.booleanuk.api.cinema.screening.model.Screening; -import com.booleanuk.api.cinema.screening.model.ScreeningDTO; import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @@ -30,132 +31,116 @@ public class MovieController { ScreeningRepository screeningRepository; @PostMapping - public ResponseEntity addMovie(@RequestBody MovieDTO movieDTO) { - try { - List screenings = convertScreeningDTOList(movieDTO.getScreenings()); + public ResponseEntity addMovie(@Valid @RequestBody Movie movie, BindingResult result) { + if (result.hasErrors()){ + return badRequestErrorResponse(); + } - Movie movie = extractMovieFromMovieDTO(movieDTO); - movie.setScreenings(screenings); - movie = this.movieRepository.save(movie); + // Set movie to each screening. + if (movie.getScreenings() != null) { + movie.getScreenings().forEach(s -> s.setMovie(movie)); + } - addMovieToScreenings(screenings, movie); - saveScreenings(screenings); + // Save movie + Movie savedMovie = this.movieRepository.save(movie); - return CreatedSuccessResponse(movie); - } catch (Exception e) { - return BadRequestErrorResponse(); - } - } + // Convert to DTO without screenings + MovieResponseDTO responseDTO = convertToResponseDTO(savedMovie); - @GetMapping - public ResponseEntity getAllMovies() { - return OkSuccessResponse(this.movieRepository.findAll()); + return createdSuccessResponse(responseDTO); } - @GetMapping("/{id}") - public ResponseEntity getMovieById(@PathVariable (name = "id") int id) { - Movie movie = findMovieById(id); - if (findMovieById(id) == null){ - return NotFoundErrorResponse(); - } - return OkSuccessResponse(movie); + @GetMapping + public ResponseEntity getAllMovies() { + List responseList = new ArrayList<>(); + this.movieRepository.findAll().forEach(movie -> responseList.add(convertToResponseDTO(movie))); + return okSuccessResponse(responseList); } @PutMapping("/{id}") - public ResponseEntity updateMovie(@PathVariable (name = "id") int id, @RequestBody Movie updatedMovie) throws ResponseStatusException { - Movie movieToUpdate = findMovieById(id); - if (movieToUpdate == null) { - return NotFoundErrorResponse(); - } + public ResponseEntity updateMovie(@PathVariable (name = "id") int id, + @Valid @RequestBody MovieUpdateDTO updatedMovie, + BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); - try { - update(movieToUpdate, updatedMovie); - return CreatedSuccessResponse(this.movieRepository.save(movieToUpdate)); - } catch (Exception e) { - return BadRequestErrorResponse(); } + + return this.movieRepository.findById(id) + .map(movieToUpdate -> { + updateMovieDetails(movieToUpdate, updatedMovie); + Movie savedMovie = this.movieRepository.save(movieToUpdate); + MovieResponseDTO responseDTO = convertToResponseDTO(savedMovie); + return createdSuccessResponse(responseDTO); + }).orElseGet(ResponseFactory::notFoundErrorResponse); } @DeleteMapping("/{id}") - public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) throws ResponseStatusException { - Movie movieToDelete = findMovieById(id); - if (movieToDelete == null){ - return NotFoundErrorResponse(); - } - this.movieRepository.delete(movieToDelete); - return OkSuccessResponse(movieToDelete); + public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) { + return this.movieRepository.findById(id) + .map(movie -> { + this.movieRepository.delete(movie); + MovieResponseDTO responseDTO = convertToResponseDTO(movie); + return okSuccessResponse(responseDTO); + }) + .orElseGet(ResponseFactory::notFoundErrorResponse); } /* Screenings */ @PostMapping("/{id}/screenings") - public ResponseEntity addScreening(@PathVariable (name = "id") int id, @RequestBody ScreeningDTO screeningDTO) { - Movie movie = findMovieById(id); - if (movie == null) { - return NotFoundErrorResponse(); + public ResponseEntity addScreening(@PathVariable (name = "id") int id, + @Valid @RequestBody Screening screening, + BindingResult result) { + + if (result.hasErrors()) { + return badRequestErrorResponse(); } - try { - Screening screening = convertFromDTO(screeningDTO); + return this.movieRepository.findById(id).map(movie -> { screening.setMovie(movie); - return CreatedSuccessResponse(this.screeningRepository.save(screening)); - } catch (Exception e) { - return BadRequestErrorResponse(); - } + return createdSuccessResponse(this.screeningRepository.save(screening)); + }).orElseGet(ResponseFactory::notFoundErrorResponse); } @GetMapping("{id}/screenings") - public ResponseEntity getAllScreenings(@PathVariable (name = "id") int id) { - Movie movie = findMovieById(id); - - if (movie == null) { - return NotFoundErrorResponse(); - } - - return OkSuccessResponse(this.screeningRepository.findScreeningsByMovie(movie)); + public ResponseEntity getAllScreenings(@PathVariable (name = "id") int id) { + return this.movieRepository.findById(id) + .map(movie -> okSuccessResponse(this.screeningRepository.findScreeningsByMovie(movie))) + .orElseGet(ResponseFactory::notFoundErrorResponse); } - /* Helper functions */ - private Screening convertFromDTO(ScreeningDTO screeningDTO) { - String formattedDate = screeningDTO.getStartsAt().replace(" ", "T"); - OffsetDateTime startsAt = OffsetDateTime.parse(formattedDate, DateTimeFormatter.ISO_OFFSET_DATE_TIME); - return new Screening(screeningDTO.getScreenNumber(), screeningDTO.getCapacity(), startsAt); - } - - private Movie findMovieById(int id) { - return this.movieRepository.findById(id).orElse(null); - } + private void updateMovieDetails(Movie oldMovie, MovieUpdateDTO newMovie) { + if (newMovie.getTitle() != null) { + oldMovie.setTitle(newMovie.getTitle()); + } - private List convertScreeningDTOList(List screeningDTOList) { - List screenings = new ArrayList<>(); - screeningDTOList.forEach(s -> screenings.add(convertFromDTO(s))); - return screenings; - } + if (newMovie.getRating() != null) { + oldMovie.setRating(newMovie.getRating()); + } - private void update(Movie oldMovie, Movie newMovie) { - oldMovie.setTitle(newMovie.getTitle()); - oldMovie.setRating(newMovie.getRating()); - oldMovie.setDescription(newMovie.getDescription()); - oldMovie.setRuntimeMins(newMovie.getRuntimeMins()); - oldMovie.setUpdatedAt(OffsetDateTime.now()); - } + if (newMovie.getDescription() != null) { + oldMovie.setDescription(newMovie.getDescription()); + } - private Movie extractMovieFromMovieDTO(MovieDTO movieDTO){ - Movie movie = new Movie(); - movie.setTitle(movieDTO.getTitle()); - movie.setRating(movieDTO.getRating()); - movie.setDescription(movieDTO.getDescription()); - movie.setRuntimeMins(movieDTO.getRuntimeMins()); - return movie; - } + if (newMovie.getRuntimeMins() != null) { + oldMovie.setRuntimeMins(newMovie.getRuntimeMins()); + } - private void saveScreenings(List screenings){ - screenings.forEach(s -> this.screeningRepository.save(s)); + oldMovie.setUpdatedAt(OffsetDateTime.now()); } - private void addMovieToScreenings(List screenings, Movie movie) { - screenings.forEach(s -> s.setMovie(movie)); + private MovieResponseDTO convertToResponseDTO(Movie movie) { + return new MovieResponseDTO( + movie.getId(), + movie.getTitle(), + movie.getRating(), + movie.getDescription(), + movie.getRuntimeMins(), + movie.getCreatedAt(), + movie.getUpdatedAt()); } } diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java index a6a2f4e6..2d0f8d96 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java @@ -2,7 +2,10 @@ import com.booleanuk.api.cinema.screening.model.Screening; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -29,17 +32,23 @@ public Movie (String title, String rating, String description, int runtimeMins, @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + // Bean validation framework. Validates object fields at application level. + @NotBlank(message = "Title is required") @Column(name = "title", nullable = false) private String title; + @NotBlank(message = "Rating is required") @Column(name = "rating", nullable = false) private String rating; + @NotBlank(message = "Description is required") @Column(name = "description", nullable = false) private String description; + // Validation check for non-string fields. + @NotNull(message = "RuntimeMins is required") @Column(name = "runtimeMins", nullable = false) - private int runtimeMins; + private Integer runtimeMins; @Column(name = "createdAt", nullable = false) private OffsetDateTime createdAt; @@ -47,9 +56,8 @@ public Movie (String title, String rating, String description, int runtimeMins, @Column(name = "updatedAt", nullable = false) private OffsetDateTime updatedAt; - @JsonIgnore - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - /* Ignore this list in response */ + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference private List screenings; @PrePersist diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java deleted file mode 100644 index 132ee097..00000000 --- a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieDTO.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.booleanuk.api.cinema.movie.model; - -import com.booleanuk.api.cinema.screening.model.ScreeningDTO; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -@NoArgsConstructor - -public class MovieDTO { - /* - Another DTO to handle the messed up time format from the API spec for screenings. - */ - public MovieDTO (String title, String rating, String description, int runtimeMins, List screeningsDTO) { - this.title = title; - this.rating = rating; - this.description = description; - this.runtimeMins = runtimeMins; - this.screenings = screeningsDTO; - } - - private int id; - - private String title; - - private String rating; - - private String description; - - private int runtimeMins; - - private List screenings; -} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java new file mode 100644 index 00000000..657b0945 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieResponseDTO.java @@ -0,0 +1,38 @@ +package com.booleanuk.api.cinema.movie.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@NoArgsConstructor + +public class MovieResponseDTO { + + public MovieResponseDTO(int id, String title, String rating, String description, Integer runtimeMins, OffsetDateTime createdAt, OffsetDateTime updatedAt) { + this.id = id; + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + private int id; + + private String title; + + private String rating; + + private String description; + + private Integer runtimeMins; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java new file mode 100644 index 00000000..5067dd79 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java @@ -0,0 +1,38 @@ +package com.booleanuk.api.cinema.movie.model; + +import com.booleanuk.api.cinema.screening.model.Screening; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor + +public class MovieUpdateDTO { + + public MovieUpdateDTO(String title, String rating, String description, Integer runtimeMins, List screenings) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = screenings; + } + + private String title; + + private String rating; + + private String description; + + private Integer runtimeMins; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; + + private List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java b/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java index 416c099b..51864196 100644 --- a/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java +++ b/src/main/java/com/booleanuk/api/cinema/response/ErrorResponse.java @@ -3,7 +3,7 @@ import java.util.HashMap; import java.util.Map; -public class ErrorResponse implements ResponseInterface { +public class ErrorResponse implements Response { private final String status; private final Map data; diff --git a/src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java b/src/main/java/com/booleanuk/api/cinema/response/Response.java similarity index 71% rename from src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java rename to src/main/java/com/booleanuk/api/cinema/response/Response.java index a2d79701..41671b50 100644 --- a/src/main/java/com/booleanuk/api/cinema/response/ResponseInterface.java +++ b/src/main/java/com/booleanuk/api/cinema/response/Response.java @@ -1,6 +1,6 @@ package com.booleanuk.api.cinema.response; -public interface ResponseInterface { +public interface Response { String getStatus(); Object getData(); } diff --git a/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java b/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java index e87acf53..138319c4 100644 --- a/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java +++ b/src/main/java/com/booleanuk/api/cinema/response/ResponseFactory.java @@ -5,26 +5,32 @@ public class ResponseFactory { + + // Utility classes should not have a public constructor. + private ResponseFactory(){ + throw new IllegalStateException("utility class"); + } + // Static method to create an HttpStatus OK response - public static ResponseEntity OkSuccessResponse(T data) { + public static ResponseEntity okSuccessResponse(T data) { SuccessResponse successResponse = new SuccessResponse<>(data); return new ResponseEntity<>(successResponse, HttpStatus.OK); } // Static method to create an HttpStatus CREATED response - public static ResponseEntity CreatedSuccessResponse(T data) { + public static ResponseEntity createdSuccessResponse(T data) { SuccessResponse successResponse = new SuccessResponse<>(data); return new ResponseEntity<>(successResponse, HttpStatus.CREATED); } // Static method to create a Bad Request response - public static ResponseEntity BadRequestErrorResponse() { + public static ResponseEntity badRequestErrorResponse() { ErrorResponse errorResponse = new ErrorResponse("bad request"); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } // Static method to create a Not Found response - public static ResponseEntity NotFoundErrorResponse() { + public static ResponseEntity notFoundErrorResponse() { ErrorResponse errorResponse = new ErrorResponse("not found"); return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); } diff --git a/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java b/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java index 4d447f4f..847ac077 100644 --- a/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java +++ b/src/main/java/com/booleanuk/api/cinema/response/SuccessResponse.java @@ -5,7 +5,7 @@ @Setter @Getter -public class SuccessResponse implements ResponseInterface { +public class SuccessResponse implements Response { private String status; private T data; diff --git a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java index 02a1678f..f36ebf10 100644 --- a/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java +++ b/src/main/java/com/booleanuk/api/cinema/screening/model/Screening.java @@ -1,8 +1,10 @@ package com.booleanuk.api.cinema.screening.model; import com.booleanuk.api.cinema.movie.model.Movie; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -29,19 +31,17 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - @ManyToOne - @JsonIgnore - @JoinColumn(name = "movieId") - private Movie movie; - + @NotNull(message = "screenNumber is required") @Column(name = "screenNumber", nullable = false) - private int screenNumber; + private Integer screenNumber; + @NotNull(message = "startsAt is required") @Column(name = "startsAt", nullable = false) private OffsetDateTime startsAt; + @NotNull(message = "capacity is required") @Column(name = "capacity", nullable = false) - private int capacity; + private Integer capacity; @Column(name = "createdAt", nullable = false) private LocalDateTime createdAt; @@ -49,6 +49,11 @@ public Screening (int screenNumber, int capacity, OffsetDateTime startsAt) { @Column(name = "updatedAt", nullable = false) private LocalDateTime updatedAt; + @ManyToOne + @JoinColumn(name = "movieId", nullable = false) + @JsonBackReference + private Movie movie; + @PrePersist private void onCreate() { /* diff --git a/src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java b/src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java deleted file mode 100644 index 7e302adc..00000000 --- a/src/main/java/com/booleanuk/api/cinema/screening/model/ScreeningDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.booleanuk.api.cinema.screening.model; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor - -public class ScreeningDTO { - /* - DTO to accept time format specified in api spec. - Used as a middle man to convert to the desired time format when storing in database. - */ - - public ScreeningDTO (int screenNumber, int capacity, String startsAt) { - this.screenNumber = screenNumber; - this.capacity = capacity; - this.startsAt = startsAt; - } - - private int screenNumber; - - private String startsAt; - - private int capacity; - -} From 4976c32eb9404a5e3e7916b070b3aedc5ae345d6 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Fri, 13 Sep 2024 13:39:33 +0200 Subject: [PATCH 22/23] Refactored customer and ticket. --- .../controller/CustomerController.java | 138 ++++++++++-------- .../api/cinema/customer/model/Customer.java | 13 ++ .../customer/model/CustomerResponseDTO.java | 35 +++++ .../api/cinema/ticket/model/Ticket.java | 23 +-- 4 files changed, 137 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java diff --git a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java index 6cbb2dbb..26796a05 100644 --- a/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/controller/CustomerController.java @@ -1,17 +1,24 @@ package com.booleanuk.api.cinema.customer.controller; import com.booleanuk.api.cinema.customer.model.Customer; +import com.booleanuk.api.cinema.customer.model.CustomerResponseDTO; import com.booleanuk.api.cinema.customer.repository.CustomerRepository; -import com.booleanuk.api.cinema.response.ResponseInterface; +import com.booleanuk.api.cinema.response.Response; +import com.booleanuk.api.cinema.response.ResponseFactory; import com.booleanuk.api.cinema.screening.model.Screening; import com.booleanuk.api.cinema.screening.repository.ScreeningRepository; import com.booleanuk.api.cinema.ticket.model.Ticket; import com.booleanuk.api.cinema.ticket.repository.TicketRepository; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import static com.booleanuk.api.cinema.response.ResponseFactory.*; @@ -28,116 +35,123 @@ public class CustomerController { @Autowired ScreeningRepository screeningRepository; - @PostMapping - public ResponseEntity addCustomer(@RequestBody Customer customer) { - try { - Customer newCustomer = this.customerRepository.save(customer); - return CreatedSuccessResponse(newCustomer); + // Workaround for exception 415 + @PostMapping(consumes = {"application/json", "application/json;charset=UTF-8"}) + public ResponseEntity addCustomer(@Valid @RequestBody Customer customer, BindingResult result) { - } catch (Exception e) { - return BadRequestErrorResponse(); + if (result.hasErrors()) { + return badRequestErrorResponse(); } + + Customer savedCustomer = this.customerRepository.save(customer); + CustomerResponseDTO response = convertToCustomerResponseDTO(savedCustomer); + return createdSuccessResponse(response); } @GetMapping - public ResponseEntity getAllCustomers() { - return OkSuccessResponse(this.customerRepository.findAll()); + public ResponseEntity getAllCustomers() { + List response = new ArrayList<>(); + this.customerRepository.findAll().forEach(customer -> + response.add(convertToCustomerResponseDTO(customer)) + ); + return okSuccessResponse(response); } @GetMapping("/{id}") - public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { - if (this.customerRepository.findById(id).isEmpty()) { - return NotFoundErrorResponse(); - } - return OkSuccessResponse(this.customerRepository.findById(id).get()); + public ResponseEntity getCustomerById(@PathVariable (name = "id") int id) { + return this.customerRepository.findById(id). + map(customer -> { + CustomerResponseDTO response = convertToCustomerResponseDTO(customer); + return okSuccessResponse(response); + }) + .orElseGet(ResponseFactory::notFoundErrorResponse); } @PutMapping("/{id}") - public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @RequestBody Customer customer) { + public ResponseEntity updateCustomer(@PathVariable (name = "id") int id, @Valid @RequestBody Customer updatedCustomer, BindingResult result) { - if (this.customerRepository.findById(id).isEmpty()) { - return NotFoundErrorResponse(); + if (result.hasErrors()) { + return badRequestErrorResponse(); } - try { - Customer customerToUpdate = this.customerRepository.findById(id).get(); - update(customerToUpdate, customer); - Customer updatedCustomer = this.customerRepository.save(customerToUpdate); - - return CreatedSuccessResponse(updatedCustomer); - - } catch (Exception e) { - return BadRequestErrorResponse(); - } + return this.customerRepository.findById(id).map(customerToUpdate -> { + updateCustomerDetails(customerToUpdate, updatedCustomer); + Customer savedCustomer = this.customerRepository.save(customerToUpdate); + CustomerResponseDTO response = convertToCustomerResponseDTO(savedCustomer); + return createdSuccessResponse(response); + }).orElseGet(ResponseFactory::notFoundErrorResponse); } @DeleteMapping("/{id}") - public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ - if (this.customerRepository.findById(id).isEmpty()) { - return NotFoundErrorResponse(); - } - - try { - Customer customerToDelete = this.customerRepository.findById(id).get(); + public ResponseEntity deleteCustomer(@PathVariable (name = "id") int id){ + return this.customerRepository.findById(id).map(customerToDelete -> { this.customerRepository.delete(customerToDelete); - return OkSuccessResponse(customerToDelete); - } catch (Exception e) { - return BadRequestErrorResponse(); - } + return okSuccessResponse(customerToDelete); + }).orElseGet(ResponseFactory::notFoundErrorResponse); } /* Tickets */ - @PostMapping("/{customerId}/screenings/{screeningId}") - public ResponseEntity bookTicket(@PathVariable (name = "customerId") int customerId, - @PathVariable (name = "screeningId") int screeningId, - @RequestBody Ticket ticket) { + public ResponseEntity bookTicket(@PathVariable (name = "customerId") int customerId, + @PathVariable (name = "screeningId") int screeningId, + @Valid @RequestBody Ticket ticket, BindingResult result) { - if (this.customerRepository.findById(customerId).isEmpty()) { - return NotFoundErrorResponse(); + if (result.hasErrors()) { + return badRequestErrorResponse(); } - if (this.screeningRepository.findById(screeningId).isEmpty()) { - return NotFoundErrorResponse(); + Optional optionalCustomer = this.customerRepository.findById(customerId); + if (optionalCustomer.isEmpty()){ + return notFoundErrorResponse(); } - Customer customer = this.customerRepository.findById(customerId).get(); - Screening screening = this.screeningRepository.findById(screeningId).get(); - - try { - ticket.setCustomer(customer); - ticket.setScreening(screening); - return CreatedSuccessResponse(ticketRepository.save(ticket)); - } catch (Exception e) { - return NotFoundErrorResponse(); + Optional optionalScreening = this.screeningRepository.findById(screeningId); + if (optionalScreening.isEmpty()){ + return notFoundErrorResponse(); } + + Customer customer = optionalCustomer.get(); + Screening screening = optionalScreening.get(); + + ticket.setCustomer(customer); + ticket.setScreening(screening); + + Ticket savedTicket = this.ticketRepository.save(ticket); + return createdSuccessResponse(savedTicket); + } @GetMapping("/{customerId}/screenings/{screeningId}") - public ResponseEntity getAllTickets(@PathVariable (name = "customerId") int customerId, - @PathVariable (name = "screeningId") int screeningId) { + public ResponseEntity getAllTickets(@PathVariable (name = "customerId") int customerId, + @PathVariable (name = "screeningId") int screeningId) { if (this.customerRepository.findById(customerId).isEmpty()) { - return NotFoundErrorResponse(); + return notFoundErrorResponse(); } if (this.screeningRepository.findById(screeningId).isEmpty()) { - return NotFoundErrorResponse(); + return notFoundErrorResponse(); } Customer customer = this.customerRepository.findById(customerId).get(); Screening screening = this.screeningRepository.findById(screeningId).get(); - return OkSuccessResponse(this.ticketRepository.findAllByCustomerAndScreening(customer, screening)); + List ticketList = this.ticketRepository.findAllByCustomerAndScreening(customer, screening); + + return okSuccessResponse(ticketList); } - private void update(Customer oldCustomer, Customer newCustomer) { + private void updateCustomerDetails(Customer oldCustomer, Customer newCustomer) { oldCustomer.setName(newCustomer.getName()); oldCustomer.setPhone(newCustomer.getPhone()); oldCustomer.setEmail(newCustomer.getEmail()); oldCustomer.setUpdatedAt(OffsetDateTime.now()); } + private CustomerResponseDTO convertToCustomerResponseDTO(Customer customer){ + return new CustomerResponseDTO(customer.getId(), customer.getName(), customer.getEmail(), customer.getPhone(), customer.getCreatedAt(), customer.getUpdatedAt()); + } + } diff --git a/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java b/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java index 2a92c09e..1fdfee2b 100644 --- a/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java +++ b/src/main/java/com/booleanuk/api/cinema/customer/model/Customer.java @@ -1,11 +1,16 @@ package com.booleanuk.api.cinema.customer.model; +import com.booleanuk.api.cinema.ticket.model.Ticket; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; @Getter @Setter @@ -19,18 +24,22 @@ public Customer (String name, String email, String phone) { this.name = name; this.email = email; this.phone = phone; + this.tickets = new ArrayList<>(); } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; + @NotBlank(message = "name is required") @Column(name = "name", nullable = false) private String name; + @NotBlank(message = "email is required") @Column(name = "email", nullable = false) private String email; + @NotBlank(message = "phone is required") @Column(name = "phone", nullable = false) private String phone; @@ -40,6 +49,10 @@ public Customer (String name, String email, String phone) { @Column(name = "updatedAt", nullable = false) private OffsetDateTime updatedAt; + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference(value = "customer-tickets") + List tickets; + @PrePersist private void onCreate() { /* diff --git a/src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java b/src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java new file mode 100644 index 00000000..bf57fc83 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/customer/model/CustomerResponseDTO.java @@ -0,0 +1,35 @@ +package com.booleanuk.api.cinema.customer.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.OffsetDateTime; + +@Getter +@Setter +@NoArgsConstructor + +public class CustomerResponseDTO { + + public CustomerResponseDTO (int id, String name, String email, String phone, OffsetDateTime createdAt, OffsetDateTime updatedAt) { + this.id = id; + this.name = name; + this.email = email; + this.phone = phone; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + private int id; + + private String name; + + private String email; + + private String phone; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java index 625fc911..454046c9 100644 --- a/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java +++ b/src/main/java/com/booleanuk/api/cinema/ticket/model/Ticket.java @@ -2,8 +2,10 @@ import com.booleanuk.api.cinema.customer.model.Customer; import com.booleanuk.api.cinema.screening.model.Screening; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.*; import java.time.LocalDateTime; @@ -20,16 +22,7 @@ public class Ticket { @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - @JsonIgnore - @ManyToOne - @JoinColumn(name = "customerId", nullable = false) - private Customer customer; - - @JsonIgnore - @ManyToOne - @JoinColumn(name = "screeningId", nullable = false) - private Screening screening; - + @NotNull(message = "numberOfSeats is required") @Column(name = "numberOfSeats") private int numberOfSeats; @@ -39,6 +32,16 @@ public class Ticket { @Column(name = "updatedAt") private LocalDateTime updatedAt; + @ManyToOne + @JoinColumn(name = "customerId", nullable = false) + @JsonBackReference(value = "customer-tickets") + private Customer customer; + + @ManyToOne + @JoinColumn(name = "screeningId", nullable = false) + @JsonBackReference(value = "movie tickets") + private Screening screening; + @PrePersist private void onCreate() { /* From 9baa322f573ed97964456cf9a67f7e7ad87cbe44 Mon Sep 17 00:00:00 2001 From: Thomas Wiik Date: Fri, 13 Sep 2024 15:31:26 +0200 Subject: [PATCH 23/23] Implemented DTO for movierequest to decouple request object from database object. --- build.gradle | 3 ++ .../api/cinema/mapper/MovieMapper.java | 19 +++++++ .../movie/controller/MovieController.java | 22 +++++--- .../api/cinema/movie/model/Movie.java | 8 +-- .../cinema/movie/model/MovieRequestDTO.java | 51 +++++++++++++++++++ .../cinema/movie/model/MovieUpdateDTO.java | 16 +----- 6 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java create mode 100644 src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java diff --git a/build.gradle b/build.gradle index 3550f099..66ca4b78 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,9 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' // To use @Valid implementation 'org.springframework.boot:spring-boot-starter-validation' + // MapStruct for mapping DTOs + implementation("org.mapstruct:mapstruct:1.5.3.Final") + annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final") } diff --git a/src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java b/src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java new file mode 100644 index 00000000..2922ad32 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/mapper/MovieMapper.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.cinema.mapper; + +import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.movie.model.MovieRequestDTO; +import com.booleanuk.api.cinema.movie.model.MovieResponseDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface MovieMapper { + + // Ignored the targets when mapping. + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Movie toEntity(MovieRequestDTO requestDTO); + + MovieResponseDTO toResponseDTO(Movie movie); +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java index 2adf3016..28bb38d1 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/controller/MovieController.java @@ -1,6 +1,8 @@ package com.booleanuk.api.cinema.movie.controller; +import com.booleanuk.api.cinema.mapper.MovieMapper; import com.booleanuk.api.cinema.movie.model.Movie; +import com.booleanuk.api.cinema.movie.model.MovieRequestDTO; import com.booleanuk.api.cinema.movie.model.MovieResponseDTO; import com.booleanuk.api.cinema.movie.model.MovieUpdateDTO; import com.booleanuk.api.cinema.movie.repository.MovieRepository; @@ -30,12 +32,17 @@ public class MovieController { @Autowired ScreeningRepository screeningRepository; + @Autowired + MovieMapper movieMapper; + @PostMapping - public ResponseEntity addMovie(@Valid @RequestBody Movie movie, BindingResult result) { + public ResponseEntity addMovie(@Valid @RequestBody MovieRequestDTO movieRequestDTO, BindingResult result) { if (result.hasErrors()){ return badRequestErrorResponse(); } + Movie movie = movieMapper.toEntity(movieRequestDTO); + // Set movie to each screening. if (movie.getScreenings() != null) { movie.getScreenings().forEach(s -> s.setMovie(movie)); @@ -45,7 +52,7 @@ public ResponseEntity addMovie(@Valid @RequestBody Movie movie, Bindin Movie savedMovie = this.movieRepository.save(movie); // Convert to DTO without screenings - MovieResponseDTO responseDTO = convertToResponseDTO(savedMovie); + MovieResponseDTO responseDTO = movieMapper.toResponseDTO(savedMovie); return createdSuccessResponse(responseDTO); } @@ -54,26 +61,25 @@ public ResponseEntity addMovie(@Valid @RequestBody Movie movie, Bindin @GetMapping public ResponseEntity getAllMovies() { List responseList = new ArrayList<>(); - this.movieRepository.findAll().forEach(movie -> responseList.add(convertToResponseDTO(movie))); + this.movieRepository.findAll().forEach(movie -> responseList.add(movieMapper.toResponseDTO(movie))); return okSuccessResponse(responseList); } @PutMapping("/{id}") public ResponseEntity updateMovie(@PathVariable (name = "id") int id, - @Valid @RequestBody MovieUpdateDTO updatedMovie, + @Valid @RequestBody MovieUpdateDTO updatedMovieDTO, BindingResult result) { if (result.hasErrors()) { return badRequestErrorResponse(); - } return this.movieRepository.findById(id) .map(movieToUpdate -> { - updateMovieDetails(movieToUpdate, updatedMovie); + updateMovieDetails(movieToUpdate, updatedMovieDTO); Movie savedMovie = this.movieRepository.save(movieToUpdate); - MovieResponseDTO responseDTO = convertToResponseDTO(savedMovie); + MovieResponseDTO responseDTO = movieMapper.toResponseDTO(savedMovie); return createdSuccessResponse(responseDTO); }).orElseGet(ResponseFactory::notFoundErrorResponse); } @@ -83,7 +89,7 @@ public ResponseEntity deleteMovie(@PathVariable (name = "id") int id) return this.movieRepository.findById(id) .map(movie -> { this.movieRepository.delete(movie); - MovieResponseDTO responseDTO = convertToResponseDTO(movie); + MovieResponseDTO responseDTO = movieMapper.toResponseDTO(movie); return okSuccessResponse(responseDTO); }) .orElseGet(ResponseFactory::notFoundErrorResponse); diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java index 2d0f8d96..35147ff0 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/Movie.java @@ -20,7 +20,7 @@ @Entity @Table(name = "movies") public class Movie { - public Movie (String title, String rating, String description, int runtimeMins, List screenings) { + public Movie (String title, String rating, String description, Integer runtimeMins, List screenings) { this.title = title; this.rating = rating; this.description = description; @@ -32,21 +32,15 @@ public Movie (String title, String rating, String description, int runtimeMins, @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; - // Bean validation framework. Validates object fields at application level. - @NotBlank(message = "Title is required") @Column(name = "title", nullable = false) private String title; - @NotBlank(message = "Rating is required") @Column(name = "rating", nullable = false) private String rating; - @NotBlank(message = "Description is required") @Column(name = "description", nullable = false) private String description; - // Validation check for non-string fields. - @NotNull(message = "RuntimeMins is required") @Column(name = "runtimeMins", nullable = false) private Integer runtimeMins; diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java new file mode 100644 index 00000000..12983a09 --- /dev/null +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieRequestDTO.java @@ -0,0 +1,51 @@ +package com.booleanuk.api.cinema.movie.model; + +import com.booleanuk.api.cinema.screening.model.Screening; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor + +public class MovieRequestDTO { + + public MovieRequestDTO(String title, String rating, String description, Integer runtimeMins) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = new ArrayList<>(); + } + + public MovieRequestDTO(String title, String rating, String description, Integer runtimeMins, List screenings) { + this.title = title; + this.rating = rating; + this.description = description; + this.runtimeMins = runtimeMins; + this.screenings = screenings; + } + + // Bean validation framework. Validates object fields at application level. + @NotBlank(message = "Title is required") + private String title; + + @NotBlank(message = "Rating is required") + private String rating; + + @NotBlank(message = "Description is required") + private String description; + + // Validation check for non-string fields. + @NotNull(message = "RuntimeMins is required") + private Integer runtimeMins; + + // Optional + private List screenings; +} diff --git a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java index 5067dd79..b2ca2ad5 100644 --- a/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java +++ b/src/main/java/com/booleanuk/api/cinema/movie/model/MovieUpdateDTO.java @@ -1,27 +1,15 @@ package com.booleanuk.api.cinema.movie.model; import com.booleanuk.api.cinema.screening.model.Screening; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.Data; import java.time.OffsetDateTime; import java.util.List; -@Getter -@Setter -@NoArgsConstructor +@Data public class MovieUpdateDTO { - public MovieUpdateDTO(String title, String rating, String description, Integer runtimeMins, List screenings) { - this.title = title; - this.rating = rating; - this.description = description; - this.runtimeMins = runtimeMins; - this.screenings = screenings; - } - private String title; private String rating;