Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/main/java/org/example/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@

import javafx.application.Application;

/**
* Application entry point for the myPod application.
*
* <p>This class contains the {@code main} method and is responsible for
* bootstrapping the JavaFX runtime. It delegates control to the
* {@link MyPod} JavaFX {@link Application} implementation.</p>
*
* <p>No application logic is handled here; this class exists solely to
* provide a clean and explicit startup entry point.</p>
*/
public class App {

/**
* Launches the JavaFX application.
*
* @param args command-line arguments passed to the application
*/
public static void main(String[] args) {
Application.launch(MyPod.class, args);
}
Expand Down
44 changes: 39 additions & 5 deletions src/main/java/org/example/DatabaseInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@

import java.util.List;

/**
* Initializes and populates the application database.
*
* <p>This class is responsible for:</p>
* <ul>
* <li>Fetching music data from the iTunes Search API</li>
* <li>Persisting artists, albums, and songs using repository abstractions</li>
* <li>Ensuring required default playlists exist</li>
* </ul>
*
* <p>The initializer is designed to be idempotent: data is only inserted
* if the database is empty or missing required entities.</p>
*/
public class DatabaseInitializer {

private static final Logger logger = LoggerFactory.getLogger(DatabaseInitializer.class);
Expand All @@ -21,6 +34,15 @@ public class DatabaseInitializer {
private final ArtistRepository artistRepo;
private final PlaylistRepository playlistRepo;

/**
* Creates a new database initializer.
*
* @param apiClient client used to fetch data from the iTunes API
* @param songRepo repository for {@link Song} entities
* @param albumRepo repository for {@link Album} entities
* @param artistRepo repository for {@link Artist} entities
* @param playlistRepo repository for {@link Playlist} entities
*/
public DatabaseInitializer(ItunesApiClient apiClient, SongRepository songRepo, AlbumRepository albumRepo, ArtistRepository artistRepo, PlaylistRepository playlistRepo) {
this.apiClient = apiClient;
this.songRepo = songRepo;
Expand All @@ -29,9 +51,21 @@ public DatabaseInitializer(ItunesApiClient apiClient, SongRepository songRepo, A
this.playlistRepo = playlistRepo;
}

/**
* Initializes the database with music data and default playlists.
*
* <p>If the song table is empty, a predefined set of artist searches
* is executed against the iTunes API. The resulting artists, albums,
* and songs are persisted while avoiding duplicates.</p>
*
* <p>The method also ensures that required default playlists
* ("Library" and "Favorites") exist.</p>
*
* @throws RuntimeException if data fetching or persistence fails
*/
public void init() {
// Check if there is data already, fill if empty
if (songRepo.count() == 0) {
// Check if database is populated, populate if empty
if (songRepo.count() == 0) { // Limited artist set due to project scope
List<String> searches = List.of("the+war+on+drugs",
"refused",
"thrice",
Expand Down Expand Up @@ -69,12 +103,12 @@ public void init() {
}
}

if (!playlistRepo.existsByUniqueId(1L)) { // Finns det en playlist borde det vara "Bibliotek"
// Ensure default playlists exist
if (!playlistRepo.existsByUniqueId(1L)) {
Playlist library = playlistRepo.createPlaylist("Library");
playlistRepo.addSongs(library, songRepo.findAll());
//Lägger bara till låtar som fanns innan listan, om fler "laddas ner" behövs de manuellt läggas till
}
if (!playlistRepo.existsByUniqueId(2L)) { // Finns det två playlist borde den andra vara "Favoriter"
if (!playlistRepo.existsByUniqueId(2L)) {
playlistRepo.createPlaylist("Favorites");
}
Comment on lines +106 to 113
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "*PlaylistRepository*" --extension java

Repository: ithsjava25/project-jpa-mypod-d

Length of output: 244


🏁 Script executed:

fd -type f -name "*PlaylistRepository*" -e java

Repository: ithsjava25/project-jpa-mypod-d

Length of output: 244


🏁 Script executed:

fd -t f -name "*PlaylistRepository*" -e java

Repository: ithsjava25/project-jpa-mypod-d

Length of output: 304


🏁 Script executed:

fd PlaylistRepository -e java

Repository: ithsjava25/project-jpa-mypod-d

Length of output: 187


🏁 Script executed:

cat -n src/main/java/org/example/repo/PlaylistRepository.java
cat -n src/main/java/org/example/repo/PlaylistRepositoryImpl.java

Repository: ithsjava25/project-jpa-mypod-d

Length of output: 14264


🏁 Script executed:

sed -n '100,120p' src/main/java/org/example/DatabaseInitializer.java

Repository: ithsjava25/project-jpa-mypod-d

Length of output: 676


Add name-based playlist existence check instead of hardcoded IDs.

Using hardcoded IDs (1L, 2L) to check for playlist existence at lines 107 and 111 is fragile and assumes specific auto-increment behavior. If the database is reset or IDs are assigned differently (e.g., in a different order), these checks will fail silently and create duplicate "Library" and "Favorites" playlists.

Implement an existsByName(String name) method in PlaylistRepository and use it instead:

Suggested fix
         // Ensure default playlists exist
-        if (!playlistRepo.existsByUniqueId(1L)) {
+        if (!playlistRepo.existsByName("Library")) {
             Playlist library = playlistRepo.createPlaylist("Library");
             playlistRepo.addSongs(library, songRepo.findAll());
         }
-        if (!playlistRepo.existsByUniqueId(2L)) {
+        if (!playlistRepo.existsByName("Favorites")) {
             playlistRepo.createPlaylist("Favorites");
         }

Add to PlaylistRepository interface:

boolean existsByName(String name);

And implement in PlaylistRepositoryImpl:

`@Override`
public boolean existsByName(String name) {
    if (name == null || name.trim().isEmpty()) {
        throw new IllegalArgumentException("name cannot be null or empty");
    }
    try (var em = emf.createEntityManager()) {
        return em.createQuery("select count(pl) from Playlist pl where pl.name = :name", Long.class)
            .setParameter("name", name)
            .getSingleResult() > 0;
    }
}
🤖 Prompt for AI Agents
In `@src/main/java/org/example/DatabaseInitializer.java` around lines 106 - 113,
Replace fragile ID-based existence checks in DatabaseInitializer with a
name-based check: add boolean existsByName(String name) to PlaylistRepository
and implement it in PlaylistRepositoryImpl (validate name, query Playlist by
name and return whether count>0). Then update DatabaseInitializer to call
playlistRepo.existsByName("Library") and playlistRepo.existsByName("Favorites")
before creating those playlists and adding songs, removing the hardcoded 1L/2L
checks.

}
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/org/example/EntityManagerFactoryProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,32 @@
import java.util.List;
import java.util.Map;

/**
* Factory utility for creating a JPA {@link EntityManagerFactory}.
*
* <p>This class programmatically configures Hibernate without relying
* on a {@code persistence.xml} file. All JPA entities are discovered
* automatically via classpath scanning.</p>
*
* <p>The factory supports additional configuration properties that
* can be supplied at runtime.</p>
*/
public class EntityManagerFactoryProvider {

/**
* Creates and configures an {@link EntityManagerFactory}.
*
* <p>The method scans the specified entity package for classes
* annotated with {@link jakarta.persistence.Entity}, registers them
* with Hibernate, and applies the provided JDBC and Hibernate
* configuration properties.</p>
*
* @param jdbcUrl JDBC connection URL
* @param username database username
* @param password database password
* @param extraProps additional Hibernate configuration properties
* @return a fully initialized {@link EntityManagerFactory}
*/
public static EntityManagerFactory create(
String jdbcUrl,
String username,
Expand All @@ -32,6 +56,15 @@ public static EntityManagerFactory create(
return cfg.createEntityManagerFactory();
}

/**
* Scans the classpath for JPA entity classes.
*
* <p>All classes annotated with {@link Entity} within the specified
* package are discovered and loaded.</p>
*
* @param pkg base package to scan
* @return list of entity classes
*/
private static List<Class<?>> scanEntities(String pkg) {
try (ScanResult scanResult =
new ClassGraph()
Expand Down
47 changes: 41 additions & 6 deletions src/main/java/org/example/ItunesApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,46 @@
import java.util.ArrayList;
import java.util.List;

public class ItunesApiClient {
/**
* Client for interacting with the iTunes Search API.
*
* <p>This class is responsible for executing HTTP requests against
* Apple's public iTunes Search API and mapping the results into
* {@link ItunesDTO} objects.</p>
*
* <p>It performs basic response validation and result filtering
* to ensure that only relevant data is returned.</p>
*/

public class ItunesApiClient {
private static final Logger logger = LoggerFactory.getLogger(ItunesApiClient.class);

private final HttpClient client;
private final ObjectMapper mapper;

/**
* Creates a new iTunes API client.
*
* <p>Initializes an {@link HttpClient} and configures a Jackson
* {@link ObjectMapper} with Java Time support.</p>
*/
public ItunesApiClient() {
this.client = HttpClient.newHttpClient();
this.mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
}

/**
* Searches for songs by artist name using the iTunes Search API.
*
* <p>The search results are filtered so that only songs whose
* artist name matches the provided term (after normalization)
* are returned.</p>
*
* @param term artist search term
* @return list of matching {@link ItunesDTO} objects
* @throws Exception if the HTTP request or JSON parsing fails
*/
public List<ItunesDTO> searchSongs(String term) throws Exception {

String encodedTerm = URLEncoder.encode(term, StandardCharsets.UTF_8);
String url = "https://itunes.apple.com/search?term=" + encodedTerm + "&entity=song&attribute=artistTerm&limit=20";

Expand All @@ -43,13 +68,13 @@ public List<ItunesDTO> searchSongs(String term) throws Exception {
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());

// Kontrollera status
// Validate HTTP response
if (response.statusCode() != 200) {
logger.error("searchSongs: status code {}", response.statusCode());
throw new RuntimeException("API-fel: " + response.statusCode());
throw new RuntimeException("API error: " + response.statusCode());
}

// Parse JSON
// Parse JSON response
JsonNode root = mapper.readTree(response.body());
JsonNode results = root.get("results");
if (results == null || !results.isArray()) {
Expand Down Expand Up @@ -78,6 +103,16 @@ public List<ItunesDTO> searchSongs(String term) throws Exception {
return songs;
}

/**
* Normalizes a string for comparison purposes.
*
* <p>The normalization process converts the string to lowercase,
* collapses whitespace and plus characters, and trims leading
* and trailing spaces.</p>
*
* @param s input string
* @return normalized string, or an empty string if {@code s} is {@code null}
*/
public String normalize(String s) {
if (s == null) {
return "";
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/example/ItunesDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
import java.net.URL;
import java.time.LocalDate;

/**
* Data Transfer Object representing a single song result
* returned from the iTunes Search API.
*
* <p>This record is designed to map directly to the JSON structure
* returned by the API and is used as an intermediate representation
* before converting the data into domain entities.</p>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ItunesDTO(Long artistId,
Long collectionId,
Expand All @@ -20,6 +28,11 @@ public record ItunesDTO(Long artistId,
URL artworkUrl100,
String previewUrl) {

/**
* Extracts the release year from the release date.
*
* @return release year, or {@code 0} if the release date is unknown
*/
public int releaseYear() {
return releaseDate != null ? releaseDate.getYear() : 0;
}
Expand Down
Loading