From f1f208644d26eeafbe9b31ce2d84fe8865315706 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:08:03 +0000 Subject: [PATCH 01/42] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5150e50f..05def9a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/_uV8Mn8f) # 📘 Projektarbete: JPA + Hibernate med GitHub-flöde Projektet genomförs som antingen en Java CLI-applikation eller med hjĂ€lp av JavaFX om ni vill ha ett grafiskt grĂ€nssnitt. From 8520f8627dc47c18022d2b864cf19ed97ade644b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Tue, 16 Dec 2025 10:15:55 +0100 Subject: [PATCH 02/42] Update pom.xml with dependencies --- src/main/java/org/example/App.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index 165e5cd5..843d22ac 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -1,7 +1,5 @@ package org.example; public class App { - public static void main(String[] args) { - System.out.println("Hello There!"); - } + } From 55e019d32433eecf53c2970fa86d4a00c46b8fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Tue, 16 Dec 2025 10:16:20 +0100 Subject: [PATCH 03/42] Update pom.xml with dependencies. --- pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index 909503d0..bea97bcf 100644 --- a/pom.xml +++ b/pom.xml @@ -50,5 +50,20 @@ classgraph 4.8.184 + + jakarta.persistence + jakarta.persistence-api + 3.2.0 + + + org.slf4j + slf4j-api + 2.0.13 + + + org.openjfx + javafx-media + 25.0.1 + From d1aaae66df4e4226a99de78b91266152f582a780 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Tue, 16 Dec 2025 22:11:50 +0100 Subject: [PATCH 04/42] First version docker-compose --- docker-compose.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..a85f75e2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.8" + +services: + mysql: + image: mysql:8.3 + container_name: movieapp_mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: moviedb + MYSQL_USER: movieuser + MYSQL_PASSWORD: moviepass + ports: + - "3306:3306" + command: --default-authentication-plugin=mysql_native_password + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: From 3b2957dbe4a21549dd439a6081e2e0ab50b410bc Mon Sep 17 00:00:00 2001 From: Tatjana Date: Tue, 16 Dec 2025 22:36:49 +0100 Subject: [PATCH 05/42] Changes after coderabbit review --- docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a85f75e2..bf94e1be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,7 @@ services: MYSQL_USER: movieuser MYSQL_PASSWORD: moviepass ports: - - "3306:3306" - command: --default-authentication-plugin=mysql_native_password + - "127.0.0.1:3306:3306" volumes: - mysql_data:/var/lib/mysql From 1787dbe75454364997a7206e3bfdd75b4743918b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Wed, 17 Dec 2025 12:00:45 +0100 Subject: [PATCH 06/42] Update skeleton, create persistence.xml and start with entities. --- src/main/java/org/example/API/TmdbClient.java | 4 ++ src/main/java/org/example/entity/Movie.java | 71 +++++++++++++++++++ src/main/java/org/example/entity/Person.java | 7 ++ src/main/java/org/example/entity/Role.java | 7 ++ .../example/repository/MovieRepository.java | 4 ++ .../repository/MovieRepositoryImp.java | 4 ++ .../org/example/service/MovieServices.java | 4 ++ src/main/java/org/example/ui/MainApp.java | 4 ++ src/main/resources/META-INF/persistence.xml | 37 ++++++++++ 9 files changed, 142 insertions(+) create mode 100644 src/main/java/org/example/API/TmdbClient.java create mode 100644 src/main/java/org/example/entity/Movie.java create mode 100644 src/main/java/org/example/entity/Person.java create mode 100644 src/main/java/org/example/entity/Role.java create mode 100644 src/main/java/org/example/repository/MovieRepository.java create mode 100644 src/main/java/org/example/repository/MovieRepositoryImp.java create mode 100644 src/main/java/org/example/service/MovieServices.java create mode 100644 src/main/java/org/example/ui/MainApp.java create mode 100644 src/main/resources/META-INF/persistence.xml diff --git a/src/main/java/org/example/API/TmdbClient.java b/src/main/java/org/example/API/TmdbClient.java new file mode 100644 index 00000000..a68fa461 --- /dev/null +++ b/src/main/java/org/example/API/TmdbClient.java @@ -0,0 +1,4 @@ +package org.example.API; + +public class TmdbClient { +} diff --git a/src/main/java/org/example/entity/Movie.java b/src/main/java/org/example/entity/Movie.java new file mode 100644 index 00000000..b7b7ec79 --- /dev/null +++ b/src/main/java/org/example/entity/Movie.java @@ -0,0 +1,71 @@ +package org.example.entity; + +import jakarta.persistence.*; + + + // Skapar tabell över filmer med information sĂ„som id (PK), title, genre, releaseYear, rating, shortDesc, longDesc, imageUrl, imdbUrl + + @Entity + @Table(name = "movie") + public class Movie { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + private String genre; + private int releaseYear; + private double rating; + + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public int getReleaseYear() { + return releaseYear; + } + + public void setReleaseYear(int releaseYear) { + this.releaseYear = releaseYear; + } + + public double getRating() { + return rating; + } + + public void setRating(double rating) { + this.rating = rating; + } + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + + @Override + public String toString() { + return "Movie{" + + "id=" + id + + ", title='" + title + '\'' + + ", genre='" + genre + '\'' + + ", releaseYear=" + releaseYear + + ", rating=" + rating + + '}'; + } + } diff --git a/src/main/java/org/example/entity/Person.java b/src/main/java/org/example/entity/Person.java new file mode 100644 index 00000000..b1a5d0e1 --- /dev/null +++ b/src/main/java/org/example/entity/Person.java @@ -0,0 +1,7 @@ +package org.example.entity; + +public class Person { + +// Skapar tabell med info om person sĂ„som name, birthDate, id (PK), imageUrl + +} diff --git a/src/main/java/org/example/entity/Role.java b/src/main/java/org/example/entity/Role.java new file mode 100644 index 00000000..22c030d9 --- /dev/null +++ b/src/main/java/org/example/entity/Role.java @@ -0,0 +1,7 @@ +package org.example.entity; + +public class Role { + + // Skapar tabeller av personer, med roller tilldelade sĂ„som actor eller director + // id (PK), roleType, creditOrder, movie_id (FK), person_id (FK) +} diff --git a/src/main/java/org/example/repository/MovieRepository.java b/src/main/java/org/example/repository/MovieRepository.java new file mode 100644 index 00000000..bb104cbc --- /dev/null +++ b/src/main/java/org/example/repository/MovieRepository.java @@ -0,0 +1,4 @@ +package org.example.repository; + +public class MovieRepository { +} diff --git a/src/main/java/org/example/repository/MovieRepositoryImp.java b/src/main/java/org/example/repository/MovieRepositoryImp.java new file mode 100644 index 00000000..718c8b1e --- /dev/null +++ b/src/main/java/org/example/repository/MovieRepositoryImp.java @@ -0,0 +1,4 @@ +package org.example.repository; + +public class MovieRepositoryImp { +} diff --git a/src/main/java/org/example/service/MovieServices.java b/src/main/java/org/example/service/MovieServices.java new file mode 100644 index 00000000..199295a4 --- /dev/null +++ b/src/main/java/org/example/service/MovieServices.java @@ -0,0 +1,4 @@ +package org.example.service; + +public class MovieServices { +} diff --git a/src/main/java/org/example/ui/MainApp.java b/src/main/java/org/example/ui/MainApp.java new file mode 100644 index 00000000..74017841 --- /dev/null +++ b/src/main/java/org/example/ui/MainApp.java @@ -0,0 +1,4 @@ +package org.example.ui; + +public class MainApp { +} diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000..d469a675 --- /dev/null +++ b/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,37 @@ + + + + + + org.hibernate.jpa.HibernatePersistenceProvider + + + + + + + + + + + + + + + + + + + + From 8ff13d23233d1340c018625f6c37bec02cb483b8 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 15:37:58 +0100 Subject: [PATCH 07/42] version 1 entity --- src/main/java/org/example/entity/Movie.java | 71 ------------------- src/main/java/org/example/entity/Person.java | 7 -- src/main/java/org/example/entity/Role.java | 7 -- .../java/org/example/movie/entity/Movie.java | 46 ++++++++++++ .../java/org/example/movie/entity/Person.java | 49 +++++++++++++ .../java/org/example/movie/entity/Role.java | 65 +++++++++++++++++ .../org/example/movie/entity/RoleType.java | 6 ++ 7 files changed, 166 insertions(+), 85 deletions(-) delete mode 100644 src/main/java/org/example/entity/Movie.java delete mode 100644 src/main/java/org/example/entity/Person.java delete mode 100644 src/main/java/org/example/entity/Role.java create mode 100644 src/main/java/org/example/movie/entity/Movie.java create mode 100644 src/main/java/org/example/movie/entity/Person.java create mode 100644 src/main/java/org/example/movie/entity/Role.java create mode 100644 src/main/java/org/example/movie/entity/RoleType.java diff --git a/src/main/java/org/example/entity/Movie.java b/src/main/java/org/example/entity/Movie.java deleted file mode 100644 index b7b7ec79..00000000 --- a/src/main/java/org/example/entity/Movie.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.example.entity; - -import jakarta.persistence.*; - - - // Skapar tabell över filmer med information sĂ„som id (PK), title, genre, releaseYear, rating, shortDesc, longDesc, imageUrl, imdbUrl - - @Entity - @Table(name = "movie") - public class Movie { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - private String genre; - private int releaseYear; - private double rating; - - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getGenre() { - return genre; - } - - public void setGenre(String genre) { - this.genre = genre; - } - - public int getReleaseYear() { - return releaseYear; - } - - public void setReleaseYear(int releaseYear) { - this.releaseYear = releaseYear; - } - - public double getRating() { - return rating; - } - - public void setRating(double rating) { - this.rating = rating; - } - - public Long getId() { - return id; - } - public void setId(Long id) { - this.id = id; - } - - @Override - public String toString() { - return "Movie{" + - "id=" + id + - ", title='" + title + '\'' + - ", genre='" + genre + '\'' + - ", releaseYear=" + releaseYear + - ", rating=" + rating + - '}'; - } - } diff --git a/src/main/java/org/example/entity/Person.java b/src/main/java/org/example/entity/Person.java deleted file mode 100644 index b1a5d0e1..00000000 --- a/src/main/java/org/example/entity/Person.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.entity; - -public class Person { - -// Skapar tabell med info om person sĂ„som name, birthDate, id (PK), imageUrl - -} diff --git a/src/main/java/org/example/entity/Role.java b/src/main/java/org/example/entity/Role.java deleted file mode 100644 index 22c030d9..00000000 --- a/src/main/java/org/example/entity/Role.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.entity; - -public class Role { - - // Skapar tabeller av personer, med roller tilldelade sĂ„som actor eller director - // id (PK), roleType, creditOrder, movie_id (FK), person_id (FK) -} diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java new file mode 100644 index 00000000..205df0ca --- /dev/null +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -0,0 +1,46 @@ +package org.example.movie.entity; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "movies") +public class Movie { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + private Integer releaseYear; + private Double imdbRating; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) + private List roles = new ArrayList<>(); + + public Movie() {} + + public Movie(String title) { + this.title = title; + } + + public Long getId() { return id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public Integer getReleaseYear() { return releaseYear; } + public void setReleaseYear(Integer releaseYear) { + this.releaseYear = releaseYear; + } + + public Double getImdbRating() { return imdbRating; } + public void setImdbRating(Double imdbRating) { + this.imdbRating = imdbRating; + } + + public List getRoles() { return roles; } +} diff --git a/src/main/java/org/example/movie/entity/Person.java b/src/main/java/org/example/movie/entity/Person.java new file mode 100644 index 00000000..31bf57b9 --- /dev/null +++ b/src/main/java/org/example/movie/entity/Person.java @@ -0,0 +1,49 @@ +package org.example.movie.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "persons") +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + private LocalDate birthDate; + private String imageUrl; + + @OneToMany(mappedBy = "person", cascade = CascadeType.ALL) + private List roles = new ArrayList<>(); + + // ===== Constructors ===== + public Person() {} + + public Person(String name) { + this.name = name; + } + + // ===== Getters & Setters ===== + public Long getId() { return id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public LocalDate getBirthDate() { return birthDate; } + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public String getImageUrl() { return imageUrl; } + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public List getRoles() { return roles; } +} diff --git a/src/main/java/org/example/movie/entity/Role.java b/src/main/java/org/example/movie/entity/Role.java new file mode 100644 index 00000000..f31f7a2a --- /dev/null +++ b/src/main/java/org/example/movie/entity/Role.java @@ -0,0 +1,65 @@ +package org.example.movie.entity; + +ppackage org.example.movie.entity; + +import jakarta.persistence.*; +import org.example.movie.entity.Movie; +import org.example.movie.entity.Person; +import org.example.movie.entity.RoleType; + +@Entity +@Table(name = "roles") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RoleType roleType; + + /** + * Used for ACTOR roles (0 = main actor, 1 = second, 2 = third) + * Can be null for DIRECTOR + */ + private Integer creditOrder; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "movie_id", nullable = false) + private Movie movie; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "person_id", nullable = false) + private Person person; + + public Role() {} + + public Role(RoleType roleType, Movie movie, Person person) { + this.roleType = roleType; + this.movie = movie; + this.person = person; + } + + public Long getId() { return id; } + + public RoleType getRoleType() { return roleType; } + public void setRoleType(RoleType roleType) { + this.roleType = roleType; + } + + public Integer getCreditOrder() { return creditOrder; } + public void setCreditOrder(Integer creditOrder) { + this.creditOrder = creditOrder; + } + + public Movie getMovie() { return movie; } + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Person getPerson() { return person; } + public void setPerson(Person person) { + this.person = person; + } +} diff --git a/src/main/java/org/example/movie/entity/RoleType.java b/src/main/java/org/example/movie/entity/RoleType.java new file mode 100644 index 00000000..f16d0769 --- /dev/null +++ b/src/main/java/org/example/movie/entity/RoleType.java @@ -0,0 +1,6 @@ +package org.example.movie.entity; + +public enum RoleType { + ACTOR, + DIRECTOR +} From d01637dfc5da028ba8d867f3932b16d23e940ab2 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 15:45:27 +0100 Subject: [PATCH 08/42] version 1 entity --- src/main/java/org/example/movie/entity/Role.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/movie/entity/Role.java b/src/main/java/org/example/movie/entity/Role.java index f31f7a2a..29820534 100644 --- a/src/main/java/org/example/movie/entity/Role.java +++ b/src/main/java/org/example/movie/entity/Role.java @@ -1,6 +1,6 @@ package org.example.movie.entity; -ppackage org.example.movie.entity; +package org.example.movie.entity; import jakarta.persistence.*; import org.example.movie.entity.Movie; From 8252c5ba819aaa2085a081370a8fa8d7b6c7108e Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 15:51:35 +0100 Subject: [PATCH 09/42] version 3 entity --- src/main/java/org/example/movie/entity/Role.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/example/movie/entity/Role.java b/src/main/java/org/example/movie/entity/Role.java index 29820534..f80cc4d6 100644 --- a/src/main/java/org/example/movie/entity/Role.java +++ b/src/main/java/org/example/movie/entity/Role.java @@ -1,7 +1,5 @@ package org.example.movie.entity; -package org.example.movie.entity; - import jakarta.persistence.*; import org.example.movie.entity.Movie; import org.example.movie.entity.Person; From ab21961e6dd3a2c1092dba402e6c28c9e19f2955 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 16:44:11 +0100 Subject: [PATCH 10/42] version 3 entity --- src/main/java/org/example/movie/entity/Movie.java | 2 +- src/main/java/org/example/movie/entity/Person.java | 2 -- src/main/java/org/example/movie/entity/Role.java | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java index 205df0ca..c1491ddf 100644 --- a/src/main/java/org/example/movie/entity/Movie.java +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -21,7 +21,7 @@ public class Movie { @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) private List roles = new ArrayList<>(); - public Movie() {} + protected Movie() {} public Movie(String title) { this.title = title; diff --git a/src/main/java/org/example/movie/entity/Person.java b/src/main/java/org/example/movie/entity/Person.java index 31bf57b9..6dfda6fc 100644 --- a/src/main/java/org/example/movie/entity/Person.java +++ b/src/main/java/org/example/movie/entity/Person.java @@ -22,14 +22,12 @@ public class Person { @OneToMany(mappedBy = "person", cascade = CascadeType.ALL) private List roles = new ArrayList<>(); - // ===== Constructors ===== public Person() {} public Person(String name) { this.name = name; } - // ===== Getters & Setters ===== public Long getId() { return id; } public String getName() { return name; } diff --git a/src/main/java/org/example/movie/entity/Role.java b/src/main/java/org/example/movie/entity/Role.java index f80cc4d6..0caa359d 100644 --- a/src/main/java/org/example/movie/entity/Role.java +++ b/src/main/java/org/example/movie/entity/Role.java @@ -1,9 +1,6 @@ package org.example.movie.entity; import jakarta.persistence.*; -import org.example.movie.entity.Movie; -import org.example.movie.entity.Person; -import org.example.movie.entity.RoleType; @Entity @Table(name = "roles") From 2730c42e6b9e408aae26119abbd79f5932351326 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 16:52:17 +0100 Subject: [PATCH 11/42] Updates with entity t Persistence --- src/main/resources/META-INF/persistence.xml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index d469a675..558617d4 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -10,8 +10,17 @@ org.hibernate.jpa.HibernatePersistenceProvider + + org.example.movie.entity.Movie + org.example.movie.entity.Person + org.example.movie.entity.Role + org.example.movie.entity.RoleType + + false + + + - + + + + + + From c86c7ed71283cafcf38c0b4f8da23f7bac34763b Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 17:17:11 +0100 Subject: [PATCH 12/42] Changes persistence file to point to the corect db --- README.md | 10 +++++----- src/main/java/org/example/App.java | 9 +++++++++ src/main/resources/META-INF/persistence.xml | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 05def9a9..502db251 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ Kommunikation med databasen ska ske med JPA och Hibernate, enligt code first-met ## đŸ—“ïž Veckoplanering med Checklista ### ✅ Vecka 1 – GrundlĂ€ggning och struktur -- [ ] Klona GitHub-repo -- [ ] Konfigurera persistence.xml eller anvĂ€nd PersistenceConfiguration i kod -- [ ] Skapa entiteter och verifiera tabellgenerering -- [ ] LĂ€gg till relationer (One-to-Many, Many-to-Many) -- [ ] Arbeta pĂ„ feature-branches och anvĂ€nd pull requests för kodgranskning +- [ x ] Klona GitHub-repo +- [ x ] Konfigurera persistence.xml eller anvĂ€nd PersistenceConfiguration i kod +- [ x ] Skapa entiteter och verifiera tabellgenerering +- [ x ] LĂ€gg till relationer (One-to-Many, Many-to-Many) +- [ x ] Arbeta pĂ„ feature-branches och anvĂ€nd pull requests för kodgranskning ### ✅ Vecka 2 – Funktionalitet och relationer - [ ] Dela upp funktioner mellan gruppmedlemmar diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index 843d22ac..338f55a7 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -1,5 +1,14 @@ package org.example; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; + public class App { + static void main(String[] args) { + EntityManagerFactory emf = + Persistence.createEntityManagerFactory("myPU"); + + emf.close(); + } } diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index 558617d4..fe88e615 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -24,7 +24,7 @@ + value="jdbc:mysql://localhost:3306/moviedb?useSSL=false&serverTimezone=UTC"/> + From 2ea5a94818ec8735fdc9cd3fc5320f0de23c59e3 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 20:33:50 +0100 Subject: [PATCH 13/42] adds dependency for dotenv and gson --- pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pom.xml b/pom.xml index bea97bcf..614d801a 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,16 @@ 5.21.0 + + com.google.code.gson + gson + 2.10.1 + + + io.github.cdimascio + dotenv-java + 3.0.0 + org.junit.jupiter junit-jupiter From 69db4ee111a5d96491c0235348c935002c7bdb52 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 20:34:04 +0100 Subject: [PATCH 14/42] adds .env to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ac465db..244268f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ /.idea/ +.env From eeaaf32057de76e494df49bd1bdc30f16f4e4605 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 20:34:51 +0100 Subject: [PATCH 15/42] adds empty MovieDTO that represents the data from API --- src/main/java/org/example/DTO/MovieDTO.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/main/java/org/example/DTO/MovieDTO.java diff --git a/src/main/java/org/example/DTO/MovieDTO.java b/src/main/java/org/example/DTO/MovieDTO.java new file mode 100644 index 00000000..1ba59182 --- /dev/null +++ b/src/main/java/org/example/DTO/MovieDTO.java @@ -0,0 +1,4 @@ +package org.example.DTO; + +public class MovieDTO { +} From e3f0aebcbbb5a4a823467cbe17b17603532362c2 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 20:35:11 +0100 Subject: [PATCH 16/42] adds TopRatedResponseDTO that represents the data from API --- .../java/org/example/DTO/TopRatedResponseDTO.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/org/example/DTO/TopRatedResponseDTO.java diff --git a/src/main/java/org/example/DTO/TopRatedResponseDTO.java b/src/main/java/org/example/DTO/TopRatedResponseDTO.java new file mode 100644 index 00000000..a6891342 --- /dev/null +++ b/src/main/java/org/example/DTO/TopRatedResponseDTO.java @@ -0,0 +1,11 @@ +package org.example.DTO; + +import java.util.List; + +public class TopRatedResponseDTO { + // This is what we get in response from the API + public int page; + public List results; + public int total_pages; + public int total_results; +} From 22354572b990ac23d57a8be91c5e73cd5a11930a Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 20:37:13 +0100 Subject: [PATCH 17/42] adds method to get top-rated movies and setup httpClient and gson --- src/main/java/org/example/API/TmdbClient.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/main/java/org/example/API/TmdbClient.java b/src/main/java/org/example/API/TmdbClient.java index a68fa461..50fe7e07 100644 --- a/src/main/java/org/example/API/TmdbClient.java +++ b/src/main/java/org/example/API/TmdbClient.java @@ -1,4 +1,58 @@ package org.example.API; +import com.google.gson.Gson; +import io.github.cdimascio.dotenv.Dotenv; +import org.example.DTO.TopRatedResponseDTO; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + public class TmdbClient { + + private final String apiKey; + private final String baseUrl; + private final HttpClient httpClient; + private final Gson gson; + + + public TmdbClient() { + this(HttpClient.newHttpClient(), new Gson()); + } + + + public TmdbClient(HttpClient httpClient, Gson gson) { + + Dotenv dotenv = Dotenv.load(); + + this.apiKey = dotenv.get("TMDB_API_KEY"); + this.baseUrl = dotenv.get("TMDB_BASE_URL"); + + if (apiKey == null || baseUrl == null) { + throw new RuntimeException("TMDB config missing in .env"); + } + + this.httpClient = httpClient; + this.gson = gson; + } + + public TopRatedResponseDTO getTopRatedMovies() { + try { + String url = baseUrl + "/movie/top_rated?api_key=" + apiKey; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return gson.fromJson(response.body(), TopRatedResponseDTO.class); + + } catch (Exception e) { + throw new RuntimeException("Could not get top rated movies from TMDB", e); + } + } } From a9161ab54b8460f964998316dc5384f6ea29fc8c Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 21:20:50 +0100 Subject: [PATCH 18/42] Updates movie entity --- .../java/org/example/movie/entity/Movie.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java index c1491ddf..3f0d1096 100644 --- a/src/main/java/org/example/movie/entity/Movie.java +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; + @Entity @Table(name = "movies") public class Movie { @@ -15,6 +16,14 @@ public class Movie { @Column(nullable = false) private String title; + private String genre; + + @Column(length = 1000) + private String description; + + @Column(name = "image_url") + private String imageUrl; + private Integer releaseYear; private Double imdbRating; @@ -32,6 +41,19 @@ public Movie(String title) { public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } + public String getGenre() { return genre; } + public void setGenre(String genre) { this.genre = genre; } + + public String getDescription() { return description; } + public void setDescription(String description) { + this.description = description; + } + + public String getImageUrl() { return imageUrl; } + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + public Integer getReleaseYear() { return releaseYear; } public void setReleaseYear(Integer releaseYear) { this.releaseYear = releaseYear; @@ -43,4 +65,9 @@ public void setImdbRating(Double imdbRating) { } public List getRoles() { return roles; } + + public void addRole(Role role) { + roles.add(role); + role.setMovie(this); + } } From 7faddff26866a9998f49b36054271e136316d7c3 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 21:21:20 +0100 Subject: [PATCH 19/42] adds CastDTO --- src/main/java/org/example/DTO/CastDTO.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/org/example/DTO/CastDTO.java diff --git a/src/main/java/org/example/DTO/CastDTO.java b/src/main/java/org/example/DTO/CastDTO.java new file mode 100644 index 00000000..54d8351d --- /dev/null +++ b/src/main/java/org/example/DTO/CastDTO.java @@ -0,0 +1,8 @@ +package org.example.DTO; + +public class CastDTO { + public int id; + public String name; + public String profile_path; + public int order; // 0 = main actor, 2 = second actor, 3 = third actor etc. +} From 5df26682be1f4977e88d128f0ffa5adee9745046 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 21:21:36 +0100 Subject: [PATCH 20/42] adds CrewDTO --- src/main/java/org/example/DTO/CrewDTO.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/org/example/DTO/CrewDTO.java diff --git a/src/main/java/org/example/DTO/CrewDTO.java b/src/main/java/org/example/DTO/CrewDTO.java new file mode 100644 index 00000000..c500bb30 --- /dev/null +++ b/src/main/java/org/example/DTO/CrewDTO.java @@ -0,0 +1,8 @@ +package org.example.DTO; + +public class CrewDTO { + public int id; + public String name; + public String profile_path; + public String job; // example: Director, Assistant Director, Producer etc. +} From c98bf88f1d7d8424c37df347ed00b09259d338a0 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 21:21:46 +0100 Subject: [PATCH 21/42] adds CreditsDTO --- src/main/java/org/example/DTO/CreditsDTO.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/org/example/DTO/CreditsDTO.java diff --git a/src/main/java/org/example/DTO/CreditsDTO.java b/src/main/java/org/example/DTO/CreditsDTO.java new file mode 100644 index 00000000..463115d6 --- /dev/null +++ b/src/main/java/org/example/DTO/CreditsDTO.java @@ -0,0 +1,9 @@ +package org.example.DTO; + +import java.util.List; + +public class CreditsDTO { + public int id; + public List cast; + public List crew; +} From a34a97baaf6bb8ac558790efccdc72a9d01e531f Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 21:22:02 +0100 Subject: [PATCH 22/42] adds MovieDetailsDTO --- src/main/java/org/example/DTO/MovieDetailsDTO.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/org/example/DTO/MovieDetailsDTO.java diff --git a/src/main/java/org/example/DTO/MovieDetailsDTO.java b/src/main/java/org/example/DTO/MovieDetailsDTO.java new file mode 100644 index 00000000..824a796c --- /dev/null +++ b/src/main/java/org/example/DTO/MovieDetailsDTO.java @@ -0,0 +1,13 @@ +package org.example.DTO; + +public class MovieDetailsDTO { + public int id; + public String title; + public String overview; + public String release_date; + public int runtime; + public double vote_average; + public String poster_path; + + // public List genres; Should we have this? +} From 5b899fdf2acbd88e8c23623cdce87eb03806af3c Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 21:22:26 +0100 Subject: [PATCH 23/42] adds metod to get MovieDetails with movieId and Credits with movieId --- src/main/java/org/example/API/TmdbClient.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/java/org/example/API/TmdbClient.java b/src/main/java/org/example/API/TmdbClient.java index 50fe7e07..c05f97cd 100644 --- a/src/main/java/org/example/API/TmdbClient.java +++ b/src/main/java/org/example/API/TmdbClient.java @@ -2,6 +2,8 @@ import com.google.gson.Gson; import io.github.cdimascio.dotenv.Dotenv; +import org.example.DTO.CreditsDTO; +import org.example.DTO.MovieDetailsDTO; import org.example.DTO.TopRatedResponseDTO; import java.net.URI; @@ -55,4 +57,43 @@ public TopRatedResponseDTO getTopRatedMovies() { throw new RuntimeException("Could not get top rated movies from TMDB", e); } } + + public MovieDetailsDTO getMovieDetails(int movieId){ + try { + String url = baseUrl + "/movie/" + movieId + "?api_key=" + apiKey; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return gson.fromJson(response.body(), MovieDetailsDTO.class); + + } catch (Exception e) { + throw new RuntimeException("Could not get movie details from TMDB"); + } + + } + + public CreditsDTO getMovieCredits(int movieId){ + try { + String url = baseUrl + "/movie/" + movieId + "/credits?api_key=" + apiKey; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return gson.fromJson(response.body(), CreditsDTO.class); + + } catch (Exception e){ + throw new RuntimeException("Could not get movie credits from TMDB"); + } + } } From 30c1788bf7d61e713e78866ee3edbed48477ecd4 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Wed, 17 Dec 2025 21:22:41 +0100 Subject: [PATCH 24/42] adds a comment --- src/main/java/org/example/DTO/TopRatedResponseDTO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/DTO/TopRatedResponseDTO.java b/src/main/java/org/example/DTO/TopRatedResponseDTO.java index a6891342..12eb58d4 100644 --- a/src/main/java/org/example/DTO/TopRatedResponseDTO.java +++ b/src/main/java/org/example/DTO/TopRatedResponseDTO.java @@ -3,7 +3,7 @@ import java.util.List; public class TopRatedResponseDTO { - // This is what we get in response from the API + // This is what we get in response from the API. API response is a wrapper-object. public int page; public List results; public int total_pages; From 113e894d87b240495b32e060056cd78bd8bb156c Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 21:29:56 +0100 Subject: [PATCH 25/42] Updates movie entity from integer to string --- src/main/java/org/example/movie/entity/Movie.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java index 3f0d1096..5209e2a6 100644 --- a/src/main/java/org/example/movie/entity/Movie.java +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -24,7 +24,7 @@ public class Movie { @Column(name = "image_url") private String imageUrl; - private Integer releaseYear; + private String releaseYear; private Double imdbRating; @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) @@ -54,8 +54,8 @@ public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } - public Integer getReleaseYear() { return releaseYear; } - public void setReleaseYear(Integer releaseYear) { + public String getReleaseYear() { return releaseYear; } + public void setReleaseYear(String releaseYear) { this.releaseYear = releaseYear; } From 67a689a3b9bdbb0351cc036b0adbcd41844f9221 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 21:36:21 +0100 Subject: [PATCH 26/42] Updates person entity from string to LocalDate --- src/main/java/org/example/movie/entity/Person.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/movie/entity/Person.java b/src/main/java/org/example/movie/entity/Person.java index 6dfda6fc..0406ea88 100644 --- a/src/main/java/org/example/movie/entity/Person.java +++ b/src/main/java/org/example/movie/entity/Person.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; + @Entity @Table(name = "persons") public class Person { @@ -17,12 +18,14 @@ public class Person { private String name; private LocalDate birthDate; + + @Column(name = "image_url") private String imageUrl; @OneToMany(mappedBy = "person", cascade = CascadeType.ALL) private List roles = new ArrayList<>(); - public Person() {} + protected Person() {} public Person(String name) { this.name = name; @@ -44,4 +47,10 @@ public void setImageUrl(String imageUrl) { } public List getRoles() { return roles; } + + public void addRole(Role role) { + roles.add(role); + role.setPerson(this); + } } + From 02513de9ae75c74ff2c16f6e41775bfc3e2c8b6c Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 21:39:18 +0100 Subject: [PATCH 27/42] Delete RoleType from persistence file --- src/main/resources/META-INF/persistence.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index fe88e615..f06ae868 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -14,7 +14,6 @@ org.example.movie.entity.Movie org.example.movie.entity.Person org.example.movie.entity.Role - org.example.movie.entity.RoleType false From 5e5c93fcc03ed5a7c44effbe28fc4601dc7edf58 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Wed, 17 Dec 2025 21:47:49 +0100 Subject: [PATCH 28/42] Updates entity role file --- .../java/org/example/movie/entity/Role.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/example/movie/entity/Role.java b/src/main/java/org/example/movie/entity/Role.java index 0caa359d..5199e2f5 100644 --- a/src/main/java/org/example/movie/entity/Role.java +++ b/src/main/java/org/example/movie/entity/Role.java @@ -14,10 +14,6 @@ public class Role { @Column(nullable = false) private RoleType roleType; - /** - * Used for ACTOR roles (0 = main actor, 1 = second, 2 = third) - * Can be null for DIRECTOR - */ private Integer creditOrder; @ManyToOne(fetch = FetchType.LAZY) @@ -28,12 +24,12 @@ public class Role { @JoinColumn(name = "person_id", nullable = false) private Person person; - public Role() {} + protected Role() {} public Role(RoleType roleType, Movie movie, Person person) { this.roleType = roleType; - this.movie = movie; - this.person = person; + setMovie(movie); + setPerson(person); } public Long getId() { return id; } @@ -51,10 +47,17 @@ public void setCreditOrder(Integer creditOrder) { public Movie getMovie() { return movie; } public void setMovie(Movie movie) { this.movie = movie; + if (movie != null && !movie.getRoles().contains(this)) { + movie.getRoles().add(this); + } } public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; + if (person != null && !person.getRoles().contains(this)) { + person.getRoles().add(this); + } } } + From 8d4a3327d20a3685a10d85988616828afd17bc21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Thu, 18 Dec 2025 09:23:52 +0100 Subject: [PATCH 29/42] Add JavaFX basics. --- pom.xml | 22 +++++++++++++++ src/main/java/org/example/App.java | 15 ++++++++++- src/main/java/org/example/ui/MainApp.java | 21 +++++++++++++++ .../java/org/example/ui/MainController.java | 27 +++++++++++++++++++ src/main/resources/MainView.fxml | 22 +++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/example/ui/MainController.java create mode 100644 src/main/resources/MainView.fxml diff --git a/pom.xml b/pom.xml index bea97bcf..0e4b675f 100644 --- a/pom.xml +++ b/pom.xml @@ -65,5 +65,27 @@ javafx-media 25.0.1 + + org.openjfx + javafx-controls + 25.0.1 + + + org.openjfx + javafx-fxml + 25.0.1 + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + org.example.App + + + + diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index 843d22ac..6227058f 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -1,5 +1,18 @@ package org.example; -public class App { +import javafx.application.Application; +import javafx.stage.Stage; +import org.example.ui.MainApp; +public class App extends Application { + + @Override + public void start(Stage primaryStage) { + MainApp mainApp = new MainApp(); + mainApp.start(primaryStage); + } + + public static void main(String[] args) { + launch(args); + } } diff --git a/src/main/java/org/example/ui/MainApp.java b/src/main/java/org/example/ui/MainApp.java index 74017841..c3f33eda 100644 --- a/src/main/java/org/example/ui/MainApp.java +++ b/src/main/java/org/example/ui/MainApp.java @@ -1,4 +1,25 @@ package org.example.ui; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Stage; + public class MainApp { + + public void start(Stage stage) { + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/MainView.fxml") + ); + + Scene scene = new Scene(loader.load(), 1920, 1080); + + stage.setTitle("Movie Database App"); + stage.setScene(scene); + stage.show(); + + } catch (Exception e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/org/example/ui/MainController.java b/src/main/java/org/example/ui/MainController.java new file mode 100644 index 00000000..66ccec63 --- /dev/null +++ b/src/main/java/org/example/ui/MainController.java @@ -0,0 +1,27 @@ +package org.example.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.ListView; +import org.example.service.MovieServices; +import org.example.entity.Movie; + +import java.util.List; + +public class MainController { + + @FXML + private ListView movieList; + + private final MovieServices movieServices = new MovieServices(); + + @FXML + private void loadMovies() { + movieList.getItems().clear(); + + /* List movies = movieServices.getAllMovies();*/ //Placeholder - hĂ€r lĂ€gger vi in metoder som byggs i MovieServices, skickas till repos och JPA + + /* for (Movie movie : movies) { + movieList.getItems().add(movie.getTitle()); + }*/ + } +} diff --git a/src/main/resources/MainView.fxml b/src/main/resources/MainView.fxml new file mode 100644 index 00000000..649f9f55 --- /dev/null +++ b/src/main/resources/MainView.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + +
+ +
+ +
From 67ca487460914b1520ba678ca432a1115af273a3 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 18 Dec 2025 18:59:25 +0100 Subject: [PATCH 30/42] Convert TMDB DTOs to immutable records and add @SerializedName mappings --- src/main/java/org/example/DTO/CastDTO.java | 8 -------- src/main/java/org/example/DTO/CreditsDTO.java | 9 --------- src/main/java/org/example/DTO/CrewDTO.java | 8 -------- src/main/java/org/example/DTO/MovieDTO.java | 4 ---- .../java/org/example/DTO/MovieDetailsDTO.java | 13 ------------- .../java/org/example/DTO/TopRatedResponseDTO.java | 11 ----------- src/main/java/org/example/dto/CastDTO.java | 10 ++++++++++ src/main/java/org/example/dto/CreditsDTO.java | 9 +++++++++ src/main/java/org/example/dto/CrewDTO.java | 10 ++++++++++ src/main/java/org/example/dto/MovieDTO.java | 12 ++++++++++++ .../java/org/example/dto/MovieDetailsDTO.java | 15 +++++++++++++++ .../java/org/example/dto/TopRatedResponseDTO.java | 12 ++++++++++++ 12 files changed, 68 insertions(+), 53 deletions(-) delete mode 100644 src/main/java/org/example/DTO/CastDTO.java delete mode 100644 src/main/java/org/example/DTO/CreditsDTO.java delete mode 100644 src/main/java/org/example/DTO/CrewDTO.java delete mode 100644 src/main/java/org/example/DTO/MovieDTO.java delete mode 100644 src/main/java/org/example/DTO/MovieDetailsDTO.java delete mode 100644 src/main/java/org/example/DTO/TopRatedResponseDTO.java create mode 100644 src/main/java/org/example/dto/CastDTO.java create mode 100644 src/main/java/org/example/dto/CreditsDTO.java create mode 100644 src/main/java/org/example/dto/CrewDTO.java create mode 100644 src/main/java/org/example/dto/MovieDTO.java create mode 100644 src/main/java/org/example/dto/MovieDetailsDTO.java create mode 100644 src/main/java/org/example/dto/TopRatedResponseDTO.java diff --git a/src/main/java/org/example/DTO/CastDTO.java b/src/main/java/org/example/DTO/CastDTO.java deleted file mode 100644 index 54d8351d..00000000 --- a/src/main/java/org/example/DTO/CastDTO.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.example.DTO; - -public class CastDTO { - public int id; - public String name; - public String profile_path; - public int order; // 0 = main actor, 2 = second actor, 3 = third actor etc. -} diff --git a/src/main/java/org/example/DTO/CreditsDTO.java b/src/main/java/org/example/DTO/CreditsDTO.java deleted file mode 100644 index 463115d6..00000000 --- a/src/main/java/org/example/DTO/CreditsDTO.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.DTO; - -import java.util.List; - -public class CreditsDTO { - public int id; - public List cast; - public List crew; -} diff --git a/src/main/java/org/example/DTO/CrewDTO.java b/src/main/java/org/example/DTO/CrewDTO.java deleted file mode 100644 index c500bb30..00000000 --- a/src/main/java/org/example/DTO/CrewDTO.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.example.DTO; - -public class CrewDTO { - public int id; - public String name; - public String profile_path; - public String job; // example: Director, Assistant Director, Producer etc. -} diff --git a/src/main/java/org/example/DTO/MovieDTO.java b/src/main/java/org/example/DTO/MovieDTO.java deleted file mode 100644 index 1ba59182..00000000 --- a/src/main/java/org/example/DTO/MovieDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.example.DTO; - -public class MovieDTO { -} diff --git a/src/main/java/org/example/DTO/MovieDetailsDTO.java b/src/main/java/org/example/DTO/MovieDetailsDTO.java deleted file mode 100644 index 824a796c..00000000 --- a/src/main/java/org/example/DTO/MovieDetailsDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.example.DTO; - -public class MovieDetailsDTO { - public int id; - public String title; - public String overview; - public String release_date; - public int runtime; - public double vote_average; - public String poster_path; - - // public List genres; Should we have this? -} diff --git a/src/main/java/org/example/DTO/TopRatedResponseDTO.java b/src/main/java/org/example/DTO/TopRatedResponseDTO.java deleted file mode 100644 index 12eb58d4..00000000 --- a/src/main/java/org/example/DTO/TopRatedResponseDTO.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.DTO; - -import java.util.List; - -public class TopRatedResponseDTO { - // This is what we get in response from the API. API response is a wrapper-object. - public int page; - public List results; - public int total_pages; - public int total_results; -} diff --git a/src/main/java/org/example/dto/CastDTO.java b/src/main/java/org/example/dto/CastDTO.java new file mode 100644 index 00000000..4ca68cba --- /dev/null +++ b/src/main/java/org/example/dto/CastDTO.java @@ -0,0 +1,10 @@ +package org.example.dto; + +import com.google.gson.annotations.SerializedName; + +public record CastDTO ( + int id, + String name, + @SerializedName("profile_path") String profilePath, + int order // 0 = main actor, 2 = second actor, 3 = third actor etc. +) {} diff --git a/src/main/java/org/example/dto/CreditsDTO.java b/src/main/java/org/example/dto/CreditsDTO.java new file mode 100644 index 00000000..1360473f --- /dev/null +++ b/src/main/java/org/example/dto/CreditsDTO.java @@ -0,0 +1,9 @@ +package org.example.dto; + +import java.util.List; + +public record CreditsDTO( + int id, + List cast, + List crew +) {} diff --git a/src/main/java/org/example/dto/CrewDTO.java b/src/main/java/org/example/dto/CrewDTO.java new file mode 100644 index 00000000..c8e54186 --- /dev/null +++ b/src/main/java/org/example/dto/CrewDTO.java @@ -0,0 +1,10 @@ +package org.example.dto; + +import com.google.gson.annotations.SerializedName; + +public record CrewDTO( + int id, + String name, + @SerializedName("profile_path") String profilePath, + String job +) {} diff --git a/src/main/java/org/example/dto/MovieDTO.java b/src/main/java/org/example/dto/MovieDTO.java new file mode 100644 index 00000000..6b2e3ce5 --- /dev/null +++ b/src/main/java/org/example/dto/MovieDTO.java @@ -0,0 +1,12 @@ +package org.example.dto; + +import com.google.gson.annotations.SerializedName; + +public record MovieDTO( + int id, + String title, + String overview, + @SerializedName("release_date") String releaseDate, + @SerializedName("vote_average") double voteAverage, + @SerializedName("poster_path") String posterPath +) {} diff --git a/src/main/java/org/example/dto/MovieDetailsDTO.java b/src/main/java/org/example/dto/MovieDetailsDTO.java new file mode 100644 index 00000000..908860a6 --- /dev/null +++ b/src/main/java/org/example/dto/MovieDetailsDTO.java @@ -0,0 +1,15 @@ +package org.example.dto; + +import com.google.gson.annotations.SerializedName; + +public record MovieDetailsDTO( + int id, + String title, + String overview, + @SerializedName("release_date") String releaseDate, + int runtime, + @SerializedName("vote_average") double voteAverage, + @SerializedName("poster_path") String posterPath + + // Should we add a genre DTO? +) {} diff --git a/src/main/java/org/example/dto/TopRatedResponseDTO.java b/src/main/java/org/example/dto/TopRatedResponseDTO.java new file mode 100644 index 00000000..88b5d414 --- /dev/null +++ b/src/main/java/org/example/dto/TopRatedResponseDTO.java @@ -0,0 +1,12 @@ +package org.example.dto; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public record TopRatedResponseDTO( + int page, + List results, + @SerializedName("total_pages") int totalPages, + @SerializedName("total_results") int totalResults +) {} From a9cc81b5a683054b20916e0ed619aade0657cf0c Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 18 Dec 2025 19:00:02 +0100 Subject: [PATCH 31/42] Improve TMDB client robustness and error handling --- .../org/example/{API => api}/TmdbClient.java | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) rename src/main/java/org/example/{API => api}/TmdbClient.java (62%) diff --git a/src/main/java/org/example/API/TmdbClient.java b/src/main/java/org/example/api/TmdbClient.java similarity index 62% rename from src/main/java/org/example/API/TmdbClient.java rename to src/main/java/org/example/api/TmdbClient.java index c05f97cd..f05061cb 100644 --- a/src/main/java/org/example/API/TmdbClient.java +++ b/src/main/java/org/example/api/TmdbClient.java @@ -1,15 +1,16 @@ -package org.example.API; +package org.example.api; import com.google.gson.Gson; import io.github.cdimascio.dotenv.Dotenv; -import org.example.DTO.CreditsDTO; -import org.example.DTO.MovieDetailsDTO; -import org.example.DTO.TopRatedResponseDTO; +import org.example.dto.CreditsDTO; +import org.example.dto.MovieDetailsDTO; +import org.example.dto.TopRatedResponseDTO; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; public class TmdbClient { @@ -39,18 +40,36 @@ public TmdbClient(HttpClient httpClient, Gson gson) { this.gson = gson; } + public TmdbClient(HttpClient httpClient, Gson gson, String apiKey, String baseUrl) { + if (apiKey == null || baseUrl == null) { + throw new IllegalArgumentException("TMDB config missing"); + } + + this.httpClient = httpClient; + this.gson = gson; + this.apiKey = apiKey; + this.baseUrl = baseUrl; + } + public TopRatedResponseDTO getTopRatedMovies() { try { String url = baseUrl + "/movie/top_rated?api_key=" + apiKey; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) + .timeout(Duration.ofSeconds(10)) .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException( + "TMDB API error: HTTP " + response.statusCode() + " - " + response.body() + ); + } + return gson.fromJson(response.body(), TopRatedResponseDTO.class); } catch (Exception e) { @@ -58,42 +77,55 @@ public TopRatedResponseDTO getTopRatedMovies() { } } - public MovieDetailsDTO getMovieDetails(int movieId){ + public MovieDetailsDTO getMovieDetails(int movieId) { try { String url = baseUrl + "/movie/" + movieId + "?api_key=" + apiKey; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) + .timeout(Duration.ofSeconds(10)) .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException( + "TMDB API error: HTTP " + response.statusCode() + " - " + response.body() + ); + } + return gson.fromJson(response.body(), MovieDetailsDTO.class); } catch (Exception e) { - throw new RuntimeException("Could not get movie details from TMDB"); + throw new RuntimeException("Could not get movie details from TMDB", e); } - } - public CreditsDTO getMovieCredits(int movieId){ + public CreditsDTO getMovieCredits(int movieId) { try { String url = baseUrl + "/movie/" + movieId + "/credits?api_key=" + apiKey; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) + .timeout(Duration.ofSeconds(10)) .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException( + "TMDB API error: HTTP " + response.statusCode() + " - " + response.body() + ); + } + return gson.fromJson(response.body(), CreditsDTO.class); - } catch (Exception e){ - throw new RuntimeException("Could not get movie credits from TMDB"); + } catch (Exception e) { + throw new RuntimeException("Could not get movie credits from TMDB", e); } } } From ee1ae4fc1913b01f4f457f7a451a1a7b971c2b2a Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 18 Dec 2025 19:26:09 +0100 Subject: [PATCH 32/42] Use IllegalArgumentException for missing TMDB configuration --- src/main/java/org/example/api/TmdbClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/api/TmdbClient.java b/src/main/java/org/example/api/TmdbClient.java index f05061cb..279e6dad 100644 --- a/src/main/java/org/example/api/TmdbClient.java +++ b/src/main/java/org/example/api/TmdbClient.java @@ -33,7 +33,7 @@ public TmdbClient(HttpClient httpClient, Gson gson) { this.baseUrl = dotenv.get("TMDB_BASE_URL"); if (apiKey == null || baseUrl == null) { - throw new RuntimeException("TMDB config missing in .env"); + throw new IllegalArgumentException("TMDB config missing in .env"); } this.httpClient = httpClient; From bffbf99c168334492996d3c10735332270fc56b4 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Thu, 18 Dec 2025 19:45:35 +0100 Subject: [PATCH 33/42] Delete variables (columns) in persons and update movie with timdbId column --- .../java/org/example/movie/entity/Movie.java | 20 ++++++++++++++++--- .../java/org/example/movie/entity/Person.java | 15 -------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java index 5209e2a6..8eb04e6f 100644 --- a/src/main/java/org/example/movie/entity/Movie.java +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -21,10 +21,13 @@ public class Movie { @Column(length = 1000) private String description; + @Column(nullable = false, unique = true) + private Integer tmdbId; + @Column(name = "image_url") private String imageUrl; - private String releaseYear; + private Integer releaseYear; private Double imdbRating; @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL) @@ -54,8 +57,8 @@ public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } - public String getReleaseYear() { return releaseYear; } - public void setReleaseYear(String releaseYear) { + public Integer getReleaseYear() { return releaseYear; } + public void setReleaseYear(Integer releaseYear) { this.releaseYear = releaseYear; } @@ -64,6 +67,17 @@ public void setImdbRating(Double imdbRating) { this.imdbRating = imdbRating; } + public Integer getTmdbId() { + return tmdbId; + } + + public void setTmdbId(Integer tmdbId) { + if (tmdbId == null || tmdbId <= 0) { + throw new IllegalArgumentException("tmdbId must be a positive number"); + } + this.tmdbId = tmdbId; + } + public List getRoles() { return roles; } public void addRole(Role role) { diff --git a/src/main/java/org/example/movie/entity/Person.java b/src/main/java/org/example/movie/entity/Person.java index 0406ea88..1ebd3d02 100644 --- a/src/main/java/org/example/movie/entity/Person.java +++ b/src/main/java/org/example/movie/entity/Person.java @@ -17,11 +17,6 @@ public class Person { @Column(nullable = false) private String name; - private LocalDate birthDate; - - @Column(name = "image_url") - private String imageUrl; - @OneToMany(mappedBy = "person", cascade = CascadeType.ALL) private List roles = new ArrayList<>(); @@ -36,16 +31,6 @@ public Person(String name) { public String getName() { return name; } public void setName(String name) { this.name = name; } - public LocalDate getBirthDate() { return birthDate; } - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public String getImageUrl() { return imageUrl; } - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - public List getRoles() { return roles; } public void addRole(Role role) { From e710b6b688e7d9d046fd0f592869097a9f295baf Mon Sep 17 00:00:00 2001 From: Tatjana Date: Thu, 18 Dec 2025 19:58:57 +0100 Subject: [PATCH 34/42] Delete variables (columns) in persons and update movie with timdbId column rabbit review --- src/main/java/org/example/movie/entity/Movie.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java index 8eb04e6f..1a1de54b 100644 --- a/src/main/java/org/example/movie/entity/Movie.java +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -35,7 +35,7 @@ public class Movie { protected Movie() {} - public Movie(String title) { + public Movie(String title, Integer tmdbId) { this.title = title; } From 720d615eeb4b3eeea7eba8b126933a5b3fa078f5 Mon Sep 17 00:00:00 2001 From: Tatjana Date: Thu, 18 Dec 2025 20:42:09 +0100 Subject: [PATCH 35/42] Moves EntityManagerFactory from App.java to JPAUtil --- src/main/java/org/example/App.java | 7 +--- src/main/java/org/example/util/JPAUtil.java | 43 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/example/util/JPAUtil.java diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index 338f55a7..cbb75feb 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -1,14 +1,9 @@ package org.example; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.Persistence; - public class App { static void main(String[] args) { - EntityManagerFactory emf = - Persistence.createEntityManagerFactory("myPU"); - emf.close(); + } } diff --git a/src/main/java/org/example/util/JPAUtil.java b/src/main/java/org/example/util/JPAUtil.java new file mode 100644 index 00000000..2cb6d31e --- /dev/null +++ b/src/main/java/org/example/util/JPAUtil.java @@ -0,0 +1,43 @@ +package org.example.util; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; + +import java.util.function.Consumer; + +public class JPAUtil { + + private static final EntityManagerFactory emf; + + static { + emf = Persistence.createEntityManagerFactory("myPU"); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (emf.isOpen()) { + emf.close(); + } + })); + } + + public static EntityManager getEntityManager() { + return emf.createEntityManager(); + } + + public static void inTransaction(Consumer work) { + EntityManager em = getEntityManager(); + try { + var tx = em.getTransaction(); + tx.begin(); + work.accept(em); + tx.commit(); + } catch (Exception e) { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + throw e; + } finally { + em.close(); + } + } +} From 32e412c5052abf94d02448f9651c8c8407e2dddf Mon Sep 17 00:00:00 2001 From: Tatjana Date: Thu, 18 Dec 2025 20:46:33 +0100 Subject: [PATCH 36/42] Changes movie constructor with tmdbid --- src/main/java/org/example/movie/entity/Movie.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/example/movie/entity/Movie.java b/src/main/java/org/example/movie/entity/Movie.java index 1a1de54b..78e125c9 100644 --- a/src/main/java/org/example/movie/entity/Movie.java +++ b/src/main/java/org/example/movie/entity/Movie.java @@ -37,6 +37,7 @@ protected Movie() {} public Movie(String title, Integer tmdbId) { this.title = title; + this.tmdbId = tmdbId; } public Long getId() { return id; } From d61ead39d13283640ef01831ba0c04d43e7b4b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Thu, 18 Dec 2025 20:49:14 +0100 Subject: [PATCH 37/42] Fix issues commented on by the all-wise rabbit. --- src/main/java/org/example/ui/MainApp.java | 2 +- src/main/java/org/example/ui/MainController.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/ui/MainApp.java b/src/main/java/org/example/ui/MainApp.java index c3f33eda..f4c65527 100644 --- a/src/main/java/org/example/ui/MainApp.java +++ b/src/main/java/org/example/ui/MainApp.java @@ -12,7 +12,7 @@ public void start(Stage stage) { getClass().getResource("/MainView.fxml") ); - Scene scene = new Scene(loader.load(), 1920, 1080); + Scene scene = new Scene(loader.load()); stage.setTitle("Movie Database App"); stage.setScene(scene); diff --git a/src/main/java/org/example/ui/MainController.java b/src/main/java/org/example/ui/MainController.java index 66ccec63..bc8c6329 100644 --- a/src/main/java/org/example/ui/MainController.java +++ b/src/main/java/org/example/ui/MainController.java @@ -12,7 +12,11 @@ public class MainController { @FXML private ListView movieList; - private final MovieServices movieServices = new MovieServices(); + private final MovieServices movieServices; + + public MainController(MovieServices movieServices) { + this.movieServices = movieServices; + } @FXML private void loadMovies() { From 25951ac6a349c4127457313c21c31646759d34da Mon Sep 17 00:00:00 2001 From: Tatjana Date: Thu, 18 Dec 2025 21:04:05 +0100 Subject: [PATCH 38/42] Changes App.java with JPAUtil --- src/main/java/org/example/App.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index cbb75feb..7c8bef30 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -1,9 +1,13 @@ package org.example; +import org.example.util.JPAUtil; + public class App { static void main(String[] args) { - + JPAUtil.inTransaction(em -> { + System.out.println("Database schema initialized"); + }); } } From 4d1809916b702b0f967fbed50a88492c8119623c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Thu, 18 Dec 2025 21:25:55 +0100 Subject: [PATCH 39/42] Fix issues commented on by the all-wise rabbit. --- src/main/java/org/example/ui/MainApp.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/ui/MainApp.java b/src/main/java/org/example/ui/MainApp.java index f4c65527..0898e35b 100644 --- a/src/main/java/org/example/ui/MainApp.java +++ b/src/main/java/org/example/ui/MainApp.java @@ -8,8 +8,12 @@ public class MainApp { public void start(Stage stage) { try { + var fxmlUrl = getClass().getResource("/MainView.fxml"); + if (fxmlUrl == null) { + throw new IllegalStateException("Cannot find FXML resource: /MainView.fxml"); + } FXMLLoader loader = new FXMLLoader( - getClass().getResource("/MainView.fxml") + fxmlUrl ); Scene scene = new Scene(loader.load()); @@ -20,6 +24,12 @@ public void start(Stage stage) { } catch (Exception e) { e.printStackTrace(); + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR, + "Failed to load the application UI: " + e.getMessage() + ); + alert.showAndWait(); + throw new RuntimeException("UI initialization failed", e); } } } From d2fdea1245b35b841669e0860ff84a2d185010cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Thu, 18 Dec 2025 21:51:44 +0100 Subject: [PATCH 40/42] Update with pull from main. --- src/main/java/org/example/ui/MainController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/ui/MainController.java b/src/main/java/org/example/ui/MainController.java index bc8c6329..ddfe30fb 100644 --- a/src/main/java/org/example/ui/MainController.java +++ b/src/main/java/org/example/ui/MainController.java @@ -3,7 +3,7 @@ import javafx.fxml.FXML; import javafx.scene.control.ListView; import org.example.service.MovieServices; -import org.example.entity.Movie; +import org.example.movie.entity.Movie; import java.util.List; From 5364429eb4c22b801b5ff94e0ea99696d5eb2366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Fri, 19 Dec 2025 10:07:54 +0100 Subject: [PATCH 41/42] Update with pull from main. --- src/main/java/org/example/ui/MainApp.java | 10 +++++++--- src/main/java/org/example/ui/MainController.java | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/example/ui/MainApp.java b/src/main/java/org/example/ui/MainApp.java index 0898e35b..8b35f86c 100644 --- a/src/main/java/org/example/ui/MainApp.java +++ b/src/main/java/org/example/ui/MainApp.java @@ -3,18 +3,22 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; +import org.example.service.MovieServices; public class MainApp { + private final MovieServices movieServices = new MovieServices(); + public void start(Stage stage) { try { var fxmlUrl = getClass().getResource("/MainView.fxml"); if (fxmlUrl == null) { throw new IllegalStateException("Cannot find FXML resource: /MainView.fxml"); } - FXMLLoader loader = new FXMLLoader( - fxmlUrl - ); + + FXMLLoader loader = new FXMLLoader(fxmlUrl); + + loader.setControllerFactory(type -> new MainController(movieServices)); Scene scene = new Scene(loader.load()); diff --git a/src/main/java/org/example/ui/MainController.java b/src/main/java/org/example/ui/MainController.java index ddfe30fb..7c189659 100644 --- a/src/main/java/org/example/ui/MainController.java +++ b/src/main/java/org/example/ui/MainController.java @@ -1,6 +1,7 @@ package org.example.ui; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; import javafx.scene.control.ListView; import org.example.service.MovieServices; import org.example.movie.entity.Movie; @@ -15,9 +16,11 @@ public class MainController { private final MovieServices movieServices; public MainController(MovieServices movieServices) { - this.movieServices = movieServices; + this.movieServices = movieServices; } + + @FXML private void loadMovies() { movieList.getItems().clear(); From 5f8f90c174c29d739acd9b144a4fba0122c6f9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20R=C3=B6nnqvist?= Date: Fri, 19 Dec 2025 11:50:22 +0100 Subject: [PATCH 42/42] Sort GUI with structure/discipline and enable loading data from API (directly for now) to create cards for each movie shown at start page. --- src/main/java/org/example/api/TmdbClient.java | 2 + .../org/example/ui/ApiMovieDataSource.java | 19 ++++ src/main/java/org/example/ui/MainApp.java | 16 ++- .../java/org/example/ui/MainController.java | 101 +++++++++++++++++- .../java/org/example/ui/MovieDataSource.java | 10 ++ src/main/resources/Images/no-poster.jpg | Bin 0 -> 11176 bytes src/main/resources/MainView.fxml | 29 +++-- 7 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/example/ui/ApiMovieDataSource.java create mode 100644 src/main/java/org/example/ui/MovieDataSource.java create mode 100644 src/main/resources/Images/no-poster.jpg diff --git a/src/main/java/org/example/api/TmdbClient.java b/src/main/java/org/example/api/TmdbClient.java index 279e6dad..fbbc4750 100644 --- a/src/main/java/org/example/api/TmdbClient.java +++ b/src/main/java/org/example/api/TmdbClient.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import io.github.cdimascio.dotenv.Dotenv; +import nonapi.io.github.classgraph.json.JSONUtils; import org.example.dto.CreditsDTO; import org.example.dto.MovieDetailsDTO; import org.example.dto.TopRatedResponseDTO; @@ -49,6 +50,7 @@ public TmdbClient(HttpClient httpClient, Gson gson, String apiKey, String baseUr this.gson = gson; this.apiKey = apiKey; this.baseUrl = baseUrl; + System.out.println("TMDB BASE URL = " + baseUrl); } public TopRatedResponseDTO getTopRatedMovies() { diff --git a/src/main/java/org/example/ui/ApiMovieDataSource.java b/src/main/java/org/example/ui/ApiMovieDataSource.java new file mode 100644 index 00000000..c38cbdd4 --- /dev/null +++ b/src/main/java/org/example/ui/ApiMovieDataSource.java @@ -0,0 +1,19 @@ +package org.example.ui; + +import java.util.List; +import org.example.api.TmdbClient; +import org.example.dto.MovieDTO; + +public class ApiMovieDataSource implements MovieDataSource { + + private final TmdbClient tmdbClient; + + public ApiMovieDataSource(TmdbClient tmdbClient) { + this.tmdbClient = tmdbClient; + } + + @Override + public List getTopRatedMovies() { + return tmdbClient.getTopRatedMovies().results(); // or searchMovies(), etc. + } +} diff --git a/src/main/java/org/example/ui/MainApp.java b/src/main/java/org/example/ui/MainApp.java index 8b35f86c..7fa49193 100644 --- a/src/main/java/org/example/ui/MainApp.java +++ b/src/main/java/org/example/ui/MainApp.java @@ -3,6 +3,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; +import org.example.api.TmdbClient; import org.example.service.MovieServices; public class MainApp { @@ -11,6 +12,8 @@ public class MainApp { public void start(Stage stage) { try { + TmdbClient tmdbClient = new TmdbClient(); + MovieDataSource dataSource = new ApiMovieDataSource(tmdbClient); var fxmlUrl = getClass().getResource("/MainView.fxml"); if (fxmlUrl == null) { throw new IllegalStateException("Cannot find FXML resource: /MainView.fxml"); @@ -18,12 +21,21 @@ public void start(Stage stage) { FXMLLoader loader = new FXMLLoader(fxmlUrl); - loader.setControllerFactory(type -> new MainController(movieServices)); + loader.setControllerFactory(type -> { + if (type == MainController.class) { + return new MainController(dataSource); + } + try { + return type.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); Scene scene = new Scene(loader.load()); - stage.setTitle("Movie Database App"); stage.setScene(scene); + stage.setTitle("Movie Database App"); stage.show(); } catch (Exception e) { diff --git a/src/main/java/org/example/ui/MainController.java b/src/main/java/org/example/ui/MainController.java index 7c189659..1b3dca9a 100644 --- a/src/main/java/org/example/ui/MainController.java +++ b/src/main/java/org/example/ui/MainController.java @@ -2,33 +2,124 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.scene.control.Label; import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.example.dto.MovieDTO; import org.example.service.MovieServices; import org.example.movie.entity.Movie; import java.util.List; +import java.util.Objects; public class MainController { - @FXML + /* @FXML private ListView movieList; private final MovieServices movieServices; public MainController(MovieServices movieServices) { this.movieServices = movieServices; + }*/ + + private final MovieDataSource dataSource; + + public MainController(MovieDataSource dataSource) { + this.dataSource = dataSource; } + @FXML + private FlowPane movieContainer; + + @FXML + public void initialize() { + try { + dataSource.getTopRatedMovies().forEach(movie -> + movieContainer.getChildren().add(createMovieCard(movie)) + ); + } catch (Exception e) { + e.printStackTrace(); + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR, + "Failed to load the application UI: " + e.getMessage() + ); + alert.showAndWait(); + throw new RuntimeException("UI initialization failed", e); + } + } + + private static final String IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500"; + private static final double CARD_WIDTH = 160; + private static final double CARD_HEIGHT = 280; + private static final double POSTER_HEIGHT = 220; + + private VBox createMovieCard(MovieDTO movie) { + VBox card = new VBox(6); + card.setPrefSize(CARD_WIDTH, CARD_HEIGHT); + card.setMinSize(CARD_WIDTH, CARD_HEIGHT); + card.setMaxSize(CARD_WIDTH, CARD_HEIGHT); + + card.setStyle(""" + -fx-background-color: #1e1e1e; + -fx-padding: 6; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 8, 0, 0, 2); + """); + + ImageView poster = new ImageView(); + poster.setFitWidth(CARD_WIDTH - 12); + poster.setFitHeight(POSTER_HEIGHT); + poster.setPreserveRatio(false); + + String posterPath = movie.posterPath(); + + if (posterPath != null && !posterPath.isBlank()) { + poster.setImage(new Image(IMAGE_BASE_URL + posterPath, true)); + } else { + poster.setImage(new Image(Objects.requireNonNull( + getClass().getResourceAsStream("/images/no-poster.jpg")))); + } + + + Label title = new Label(movie.title()); + title.setWrapText(true); + title.setMaxHeight(40); // ≈ 2 lines + title.setStyle(""" + -fx-text-fill: white; + -fx-font-size: 12px; + -fx-font-weight: bold; + """); + + Label overview = new Label(movie.overview()); + overview.setWrapText(true); + overview.setMaxWidth(150); + + VBox.setVgrow(title, Priority.NEVER); + + card.getChildren().addAll(poster, title); + return card; + } @FXML + private TextField searchField; + + + /* @FXML private void loadMovies() { movieList.getItems().clear(); - /* List movies = movieServices.getAllMovies();*/ //Placeholder - hĂ€r lĂ€gger vi in metoder som byggs i MovieServices, skickas till repos och JPA + *//* List movies = movieServices.getAllMovies();*//* //Placeholder - hĂ€r lĂ€gger vi in metoder som byggs i MovieServices, skickas till repos och JPA - /* for (Movie movie : movies) { + *//* for (Movie movie : movies) { movieList.getItems().add(movie.getTitle()); - }*/ - } + }*//* + }*/ } diff --git a/src/main/java/org/example/ui/MovieDataSource.java b/src/main/java/org/example/ui/MovieDataSource.java new file mode 100644 index 00000000..5f1cec00 --- /dev/null +++ b/src/main/java/org/example/ui/MovieDataSource.java @@ -0,0 +1,10 @@ +package org.example.ui; + +import java.util.List; +import org.example.dto.MovieDTO; + +public interface MovieDataSource { + List getTopRatedMovies(); + + +} diff --git a/src/main/resources/Images/no-poster.jpg b/src/main/resources/Images/no-poster.jpg new file mode 100644 index 0000000000000000000000000000000000000000..729aeea637cc54b273a8a3692e5eeb26e7e5cfde GIT binary patch literal 11176 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 + - + -
- -