Implement trackhasgenre relationship management

Add complete implementation for track-genre relationship management,
  following the established albumhasgenre pattern. This enables tracking
  which genres are associated with each track in the media library.

  Changes:
  - Fix trackhasgenre.proto: Correct CreateTrackHasGenreRequest to use
    fk_track_id instead of fk_album_id
  - Enhance TrackHasGenre model with nullable constraints, constructor,
    and toString method
  - Implement TrackHasGenreRepository with full CRUD operations
  - Implement TrackHasGenreService with business logic and validation
  - Add TrackHasGenreMapper for entity/protobuf conversion
  - Create action handlers:
    * CreateTrackHasGenreHandler (trackhasgenre.create)
    * DeleteTrackHasGenreHandler (trackhasgenre.delete)
    * GetTrackHasGenreByIdHandler (trackhasgenre.getById)
    * GetTrackHasGenreHandler (trackhasgenre.getAll)
  - Register TrackHasGenreService in DelegateActionManager for
    automatic handler discovery and dependency injection

  The implementation validates track and genre existence before creating
  relationships and provides proper error handling with appropriate HTTP
  status codes.

  🤖 Generated with [Claude Code](https://claude.com/claude-code)

  Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Henrique Santos Souza de Miranda 2025-12-09 02:24:40 -03:00
parent a804d76905
commit e0799828d0
10 changed files with 535 additions and 0 deletions

View File

@ -0,0 +1,47 @@
package com.mediamanager.mapper;
import com.mediamanager.model.TrackHasGenre;
import com.mediamanager.protocol.messages.TrackHasGenreMessages;
public class TrackHasGenreMapper {
public static TrackHasGenreMessages.TrackHasGenre toProtobuf(TrackHasGenre entity) {
if (entity == null) {
return null;
}
TrackHasGenreMessages.TrackHasGenre.Builder builder = TrackHasGenreMessages.TrackHasGenre.newBuilder();
Integer id = entity.getId();
if (id != null) {
builder.setId(id);
}
// Map Track foreign key
if (entity.getTrack() != null && entity.getTrack().getId() != null) {
builder.setFkTrackId(entity.getTrack().getId());
}
// Map Genre foreign key
if (entity.getGenre() != null && entity.getGenre().getId() != null) {
builder.setFkGenreId(entity.getGenre().getId());
}
return builder.build();
}
public static TrackHasGenre toEntity(TrackHasGenreMessages.TrackHasGenre protobuf) {
if (protobuf == null) {
return null;
}
TrackHasGenre entity = new TrackHasGenre();
if (protobuf.getId() > 0) {
entity.setId(protobuf.getId());
}
// Note: Foreign key relationships (Track, Genre) are handled in the service layer
return entity;
}
}

View File

@ -0,0 +1,55 @@
package com.mediamanager.model;
import jakarta.persistence.*;
@Entity
@Table(name = "trackhasgenre")
public class TrackHasGenre {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_track_id", nullable = false)
private Track track;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_genre_id", nullable = false)
private Genre genre;
public TrackHasGenre() {}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Track getTrack() {
return track;
}
public void setTrack(Track track) {
this.track = track;
}
public Genre getGenre() {
return genre;
}
public void setGenre(Genre genre) {
this.genre = genre;
}
@Override
public String toString() {
return "TrackHasGenre{" +
"id=" + id +
", trackId=" + (track != null ? track.getId() : null) +
", genreId=" + (genre != null ? genre.getId() : null) +
'}';
}
}

View File

@ -0,0 +1,85 @@
package com.mediamanager.repository;
import com.mediamanager.model.TrackHasGenre;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Optional;
public class TrackHasGenreRepository {
private static final Logger logger = LogManager.getLogger(TrackHasGenreRepository.class);
private final EntityManagerFactory entityManagerFactory;
public TrackHasGenreRepository(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
public TrackHasGenre save(TrackHasGenre trackHasGenre) {
logger.debug("Saving TrackHasGenre: {}", trackHasGenre);
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
try {
em.persist(trackHasGenre);
em.getTransaction().commit();
logger.debug("TrackHasGenre has been saved successfully");
return trackHasGenre;
} catch (Exception e) {
em.getTransaction().rollback();
logger.error("Error while saving TrackHasGenre: {}", e.getMessage());
throw e;
} finally {
if (em.isOpen()) em.close();
}
}
public List<TrackHasGenre> findAll() {
logger.debug("Finding All TrackHasGenre");
EntityManager em = entityManagerFactory.createEntityManager();
try{
return em.createQuery("select t from TrackHasGenre t", TrackHasGenre.class).getResultList();
}finally {
if (em.isOpen()) em.close();
}
}
public Optional<TrackHasGenre> findById(Integer id) {
logger.debug("Finding TrackHasGenre with id: {}", id);
EntityManager em = entityManagerFactory.createEntityManager();
try{
TrackHasGenre trackHasGenre = em.find(TrackHasGenre.class, id);
return Optional.ofNullable(trackHasGenre);
}finally {
if (em.isOpen()) em.close();
}
}
public boolean deleteById(Integer id){
logger.debug("Deleting TrackHasGenre with id: {}", id);
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
try{
TrackHasGenre trackHasGenre = em.find(TrackHasGenre.class, id);
if (trackHasGenre == null) {
em.getTransaction().rollback();
return false;
}
em.remove(trackHasGenre);
em.getTransaction().commit();
logger.debug("TrackHasGenre has been deleted successfully");
return true;
} catch (Exception e) {
em.getTransaction().rollback();
logger.error("Error while deleting TrackHasGenre: {}", e.getMessage());
throw e;
} finally {
if (em.isOpen()) em.close();
}
}
}

View File

@ -12,6 +12,7 @@ import com.mediamanager.service.albumtype.AlbumTypeService;
import com.mediamanager.service.bitdepth.BitDepthService;
import com.mediamanager.service.bitrate.BitRateService;
import com.mediamanager.service.composer.ComposerService;
import com.mediamanager.service.trackhasgenre.TrackHasGenreService;
import com.mediamanager.repository.GenreRepository;
import com.mediamanager.service.artist.ArtistService;
@ -112,6 +113,10 @@ public class DelegateActionManager {
TrackService trackService = new TrackService(trackRepository, discRepository, composerRepository, bitDepthRepository, bitRateRepository, samplingRateRepository);
serviceLocator.register(TrackService.class, trackService);
TrackHasGenreRepository trackHasGenreRepository = new TrackHasGenreRepository(entityManagerFactory);
TrackHasGenreService trackHasGenreService = new TrackHasGenreService(trackHasGenreRepository, trackRepository, genreRepository);
serviceLocator.register(TrackHasGenreService.class, trackHasGenreService);
serviceLocator.logRegisteredServices();
logger.info("Services initialized successfully");

View File

@ -0,0 +1,54 @@
package com.mediamanager.service.delegate.handler.trackhasgenre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.TrackHasGenreMapper;
import com.mediamanager.model.TrackHasGenre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.TrackHasGenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.delegate.annotation.Action;
import com.mediamanager.service.trackhasgenre.TrackHasGenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Action("trackhasgenre.create")
public class CreateTrackHasGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(CreateTrackHasGenreHandler.class);
private final TrackHasGenreService trackHasGenreService;
public CreateTrackHasGenreHandler(TrackHasGenreService trackHasGenreService) {
this.trackHasGenreService = trackHasGenreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException {
try{
TrackHasGenreMessages.CreateTrackHasGenreRequest createRequest =
TrackHasGenreMessages.CreateTrackHasGenreRequest.parseFrom(requestPayload);
TrackHasGenre trackHasGenre = trackHasGenreService.createTrackHasGenre(
createRequest.getFkTrackId() > 0 ? createRequest.getFkTrackId() : null,
createRequest.getFkGenreId() > 0 ? createRequest.getFkGenreId() : null
);
TrackHasGenreMessages.TrackHasGenre trackHasGenreProto = TrackHasGenreMapper.toProtobuf(trackHasGenre);
TrackHasGenreMessages.CreateTrackHasGenreResponse createTrackHasGenreResponse = TrackHasGenreMessages.CreateTrackHasGenreResponse.newBuilder()
.setTrackhasgenre(trackHasGenreProto)
.build();
return TransportProtocol.Response.newBuilder()
.setPayload(createTrackHasGenreResponse.toByteString());
} catch (IllegalArgumentException e) {
logger.error("Validation error", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(400)
.setPayload(ByteString.copyFromUtf8("Validation error: " + e.getMessage()));
} catch (Exception e) {
logger.error("Error creating track has genre", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
}

View File

@ -0,0 +1,62 @@
package com.mediamanager.service.delegate.handler.trackhasgenre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.TrackHasGenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.delegate.annotation.Action;
import com.mediamanager.service.trackhasgenre.TrackHasGenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Action("trackhasgenre.delete")
public class DeleteTrackHasGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(DeleteTrackHasGenreHandler.class);
private final TrackHasGenreService trackHasGenreService;
public DeleteTrackHasGenreHandler(TrackHasGenreService trackHasGenreService) {
this.trackHasGenreService = trackHasGenreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException {
try {
TrackHasGenreMessages.DeleteTrackHasGenreRequest deleteRequest =
TrackHasGenreMessages.DeleteTrackHasGenreRequest.parseFrom(requestPayload);
int id = deleteRequest.getId();
boolean success = trackHasGenreService.deleteTrackHasGenre(id);
TrackHasGenreMessages.DeleteTrackHasGenreResponse deleteResponse;
if (success) {
deleteResponse = TrackHasGenreMessages.DeleteTrackHasGenreResponse.newBuilder()
.setSuccess(true)
.setMessage("Track has genre deleted successfully")
.build();
return TransportProtocol.Response.newBuilder()
.setPayload(deleteResponse.toByteString());
} else {
deleteResponse = TrackHasGenreMessages.DeleteTrackHasGenreResponse.newBuilder()
.setSuccess(false)
.setMessage("Track has genre not found")
.build();
return TransportProtocol.Response.newBuilder()
.setStatusCode(404)
.setPayload(deleteResponse.toByteString());
}
} catch (Exception e) {
logger.error("Error deleting track has genre", e);
TrackHasGenreMessages.DeleteTrackHasGenreResponse deleteResponse =
TrackHasGenreMessages.DeleteTrackHasGenreResponse.newBuilder()
.setSuccess(false)
.setMessage("Error: " + e.getMessage())
.build();
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(deleteResponse.toByteString());
}
}
}

View File

@ -0,0 +1,56 @@
package com.mediamanager.service.delegate.handler.trackhasgenre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.TrackHasGenreMapper;
import com.mediamanager.model.TrackHasGenre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.TrackHasGenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.delegate.annotation.Action;
import com.mediamanager.service.trackhasgenre.TrackHasGenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Optional;
@Action(value = "trackhasgenre.getById")
public class GetTrackHasGenreByIdHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(GetTrackHasGenreByIdHandler.class);
private final TrackHasGenreService trackHasGenreService;
public GetTrackHasGenreByIdHandler(TrackHasGenreService trackHasGenreService) {
this.trackHasGenreService = trackHasGenreService;
}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload)
throws InvalidProtocolBufferException{
try{
TrackHasGenreMessages.GetTrackHasGenreByIdRequest getByIdRequest =
TrackHasGenreMessages.GetTrackHasGenreByIdRequest.parseFrom(requestPayload);
int id = getByIdRequest.getId();
Optional<TrackHasGenre> trackHasGenreOpt = trackHasGenreService.getTrackHasGenreById(id);
if (trackHasGenreOpt.isEmpty()){
logger.warn("TrackHasGenre not found with ID: {}", id);
return TransportProtocol.Response.newBuilder()
.setStatusCode(404)
.setPayload(ByteString.copyFromUtf8("TrackHasGenre not found"));
}
TrackHasGenreMessages.TrackHasGenre trackHasGenreProto = TrackHasGenreMapper.toProtobuf(trackHasGenreOpt.get());
TrackHasGenreMessages.GetTrackHasGenreByIdResponse getByIdResponse = TrackHasGenreMessages.GetTrackHasGenreByIdResponse.newBuilder()
.setTrackhasgenre(trackHasGenreProto)
.build();
return TransportProtocol.Response.newBuilder()
.setPayload(getByIdResponse.toByteString());
} catch (Exception e) {
logger.error("Error getting track has genre by ID", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: "+ e.getMessage()));
}
}
}

View File

@ -0,0 +1,48 @@
package com.mediamanager.service.delegate.handler.trackhasgenre;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mediamanager.mapper.TrackHasGenreMapper;
import com.mediamanager.model.TrackHasGenre;
import com.mediamanager.protocol.TransportProtocol;
import com.mediamanager.protocol.messages.TrackHasGenreMessages;
import com.mediamanager.service.delegate.ActionHandler;
import com.mediamanager.service.delegate.annotation.Action;
import com.mediamanager.service.trackhasgenre.TrackHasGenreService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
@Action("trackhasgenre.getAll")
public class GetTrackHasGenreHandler implements ActionHandler {
private static final Logger logger = LogManager.getLogger(GetTrackHasGenreHandler.class);
private final TrackHasGenreService trackHasGenreService;
public GetTrackHasGenreHandler(TrackHasGenreService trackHasGenreService){this.trackHasGenreService = trackHasGenreService;}
@Override
public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException {
try{
List<TrackHasGenre> trackHasGenres = trackHasGenreService.getAllTrackHasGenres();
TrackHasGenreMessages.GetTrackHasGenresResponse.Builder responseBuilder = TrackHasGenreMessages.GetTrackHasGenresResponse.newBuilder();
for (TrackHasGenre trackHasGenre : trackHasGenres) {
TrackHasGenreMessages.TrackHasGenre trackHasGenreProto = TrackHasGenreMapper.toProtobuf(trackHasGenre);
responseBuilder.addTrackhasgenre(trackHasGenreProto);
}
TrackHasGenreMessages.GetTrackHasGenresResponse getTrackHasGenresResponse = responseBuilder.build();
return TransportProtocol.Response.newBuilder()
.setPayload(getTrackHasGenresResponse.toByteString());
}catch (Exception e){
logger.error("Error getting track has genres", e);
return TransportProtocol.Response.newBuilder()
.setStatusCode(500)
.setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage()));
}
}
}

View File

@ -0,0 +1,77 @@
package com.mediamanager.service.trackhasgenre;
import com.mediamanager.model.Track;
import com.mediamanager.model.TrackHasGenre;
import com.mediamanager.model.Genre;
import com.mediamanager.repository.TrackHasGenreRepository;
import com.mediamanager.repository.TrackRepository;
import com.mediamanager.repository.GenreRepository;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Optional;
public class TrackHasGenreService {
private static final Logger logger = LogManager.getLogger(TrackHasGenreService.class);
private final TrackHasGenreRepository repository;
private final TrackRepository trackRepository;
private final GenreRepository genreRepository;
public TrackHasGenreService(TrackHasGenreRepository repository, TrackRepository trackRepository, GenreRepository genreRepository) {
this.repository = repository;
this.trackRepository = trackRepository;
this.genreRepository = genreRepository;
}
public TrackHasGenre createTrackHasGenre(Integer trackId, Integer genreId) {
logger.debug("Creating track has genre relationship - trackId:{}, genreId:{}", trackId, genreId);
if (trackId == null || trackId <= 0) {
throw new IllegalArgumentException("Track ID cannot be null or invalid");
}
if (genreId == null || genreId <= 0) {
throw new IllegalArgumentException("Genre ID cannot be null or invalid");
}
// Verify Track exists
Optional<Track> track = trackRepository.findById(trackId);
if (track.isEmpty()) {
throw new IllegalArgumentException("Track not found with id: " + trackId);
}
// Verify Genre exists
Optional<Genre> genre = genreRepository.findById(genreId);
if (genre.isEmpty()) {
throw new IllegalArgumentException("Genre not found with id: " + genreId);
}
TrackHasGenre trackHasGenre = new TrackHasGenre();
trackHasGenre.setTrack(track.get());
trackHasGenre.setGenre(genre.get());
return repository.save(trackHasGenre);
}
public List<TrackHasGenre> getAllTrackHasGenres() {
logger.info("Getting all track has genre relationships");
return repository.findAll();
}
public Optional<TrackHasGenre> getTrackHasGenreById(Integer id) {
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
logger.info("Getting track has genre by id:{}", id);
return repository.findById(id);
}
public boolean deleteTrackHasGenre(Integer id) {
if (id == null) {
throw new IllegalArgumentException("Track has genre id cannot be null");
}
logger.info("Deleting track has genre:{}", id);
return repository.deleteById(id);
}
}

View File

@ -0,0 +1,46 @@
syntax = "proto3";
option java_package = "com.mediamanager.protocol.messages";
option java_outer_classname = "TrackHasGenreMessages";
package mediamanager.messages;
message TrackHasGenre{
int32 id = 1;
int32 fk_track_id = 2;
int32 fk_genre_id =3;
}
message CreateTrackHasGenreRequest {
int32 fk_track_id = 1;
int32 fk_genre_id = 2;
}
message CreateTrackHasGenreResponse {
TrackHasGenre trackhasgenre = 1;
}
message GetTrackHasGenresRequest {
}
message GetTrackHasGenresResponse {
repeated TrackHasGenre trackhasgenre = 1;
}
message GetTrackHasGenreByIdRequest {
int32 id = 1;
}
message GetTrackHasGenreByIdResponse {
TrackHasGenre trackhasgenre = 1;
}
message DeleteTrackHasGenreRequest {
int32 id = 1;
}
message DeleteTrackHasGenreResponse {
bool success = 1;
string message = 2;
}