From 21f5b93f48afcf62669a1ae472e0bd121ab770fd Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 7 Dec 2025 20:07:34 -0300 Subject: [PATCH 1/7] Implement Album management with full CRUD operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces comprehensive Album entity management following the established repository pattern used for other entities like AlbumType and SamplingRate. The Album entity includes relationships with AlbumType and AlbumArt entities. Key components implemented: Model Layer: - Album entity with complete field set: * Basic fields: id, name, year, numberOfDiscs, code, isCompilation * Foreign key relationships: AlbumType and AlbumArt (ManyToOne) * Audit timestamps: createdAt, updatedAt with JPA lifecycle hooks - JPA annotations with @PrePersist and @PreUpdate for automatic timestamps - Complete getters and setters Repository Layer: - AlbumRepository with EntityManager-based operations - Full transaction management with proper rollback handling - Methods: save, findAll, findById, update, deleteById Service Layer: - AlbumService with business logic and validation - Constructor injection of AlbumRepository, AlbumTypeRepository, AlbumArtRepository - Relationship handling: validates and sets AlbumType and AlbumArt entities - Input validation for null/empty name field - ID validation for all operations requiring entity lookup - Comprehensive logging using Log4j2 Mapper Layer: - AlbumMapper for bidirectional entity/protobuf conversion - Timestamp conversion between LocalDateTime and epoch milliseconds - Foreign key mapping for AlbumType and AlbumArt relationships - Null safety checks and validation - Proper handling of optional fields Action Handlers: - CreateAlbumHandler (album.create) - GetAlbumHandler (album.getAll) - GetAlbumByIdHandler (album.getById) - UpdateAlbumHandler (album.update) - DeleteAlbumHandler (album.delete) - HTTP status code handling: 200 (success), 400 (validation), 404 (not found), 500 (server error) - Handles optional fields with proper default value checks Protocol Buffers: - Fixed proto definition with correct Album fields - CreateAlbumRequest and UpdateAlbumRequest with all entity fields - All CRUD message definitions (Create, Get, GetById, Update, Delete) - Support for foreign keys and timestamps Service Registration: - AlbumRepository initialized with EntityManagerFactory - AlbumService registered with ServiceLocator with required dependencies - Ensures all Album action handlers can resolve dependencies The implementation follows best practices with proper error handling, logging, validation, relationship management, and consistency with existing codebase patterns. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../com/mediamanager/mapper/AlbumMapper.java | 110 +++++++++++++ .../java/com/mediamanager/model/Album.java | 145 ++++++++++++++++++ .../repository/AlbumRepository.java | 103 +++++++++++++ .../service/album/AlbumService.java | 130 ++++++++++++++++ .../delegate/DelegateActionManager.java | 5 + .../handler/album/CreateAlbumHandler.java | 59 +++++++ .../handler/album/DeleteAlbumHandler.java | 62 ++++++++ .../handler/album/GetAlbumByIdHandler.java | 56 +++++++ .../handler/album/GetAlbumHandler.java | 48 ++++++ .../handler/album/UpdateAlbumHandler.java | 72 +++++++++ src/main/proto/album.proto | 77 ++++++++++ 11 files changed, 867 insertions(+) create mode 100644 src/main/java/com/mediamanager/mapper/AlbumMapper.java create mode 100644 src/main/java/com/mediamanager/model/Album.java create mode 100644 src/main/java/com/mediamanager/repository/AlbumRepository.java create mode 100644 src/main/java/com/mediamanager/service/album/AlbumService.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/album/CreateAlbumHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/album/DeleteAlbumHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumByIdHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java create mode 100644 src/main/proto/album.proto diff --git a/src/main/java/com/mediamanager/mapper/AlbumMapper.java b/src/main/java/com/mediamanager/mapper/AlbumMapper.java new file mode 100644 index 0000000..57ff6bd --- /dev/null +++ b/src/main/java/com/mediamanager/mapper/AlbumMapper.java @@ -0,0 +1,110 @@ +package com.mediamanager.mapper; + +import com.mediamanager.model.Album; +import com.mediamanager.protocol.messages.AlbumMessages; + +import java.time.Instant; +import java.time.ZoneId; + +public class AlbumMapper { + public static AlbumMessages.Album toProtobuf(Album entity) { + if (entity == null) { + return null; + } + + String name = entity.getName(); + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + + AlbumMessages.Album.Builder builder = AlbumMessages.Album.newBuilder() + .setName(name); + + Integer id = entity.getId(); + if (id != null) { + builder.setId(id); + } + + Integer year = entity.getYear(); + if (year != null) { + builder.setYear(year); + } + + Integer numberOfDiscs = entity.getNumberOfDiscs(); + if (numberOfDiscs != null) { + builder.setNumberOfDiscs(numberOfDiscs); + } + + String code = entity.getCode(); + if (code != null) { + builder.setCode(code); + } + + Boolean isCompilation = entity.getIsCompilation(); + if (isCompilation != null) { + builder.setIsCompilation(isCompilation); + } + + // Map AlbumType foreign key + if (entity.getAlbumType() != null && entity.getAlbumType().getId() != null) { + builder.setFkAlbumtypeId(entity.getAlbumType().getId()); + } + + // Map AlbumArt foreign key + if (entity.getAlbumArt() != null && entity.getAlbumArt().getId() != null) { + builder.setFkAlbumartId(entity.getAlbumArt().getId()); + } + + // Map timestamps + if (entity.getCreatedAt() != null) { + long createdAtEpoch = entity.getCreatedAt() + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + builder.setCreatedAt(createdAtEpoch); + } + + if (entity.getUpdatedAt() != null) { + long updatedAtEpoch = entity.getUpdatedAt() + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + builder.setUpdatedAt(updatedAtEpoch); + } + + return builder.build(); + } + + public static Album toEntity(AlbumMessages.Album protobuf) { + if (protobuf == null) { + return null; + } + + Album entity = new Album(); + + if (protobuf.getId() > 0) { + entity.setId(protobuf.getId()); + } + + entity.setName(protobuf.getName()); + + if (protobuf.getYear() > 0) { + entity.setYear(protobuf.getYear()); + } + + if (protobuf.getNumberOfDiscs() > 0) { + entity.setNumberOfDiscs(protobuf.getNumberOfDiscs()); + } + + if (!protobuf.getCode().isEmpty()) { + entity.setCode(protobuf.getCode()); + } + + entity.setIsCompilation(protobuf.getIsCompilation()); + + // Note: Foreign key relationships (AlbumType, AlbumArt) are handled in the service layer + // Timestamps are managed by JPA @PrePersist and @PreUpdate + + return entity; + } +} diff --git a/src/main/java/com/mediamanager/model/Album.java b/src/main/java/com/mediamanager/model/Album.java new file mode 100644 index 0000000..6a740fc --- /dev/null +++ b/src/main/java/com/mediamanager/model/Album.java @@ -0,0 +1,145 @@ +package com.mediamanager.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "album") +public class Album { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private String name; + + @Column + private Integer year; + + @Column(name = "number_of_discs") + private Integer numberOfDiscs; + + @Column + private String code; + + @Column(name = "is_compilation") + private Boolean isCompilation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fk_albumtype_id") + private AlbumType albumType; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fk_albumart_id") + private AlbumArt albumArt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + public Album() {} + + // Getters and Setters + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getNumberOfDiscs() { + return numberOfDiscs; + } + + public void setNumberOfDiscs(Integer numberOfDiscs) { + this.numberOfDiscs = numberOfDiscs; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public Boolean getIsCompilation() { + return isCompilation; + } + + public void setIsCompilation(Boolean isCompilation) { + this.isCompilation = isCompilation; + } + + public AlbumType getAlbumType() { + return albumType; + } + + public void setAlbumType(AlbumType albumType) { + this.albumType = albumType; + } + + public AlbumArt getAlbumArt() { + return albumArt; + } + + public void setAlbumArt(AlbumArt albumArt) { + this.albumArt = albumArt; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public String toString() { + return "Album{" + + "id=" + id + + ", name='" + name + '\'' + + ", year=" + year + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/mediamanager/repository/AlbumRepository.java b/src/main/java/com/mediamanager/repository/AlbumRepository.java new file mode 100644 index 0000000..f744222 --- /dev/null +++ b/src/main/java/com/mediamanager/repository/AlbumRepository.java @@ -0,0 +1,103 @@ +package com.mediamanager.repository; + + +import com.mediamanager.model.Album; +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 AlbumRepository { + private static final Logger logger = LogManager.getLogger(AlbumRepository.class); + + private final EntityManagerFactory entityManagerFactory; + + public AlbumRepository(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + public Album save(Album album) { + logger.debug("Saving Album: {}", album.getName()); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try { + em.persist(album); + em.getTransaction().commit(); + logger.debug("Album has been saved successfully"); + return album; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while saving Album: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + + public List findAll() { + logger.debug("Finding All Album"); + EntityManager em = entityManagerFactory.createEntityManager(); + try{ + return em.createQuery("select a from Album a", Album.class).getResultList(); + }finally { + if (em.isOpen()) em.close(); + } + } + + public Optional findById(Integer id) { + logger.debug("Finding Album with id: {}", id); + EntityManager em = entityManagerFactory.createEntityManager(); + try{ + Album album = em.find(Album.class, id); + return Optional.ofNullable(album); + }finally { + if (em.isOpen()) em.close(); + } + } + + public Album update(Album album) { + logger.debug("Updating Album: {}", album.getName()); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try { + Album updated = em.merge(album); + em.getTransaction().commit(); + logger.debug("Album has been updated successfully"); + return updated; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while updating Album: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + + public boolean deleteById(Integer id){ + logger.debug("Deleting Album with id: {}", id); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try{ + Album album = em.find(Album.class, id); + if (album == null) { + em.getTransaction().rollback(); + return false; + } + em.remove(album); + em.getTransaction().commit(); + logger.debug("Album has been deleted successfully"); + return true; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while deleting Album: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + +} diff --git a/src/main/java/com/mediamanager/service/album/AlbumService.java b/src/main/java/com/mediamanager/service/album/AlbumService.java new file mode 100644 index 0000000..82d1664 --- /dev/null +++ b/src/main/java/com/mediamanager/service/album/AlbumService.java @@ -0,0 +1,130 @@ +package com.mediamanager.service.album; + +import com.mediamanager.model.Album; +import com.mediamanager.model.AlbumArt; +import com.mediamanager.model.AlbumType; +import com.mediamanager.repository.AlbumRepository; +import com.mediamanager.repository.AlbumArtRepository; +import com.mediamanager.repository.AlbumTypeRepository; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; +import java.util.Optional; + +public class AlbumService { + private static final Logger logger = LogManager.getLogger(AlbumService.class); + private final AlbumRepository repository; + private final AlbumTypeRepository albumTypeRepository; + private final AlbumArtRepository albumArtRepository; + + public AlbumService(AlbumRepository repository, AlbumTypeRepository albumTypeRepository, AlbumArtRepository albumArtRepository) { + this.repository = repository; + this.albumTypeRepository = albumTypeRepository; + this.albumArtRepository = albumArtRepository; + } + + public Album createAlbum(String name, Integer year, Integer numberOfDiscs, String code, Boolean isCompilation, Integer albumTypeId, Integer albumArtId) { + logger.debug("Creating album:{}", name); + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Album name cannot be null or empty"); + } + + Album album = new Album(); + album.setName(name); + album.setYear(year); + album.setNumberOfDiscs(numberOfDiscs); + album.setCode(code); + album.setIsCompilation(isCompilation); + + // Set AlbumType if provided + if (albumTypeId != null && albumTypeId > 0) { + Optional albumType = albumTypeRepository.findById(albumTypeId); + if (albumType.isEmpty()) { + throw new IllegalArgumentException("AlbumType not found with id: " + albumTypeId); + } + album.setAlbumType(albumType.get()); + } + + // Set AlbumArt if provided + if (albumArtId != null && albumArtId > 0) { + Optional albumArt = albumArtRepository.findById(albumArtId); + if (albumArt.isEmpty()) { + throw new IllegalArgumentException("AlbumArt not found with id: " + albumArtId); + } + album.setAlbumArt(albumArt.get()); + } + + return repository.save(album); + } + + public List getAllAlbums() { + logger.info("Getting all albums"); + return repository.findAll(); + } + + public Optional getAlbumById(Integer id) { + if (id == null) { + throw new IllegalArgumentException("ID cannot be null"); + } + logger.info("Getting album by id:{}", id); + return repository.findById(id); + } + + public Optional updateAlbum(Integer id, String name, Integer year, Integer numberOfDiscs, String code, Boolean isCompilation, Integer albumTypeId, Integer albumArtId) { + if (id == null) { + throw new IllegalArgumentException("ID cannot be null"); + } + logger.info("Updating album:{}", name); + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Album name cannot be null or empty"); + } + + Optional existingAlbum = repository.findById(id); + if(existingAlbum.isEmpty()) { + logger.warn("Album not found with id:{}", id); + return Optional.empty(); + } + + Album album = existingAlbum.get(); + album.setName(name); + album.setYear(year); + album.setNumberOfDiscs(numberOfDiscs); + album.setCode(code); + album.setIsCompilation(isCompilation); + + // Update AlbumType if provided + if (albumTypeId != null && albumTypeId > 0) { + Optional albumType = albumTypeRepository.findById(albumTypeId); + if (albumType.isEmpty()) { + throw new IllegalArgumentException("AlbumType not found with id: " + albumTypeId); + } + album.setAlbumType(albumType.get()); + } else { + album.setAlbumType(null); + } + + // Update AlbumArt if provided + if (albumArtId != null && albumArtId > 0) { + Optional albumArt = albumArtRepository.findById(albumArtId); + if (albumArt.isEmpty()) { + throw new IllegalArgumentException("AlbumArt not found with id: " + albumArtId); + } + album.setAlbumArt(albumArt.get()); + } else { + album.setAlbumArt(null); + } + + Album updatedAlbum = repository.update(album); + return Optional.of(updatedAlbum); + } + + public boolean deleteAlbum(Integer id) { + if (id == null) { + throw new IllegalArgumentException("Album id cannot be null"); + } + logger.info("Deleting album:{}", id); + return repository.deleteById(id); + } + +} diff --git a/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java b/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java index a001eaa..c8a7251 100644 --- a/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java +++ b/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java @@ -3,6 +3,7 @@ package com.mediamanager.service.delegate; import com.google.protobuf.ByteString; import com.mediamanager.protocol.TransportProtocol; import com.mediamanager.repository.*; +import com.mediamanager.service.album.AlbumService; import com.mediamanager.service.albumart.AlbumArtService; import com.mediamanager.service.albumtype.AlbumTypeService; import com.mediamanager.service.bitdepth.BitDepthService; @@ -85,6 +86,10 @@ public class DelegateActionManager { AlbumTypeService albumTypeService = new AlbumTypeService(albumTypeRepository); serviceLocator.register(AlbumTypeService.class, albumTypeService); + AlbumRepository albumRepository = new AlbumRepository(entityManagerFactory); + AlbumService albumService = new AlbumService(albumRepository, albumTypeRepository, albumArtRepository); + serviceLocator.register(AlbumService.class, albumService); + serviceLocator.logRegisteredServices(); logger.info("Services initialized successfully"); diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/CreateAlbumHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/CreateAlbumHandler.java new file mode 100644 index 0000000..a7c3d75 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/CreateAlbumHandler.java @@ -0,0 +1,59 @@ +package com.mediamanager.service.delegate.handler.album; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumMapper; +import com.mediamanager.model.Album; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.album.AlbumService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Action("album.create") +public class CreateAlbumHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(CreateAlbumHandler.class); + private final AlbumService albumService; + + public CreateAlbumHandler(AlbumService albumService) { + this.albumService = albumService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + AlbumMessages.CreateAlbumRequest createRequest = + AlbumMessages.CreateAlbumRequest.parseFrom(requestPayload); + + Album album = albumService.createAlbum( + createRequest.getName(), + createRequest.getYear() > 0 ? createRequest.getYear() : null, + createRequest.getNumberOfDiscs() > 0 ? createRequest.getNumberOfDiscs() : null, + createRequest.getCode().isEmpty() ? null : createRequest.getCode(), + createRequest.getIsCompilation(), + createRequest.getFkAlbumtypeId() > 0 ? createRequest.getFkAlbumtypeId() : null, + createRequest.getFkAlbumartId() > 0 ? createRequest.getFkAlbumartId() : null + ); + + AlbumMessages.Album albumProto = AlbumMapper.toProtobuf(album); + AlbumMessages.CreateAlbumResponse createAlbumResponse = AlbumMessages.CreateAlbumResponse.newBuilder() + .setAlbum(albumProto) + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(createAlbumResponse.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 album", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/DeleteAlbumHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/DeleteAlbumHandler.java new file mode 100644 index 0000000..101325c --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/DeleteAlbumHandler.java @@ -0,0 +1,62 @@ +package com.mediamanager.service.delegate.handler.album; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.album.AlbumService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Action("album.delete") +public class DeleteAlbumHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(DeleteAlbumHandler.class); + + private final AlbumService albumService; + + public DeleteAlbumHandler(AlbumService albumService) { + this.albumService = albumService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException { + + try { + AlbumMessages.DeleteAlbumRequest deleteRequest = + AlbumMessages.DeleteAlbumRequest.parseFrom(requestPayload); + int id = deleteRequest.getId(); + boolean success = albumService.deleteAlbum(id); + AlbumMessages.DeleteAlbumResponse deleteResponse; + if (success) { + deleteResponse = AlbumMessages.DeleteAlbumResponse.newBuilder() + .setSuccess(true) + .setMessage("Album deleted successfully") + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(deleteResponse.toByteString()); + } else { + deleteResponse = AlbumMessages.DeleteAlbumResponse.newBuilder() + .setSuccess(false) + .setMessage("Album not found") + .build(); + + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(deleteResponse.toByteString()); + } + } catch (Exception e) { + logger.error("Error deleting album", e); + AlbumMessages.DeleteAlbumResponse deleteResponse = + AlbumMessages.DeleteAlbumResponse.newBuilder() + .setSuccess(false) + .setMessage("Error: " + e.getMessage()) + .build(); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(deleteResponse.toByteString()); + } + } + } diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumByIdHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumByIdHandler.java new file mode 100644 index 0000000..97d2814 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumByIdHandler.java @@ -0,0 +1,56 @@ +package com.mediamanager.service.delegate.handler.album; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumMapper; +import com.mediamanager.model.Album; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.album.AlbumService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +@Action(value = "album.getById") +public class GetAlbumByIdHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(GetAlbumByIdHandler.class); + private final AlbumService albumService; + + public GetAlbumByIdHandler(AlbumService albumService) { + this.albumService = albumService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException{ + + try{ + AlbumMessages.GetAlbumByIdRequest getByIdRequest = + AlbumMessages.GetAlbumByIdRequest.parseFrom(requestPayload); + int id = getByIdRequest.getId(); + + Optional albumOpt = albumService.getAlbumById(id); + + if (albumOpt.isEmpty()){ + logger.warn("Album not found with ID: {}", id); + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(ByteString.copyFromUtf8("Album not found")); + } + AlbumMessages.Album albumProto = AlbumMapper.toProtobuf(albumOpt.get()); + AlbumMessages.GetAlbumByIdResponse getByIdResponse = AlbumMessages.GetAlbumByIdResponse.newBuilder() + .setAlbum(albumProto) + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(getByIdResponse.toByteString()); + } catch (Exception e) { + logger.error("Error getting album by ID", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: "+ e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumHandler.java new file mode 100644 index 0000000..9d388f3 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/GetAlbumHandler.java @@ -0,0 +1,48 @@ +package com.mediamanager.service.delegate.handler.album; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumMapper; +import com.mediamanager.model.Album; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.album.AlbumService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; + + +@Action("album.getAll") +public class GetAlbumHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(GetAlbumHandler.class); + + private final AlbumService albumService; + + public GetAlbumHandler(AlbumService albumService){this.albumService = albumService;} + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + List albums = albumService.getAllAlbums(); + AlbumMessages.GetAlbumsResponse.Builder responseBuilder = AlbumMessages.GetAlbumsResponse.newBuilder(); + + for (Album album : albums) { + AlbumMessages.Album albumProto = AlbumMapper.toProtobuf(album); + responseBuilder.addAlbums(albumProto); + } + AlbumMessages.GetAlbumsResponse getAlbumsResponse = responseBuilder.build(); + + return TransportProtocol.Response.newBuilder() + .setPayload(getAlbumsResponse.toByteString()); + + }catch (Exception e){ + logger.error("Error getting albums", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java new file mode 100644 index 0000000..ac10f55 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java @@ -0,0 +1,72 @@ +package com.mediamanager.service.delegate.handler.album; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumMapper; +import com.mediamanager.model.Album; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.album.AlbumService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +@Action("album.update") +public class UpdateAlbumHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(UpdateAlbumHandler.class); + private final AlbumService albumService; + + public UpdateAlbumHandler(AlbumService albumService) { + this.albumService = albumService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + AlbumMessages.UpdateAlbumRequest updateRequest = + AlbumMessages.UpdateAlbumRequest.parseFrom(requestPayload); + + int id = updateRequest.getId(); + Optional albumOpt = albumService.updateAlbum( + id, + updateRequest.getName(), + updateRequest.getYear() > 0 ? updateRequest.getYear() : null, + updateRequest.getNumberOfDiscs() > 0 ? updateRequest.getNumberOfDiscs() : null, + updateRequest.getCode().isEmpty() ? null : updateRequest.getCode(), + updateRequest.getIsCompilation(), + updateRequest.getFkAlbumtypeId() > 0 ? updateRequest.getFkAlbumtypeId() : null, + updateRequest.getFkAlbumartId() > 0 ? updateRequest.getFkAlbumartId() : null + ); + + if(albumOpt.isEmpty()){ + logger.warn("Album not found with ID: {}", id); + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(ByteString.copyFromUtf8("Album not found")); + } + + AlbumMessages.Album albumProto = AlbumMapper.toProtobuf(albumOpt.get()); + + AlbumMessages.UpdateAlbumResponse updateResponse = AlbumMessages.UpdateAlbumResponse.newBuilder() + .setAlbum(albumProto) + .build(); + + return TransportProtocol.Response.newBuilder() + .setPayload(updateResponse.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 updating album", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/proto/album.proto b/src/main/proto/album.proto new file mode 100644 index 0000000..44e876e --- /dev/null +++ b/src/main/proto/album.proto @@ -0,0 +1,77 @@ +syntax = "proto3"; + +option java_package = "com.mediamanager.protocol.messages"; +option java_outer_classname = "AlbumMessages"; + +package mediamanager.messages; + +message Album { + int32 id = 1; + string name = 2; + int32 year = 3; + int32 number_of_discs = 4; + string code = 5; + bool is_compilation = 6; + + + int32 fk_albumtype_id = 7; + int32 fk_albumart_id = 8; + + + int64 created_at = 9; + int64 updated_at = 10; +} + +message CreateAlbumRequest { + string name = 1; + int32 year = 2; + int32 number_of_discs = 3; + string code = 4; + bool is_compilation = 5; + int32 fk_albumtype_id = 6; + int32 fk_albumart_id = 7; +} + +message CreateAlbumResponse { + Album album = 1; +} + +message GetAlbumsRequest { + +} + +message GetAlbumsResponse { + repeated Album albums = 1; +} + +message GetAlbumByIdRequest { + int32 id = 1; +} + +message GetAlbumByIdResponse { + Album album = 1; +} + +message UpdateAlbumRequest { + int32 id = 1; + string name = 2; + int32 year = 3; + int32 number_of_discs = 4; + string code = 5; + bool is_compilation = 6; + int32 fk_albumtype_id = 7; + int32 fk_albumart_id = 8; +} + +message UpdateAlbumResponse { + Album album = 1; +} + +message DeleteAlbumRequest { + int32 id = 1; +} + +message DeleteAlbumResponse { + bool success = 1; + string message = 2; +} From f31c657d61d0371c2c77905a04198c9a70463293 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 7 Dec 2025 21:03:20 -0300 Subject: [PATCH 2/7] Implement AlbumHasArtist relationship management with CRUD operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the AlbumHasArtist join table entity management, establishing the many-to-many relationship between Album and Artist entities. This implementation follows the established repository pattern used for other entities. Key components implemented: Model Layer: - AlbumHasArtist entity representing the join table - ManyToOne relationships to Album and Artist entities - JPA annotations with proper foreign key constraints - Complete getters and setters - Custom toString method for debugging Repository Layer: - AlbumHasArtistRepository with EntityManager-based operations - Full transaction management with proper rollback handling - Methods: save, findAll, findById, deleteById - No update method (join tables typically only need create/delete) Service Layer: - AlbumHasArtistService with business logic and validation - Constructor injection of AlbumHasArtistRepository, AlbumRepository, ArtistRepository - Relationship validation: ensures both Album and Artist exist before creating association - Input validation for null/invalid IDs - ID validation for all operations requiring entity lookup - Comprehensive logging using Log4j2 Mapper Layer: - AlbumHasArtistMapper for bidirectional entity/protobuf conversion - Foreign key mapping for Album and Artist relationships - Null safety checks and validation - Proper handling of optional ID field Action Handlers: - CreateAlbumHasArtistHandler (albumhasartist.create) - GetAlbumHasArtistHandler (albumhasartist.getAll) - GetAlbumHasArtistByIdHandler (albumhasartist.getById) - DeleteAlbumHasArtistHandler (albumhasartist.delete) - HTTP status code handling: 200 (success), 400 (validation), 404 (not found), 500 (server error) - No update handler as join tables typically only require create/delete operations Protocol Buffers: - Complete proto definition with AlbumHasArtistMessages - Messages support album_id and artist_id foreign keys - CRUD message definitions (Create, Get, GetById, Delete) - No Update messages as per join table requirements Service Registration: - AlbumHasArtistRepository initialized with EntityManagerFactory - AlbumHasArtistService registered with ServiceLocator with required dependencies - Ensures all AlbumHasArtist action handlers can resolve dependencies - Proper dependency injection of AlbumRepository and ArtistRepository The implementation follows best practices with proper error handling, logging, validation, relationship integrity checks, and consistency with existing codebase patterns. This enables proper many-to-many relationship management between albums and artists. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../mapper/AlbumHasArtistMapper.java | 47 ++++++++++ .../mediamanager/model/AlbumHasArtist.java | 56 ++++++++++++ .../repository/AlbumHasArtistRepository.java | 85 +++++++++++++++++++ .../albumhasartist/AlbumHasArtistService.java | 77 +++++++++++++++++ .../delegate/DelegateActionManager.java | 5 ++ .../CreateAlbumHasArtistHandler.java | 54 ++++++++++++ .../DeleteAlbumHasArtistHandler.java | 62 ++++++++++++++ .../GetAlbumHasArtistByIdHandler.java | 56 ++++++++++++ .../GetAlbumHasArtistHandler.java | 48 +++++++++++ src/main/proto/albumhasartist.proto | 46 ++++++++++ 10 files changed, 536 insertions(+) create mode 100644 src/main/java/com/mediamanager/mapper/AlbumHasArtistMapper.java create mode 100644 src/main/java/com/mediamanager/model/AlbumHasArtist.java create mode 100644 src/main/java/com/mediamanager/repository/AlbumHasArtistRepository.java create mode 100644 src/main/java/com/mediamanager/service/albumhasartist/AlbumHasArtistService.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/CreateAlbumHasArtistHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/DeleteAlbumHasArtistHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistByIdHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistHandler.java create mode 100644 src/main/proto/albumhasartist.proto diff --git a/src/main/java/com/mediamanager/mapper/AlbumHasArtistMapper.java b/src/main/java/com/mediamanager/mapper/AlbumHasArtistMapper.java new file mode 100644 index 0000000..58bf3ca --- /dev/null +++ b/src/main/java/com/mediamanager/mapper/AlbumHasArtistMapper.java @@ -0,0 +1,47 @@ +package com.mediamanager.mapper; + +import com.mediamanager.model.AlbumHasArtist; +import com.mediamanager.protocol.messages.AlbumHasArtistMessages; + +public class AlbumHasArtistMapper { + public static AlbumHasArtistMessages.AlbumHasArtist toProtobuf(AlbumHasArtist entity) { + if (entity == null) { + return null; + } + + AlbumHasArtistMessages.AlbumHasArtist.Builder builder = AlbumHasArtistMessages.AlbumHasArtist.newBuilder(); + + Integer id = entity.getId(); + if (id != null) { + builder.setId(id); + } + + // Map Album foreign key + if (entity.getAlbum() != null && entity.getAlbum().getId() != null) { + builder.setFkAlbumId(entity.getAlbum().getId()); + } + + // Map Artist foreign key + if (entity.getArtist() != null && entity.getArtist().getId() != null) { + builder.setFkArtistId(entity.getArtist().getId()); + } + + return builder.build(); + } + + public static AlbumHasArtist toEntity(AlbumHasArtistMessages.AlbumHasArtist protobuf) { + if (protobuf == null) { + return null; + } + + AlbumHasArtist entity = new AlbumHasArtist(); + + if (protobuf.getId() > 0) { + entity.setId(protobuf.getId()); + } + + // Note: Foreign key relationships (Album, Artist) are handled in the service layer + + return entity; + } +} diff --git a/src/main/java/com/mediamanager/model/AlbumHasArtist.java b/src/main/java/com/mediamanager/model/AlbumHasArtist.java new file mode 100644 index 0000000..aa04587 --- /dev/null +++ b/src/main/java/com/mediamanager/model/AlbumHasArtist.java @@ -0,0 +1,56 @@ +package com.mediamanager.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "albumshasartist") +public class AlbumHasArtist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fk_album_id", nullable = false) + private Album album; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fk_artist_id", nullable = false) + private Artist artist; + + public AlbumHasArtist() {} + + // Getters and Setters + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + public Artist getArtist() { + return artist; + } + + public void setArtist(Artist artist) { + this.artist = artist; + } + + @Override + public String toString() { + return "AlbumHasArtist{" + + "id=" + id + + ", albumId=" + (album != null ? album.getId() : null) + + ", artistId=" + (artist != null ? artist.getId() : null) + + '}'; + } +} diff --git a/src/main/java/com/mediamanager/repository/AlbumHasArtistRepository.java b/src/main/java/com/mediamanager/repository/AlbumHasArtistRepository.java new file mode 100644 index 0000000..f57140c --- /dev/null +++ b/src/main/java/com/mediamanager/repository/AlbumHasArtistRepository.java @@ -0,0 +1,85 @@ +package com.mediamanager.repository; + + +import com.mediamanager.model.AlbumHasArtist; +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 AlbumHasArtistRepository { + private static final Logger logger = LogManager.getLogger(AlbumHasArtistRepository.class); + + private final EntityManagerFactory entityManagerFactory; + + public AlbumHasArtistRepository(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + public AlbumHasArtist save(AlbumHasArtist albumHasArtist) { + logger.debug("Saving AlbumHasArtist: {}", albumHasArtist); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try { + em.persist(albumHasArtist); + em.getTransaction().commit(); + logger.debug("AlbumHasArtist has been saved successfully"); + return albumHasArtist; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while saving AlbumHasArtist: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + + public List findAll() { + logger.debug("Finding All AlbumHasArtist"); + EntityManager em = entityManagerFactory.createEntityManager(); + try{ + return em.createQuery("select a from AlbumHasArtist a", AlbumHasArtist.class).getResultList(); + }finally { + if (em.isOpen()) em.close(); + } + } + + public Optional findById(Integer id) { + logger.debug("Finding AlbumHasArtist with id: {}", id); + EntityManager em = entityManagerFactory.createEntityManager(); + try{ + AlbumHasArtist albumHasArtist = em.find(AlbumHasArtist.class, id); + return Optional.ofNullable(albumHasArtist); + }finally { + if (em.isOpen()) em.close(); + } + } + + public boolean deleteById(Integer id){ + logger.debug("Deleting AlbumHasArtist with id: {}", id); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try{ + AlbumHasArtist albumHasArtist = em.find(AlbumHasArtist.class, id); + if (albumHasArtist == null) { + em.getTransaction().rollback(); + return false; + } + em.remove(albumHasArtist); + em.getTransaction().commit(); + logger.debug("AlbumHasArtist has been deleted successfully"); + return true; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while deleting AlbumHasArtist: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + +} diff --git a/src/main/java/com/mediamanager/service/albumhasartist/AlbumHasArtistService.java b/src/main/java/com/mediamanager/service/albumhasartist/AlbumHasArtistService.java new file mode 100644 index 0000000..3687d2a --- /dev/null +++ b/src/main/java/com/mediamanager/service/albumhasartist/AlbumHasArtistService.java @@ -0,0 +1,77 @@ +package com.mediamanager.service.albumhasartist; + +import com.mediamanager.model.Album; +import com.mediamanager.model.AlbumHasArtist; +import com.mediamanager.model.Artist; +import com.mediamanager.repository.AlbumHasArtistRepository; +import com.mediamanager.repository.AlbumRepository; +import com.mediamanager.repository.ArtistRepository; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; +import java.util.Optional; + +public class AlbumHasArtistService { + private static final Logger logger = LogManager.getLogger(AlbumHasArtistService.class); + private final AlbumHasArtistRepository repository; + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + + public AlbumHasArtistService(AlbumHasArtistRepository repository, AlbumRepository albumRepository, ArtistRepository artistRepository) { + this.repository = repository; + this.albumRepository = albumRepository; + this.artistRepository = artistRepository; + } + + public AlbumHasArtist createAlbumHasArtist(Integer albumId, Integer artistId) { + logger.debug("Creating album has artist relationship - albumId:{}, artistId:{}", albumId, artistId); + + if (albumId == null || albumId <= 0) { + throw new IllegalArgumentException("Album ID cannot be null or invalid"); + } + if (artistId == null || artistId <= 0) { + throw new IllegalArgumentException("Artist ID cannot be null or invalid"); + } + + // Verify Album exists + Optional album = albumRepository.findById(albumId); + if (album.isEmpty()) { + throw new IllegalArgumentException("Album not found with id: " + albumId); + } + + // Verify Artist exists + Optional artist = artistRepository.findById(artistId); + if (artist.isEmpty()) { + throw new IllegalArgumentException("Artist not found with id: " + artistId); + } + + AlbumHasArtist albumHasArtist = new AlbumHasArtist(); + albumHasArtist.setAlbum(album.get()); + albumHasArtist.setArtist(artist.get()); + + return repository.save(albumHasArtist); + } + + public List getAllAlbumHasArtists() { + logger.info("Getting all album has artist relationships"); + return repository.findAll(); + } + + public Optional getAlbumHasArtistById(Integer id) { + if (id == null) { + throw new IllegalArgumentException("ID cannot be null"); + } + logger.info("Getting album has artist by id:{}", id); + return repository.findById(id); + } + + public boolean deleteAlbumHasArtist(Integer id) { + if (id == null) { + throw new IllegalArgumentException("Album has artist id cannot be null"); + } + logger.info("Deleting album has artist:{}", id); + return repository.deleteById(id); + } + +} diff --git a/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java b/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java index c8a7251..8fca373 100644 --- a/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java +++ b/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java @@ -5,6 +5,7 @@ import com.mediamanager.protocol.TransportProtocol; import com.mediamanager.repository.*; import com.mediamanager.service.album.AlbumService; import com.mediamanager.service.albumart.AlbumArtService; +import com.mediamanager.service.albumhasartist.AlbumHasArtistService; import com.mediamanager.service.albumtype.AlbumTypeService; import com.mediamanager.service.bitdepth.BitDepthService; import com.mediamanager.service.bitrate.BitRateService; @@ -90,6 +91,10 @@ public class DelegateActionManager { AlbumService albumService = new AlbumService(albumRepository, albumTypeRepository, albumArtRepository); serviceLocator.register(AlbumService.class, albumService); + AlbumHasArtistRepository albumHasArtistRepository = new AlbumHasArtistRepository(entityManagerFactory); + AlbumHasArtistService albumHasArtistService = new AlbumHasArtistService(albumHasArtistRepository, albumRepository, artistRepository); + serviceLocator.register(AlbumHasArtistService.class, albumHasArtistService); + serviceLocator.logRegisteredServices(); logger.info("Services initialized successfully"); diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/CreateAlbumHasArtistHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/CreateAlbumHasArtistHandler.java new file mode 100644 index 0000000..07c65be --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/CreateAlbumHasArtistHandler.java @@ -0,0 +1,54 @@ +package com.mediamanager.service.delegate.handler.albumhasartist; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumHasArtistMapper; +import com.mediamanager.model.AlbumHasArtist; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasArtistMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasartist.AlbumHasArtistService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Action("albumhasartist.create") +public class CreateAlbumHasArtistHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(CreateAlbumHasArtistHandler.class); + private final AlbumHasArtistService albumHasArtistService; + + public CreateAlbumHasArtistHandler(AlbumHasArtistService albumHasArtistService) { + this.albumHasArtistService = albumHasArtistService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + AlbumHasArtistMessages.CreateAlbumHasArtistRequest createRequest = + AlbumHasArtistMessages.CreateAlbumHasArtistRequest.parseFrom(requestPayload); + + AlbumHasArtist albumHasArtist = albumHasArtistService.createAlbumHasArtist( + createRequest.getFkAlbumId() > 0 ? createRequest.getFkAlbumId() : null, + createRequest.getFkArtistId() > 0 ? createRequest.getFkArtistId() : null + ); + + AlbumHasArtistMessages.AlbumHasArtist albumHasArtistProto = AlbumHasArtistMapper.toProtobuf(albumHasArtist); + AlbumHasArtistMessages.CreateAlbumHasArtistResponse createAlbumHasArtistResponse = AlbumHasArtistMessages.CreateAlbumHasArtistResponse.newBuilder() + .setAlbumhasartist(albumHasArtistProto) + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(createAlbumHasArtistResponse.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 album has artist", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/DeleteAlbumHasArtistHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/DeleteAlbumHasArtistHandler.java new file mode 100644 index 0000000..c095e1a --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/DeleteAlbumHasArtistHandler.java @@ -0,0 +1,62 @@ +package com.mediamanager.service.delegate.handler.albumhasartist; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasArtistMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasartist.AlbumHasArtistService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Action("albumhasartist.delete") +public class DeleteAlbumHasArtistHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(DeleteAlbumHasArtistHandler.class); + + private final AlbumHasArtistService albumHasArtistService; + + public DeleteAlbumHasArtistHandler(AlbumHasArtistService albumHasArtistService) { + this.albumHasArtistService = albumHasArtistService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException { + + try { + AlbumHasArtistMessages.DeleteAlbumHasArtistRequest deleteRequest = + AlbumHasArtistMessages.DeleteAlbumHasArtistRequest.parseFrom(requestPayload); + int id = deleteRequest.getId(); + boolean success = albumHasArtistService.deleteAlbumHasArtist(id); + AlbumHasArtistMessages.DeleteAlbumHasArtistResponse deleteResponse; + if (success) { + deleteResponse = AlbumHasArtistMessages.DeleteAlbumHasArtistResponse.newBuilder() + .setSuccess(true) + .setMessage("Album has artist deleted successfully") + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(deleteResponse.toByteString()); + } else { + deleteResponse = AlbumHasArtistMessages.DeleteAlbumHasArtistResponse.newBuilder() + .setSuccess(false) + .setMessage("Album has artist not found") + .build(); + + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(deleteResponse.toByteString()); + } + } catch (Exception e) { + logger.error("Error deleting album has artist", e); + AlbumHasArtistMessages.DeleteAlbumHasArtistResponse deleteResponse = + AlbumHasArtistMessages.DeleteAlbumHasArtistResponse.newBuilder() + .setSuccess(false) + .setMessage("Error: " + e.getMessage()) + .build(); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(deleteResponse.toByteString()); + } + } + } diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistByIdHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistByIdHandler.java new file mode 100644 index 0000000..03cec95 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistByIdHandler.java @@ -0,0 +1,56 @@ +package com.mediamanager.service.delegate.handler.albumhasartist; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumHasArtistMapper; +import com.mediamanager.model.AlbumHasArtist; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasArtistMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasartist.AlbumHasArtistService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +@Action(value = "albumhasartist.getById") +public class GetAlbumHasArtistByIdHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(GetAlbumHasArtistByIdHandler.class); + private final AlbumHasArtistService albumHasArtistService; + + public GetAlbumHasArtistByIdHandler(AlbumHasArtistService albumHasArtistService) { + this.albumHasArtistService = albumHasArtistService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException{ + + try{ + AlbumHasArtistMessages.GetAlbumHasArtistByIdRequest getByIdRequest = + AlbumHasArtistMessages.GetAlbumHasArtistByIdRequest.parseFrom(requestPayload); + int id = getByIdRequest.getId(); + + Optional albumHasArtistOpt = albumHasArtistService.getAlbumHasArtistById(id); + + if (albumHasArtistOpt.isEmpty()){ + logger.warn("AlbumHasArtist not found with ID: {}", id); + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(ByteString.copyFromUtf8("AlbumHasArtist not found")); + } + AlbumHasArtistMessages.AlbumHasArtist albumHasArtistProto = AlbumHasArtistMapper.toProtobuf(albumHasArtistOpt.get()); + AlbumHasArtistMessages.GetAlbumHasArtistByIdResponse getByIdResponse = AlbumHasArtistMessages.GetAlbumHasArtistByIdResponse.newBuilder() + .setAlbumhasartist(albumHasArtistProto) + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(getByIdResponse.toByteString()); + } catch (Exception e) { + logger.error("Error getting album has artist by ID", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: "+ e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistHandler.java new file mode 100644 index 0000000..1f2d0fc --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasartist/GetAlbumHasArtistHandler.java @@ -0,0 +1,48 @@ +package com.mediamanager.service.delegate.handler.albumhasartist; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumHasArtistMapper; +import com.mediamanager.model.AlbumHasArtist; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasArtistMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasartist.AlbumHasArtistService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; + + +@Action("albumhasartist.getAll") +public class GetAlbumHasArtistHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(GetAlbumHasArtistHandler.class); + + private final AlbumHasArtistService albumHasArtistService; + + public GetAlbumHasArtistHandler(AlbumHasArtistService albumHasArtistService){this.albumHasArtistService = albumHasArtistService;} + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + List albumHasArtists = albumHasArtistService.getAllAlbumHasArtists(); + AlbumHasArtistMessages.GetAlbumHasArtistsResponse.Builder responseBuilder = AlbumHasArtistMessages.GetAlbumHasArtistsResponse.newBuilder(); + + for (AlbumHasArtist albumHasArtist : albumHasArtists) { + AlbumHasArtistMessages.AlbumHasArtist albumHasArtistProto = AlbumHasArtistMapper.toProtobuf(albumHasArtist); + responseBuilder.addAlbumhasartist(albumHasArtistProto); + } + AlbumHasArtistMessages.GetAlbumHasArtistsResponse getAlbumHasArtistsResponse = responseBuilder.build(); + + return TransportProtocol.Response.newBuilder() + .setPayload(getAlbumHasArtistsResponse.toByteString()); + + }catch (Exception e){ + logger.error("Error getting album has artists", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/proto/albumhasartist.proto b/src/main/proto/albumhasartist.proto new file mode 100644 index 0000000..190a5c7 --- /dev/null +++ b/src/main/proto/albumhasartist.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +option java_package = "com.mediamanager.protocol.messages"; +option java_outer_classname = "AlbumHasArtistMessages"; + +package mediamanager.messages; + +message AlbumHasArtist { + int32 id = 1; + int32 fk_album_id = 2; + int32 fk_artist_id = 3; +} + +message CreateAlbumHasArtistRequest { + int32 fk_album_id = 1; + int32 fk_artist_id = 2; +} + +message CreateAlbumHasArtistResponse { + AlbumHasArtist albumhasartist = 1; +} + +message GetAlbumHasArtistsRequest { + +} + +message GetAlbumHasArtistsResponse { + repeated AlbumHasArtist albumhasartist = 1; +} + +message GetAlbumHasArtistByIdRequest { + int32 id = 1; +} + +message GetAlbumHasArtistByIdResponse { + AlbumHasArtist albumhasartist = 1; +} + +message DeleteAlbumHasArtistRequest { + int32 id = 1; +} + +message DeleteAlbumHasArtistResponse { + bool success = 1; + string message = 2; +} \ No newline at end of file From adb536e135ac230a26dd32972d069b8a54ebbcc0 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 7 Dec 2025 23:03:14 -0300 Subject: [PATCH 3/7] Enhance Album entity with bidirectional relationships and partial update support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces significant improvements to the Album entity and its update functionality, adding bidirectional relationship management with AlbumHasArtist and implementing proper partial update semantics using protobuf wrapper types. Key enhancements: Model Layer Improvements: - Added OneToMany relationship from Album to AlbumHasArtist * Bidirectional mapping with cascade ALL and orphan removal * Maintains collection of album-artist associations - Implemented helper methods for artist management: * addArtist(): creates and adds AlbumHasArtist association * removeArtist(): removes association by artist ID * getArtists(): convenience method to extract Artist list from associations - Added getters/setters for albumArtists collection - Improved entity relationship management Service Layer Enhancements: - Refactored updateAlbum() to support partial updates - Added boolean flags to control field updates: * updateYear: controls whether year field should be modified * updateNumberOfDiscs: controls disc count updates * updateAlbumType: controls AlbumType relationship updates * updateAlbumArt: controls AlbumArt relationship updates - Only updates fields when explicitly provided by client - Maintains existing values for non-provided fields - Allows explicit null/removal of optional relationships - Improved update logic clarity and maintainability Handler Layer Improvements (UpdateAlbumHandler): - Integrated protobuf wrapper type handling - Uses hasYear(), hasNumberOfDiscs(), etc. to detect field presence - Extracts values from Int32Value wrappers - Passes presence flags to service layer - Properly distinguishes between "not provided" and "null/0" - Improved code formatting and readability Protocol Buffer Enhancements: - Added import for google.protobuf.wrappers - Changed UpdateAlbumRequest optional fields to wrapper types: * year: int32 → google.protobuf.Int32Value * number_of_discs: int32 → google.protobuf.Int32Value * fk_albumtype_id: int32 → google.protobuf.Int32Value * fk_albumart_id: int32 → google.protobuf.Int32Value - Enables proper optional field semantics - Allows clients to omit fields from update requests - Supports explicit null to remove relationships Benefits of these changes: - Proper partial update support: clients can update only specific fields - Prevents unintended field overwrites with default values - Bidirectional navigation from Album to Artists - Cascade operations for album-artist associations - Cleaner API semantics with explicit field presence detection - Better alignment with REST PATCH semantics - Maintains backward compatibility for required fields Example usage: - Update only name: provide name, omit other fields - Remove AlbumType: provide fk_albumtype_id with value 0 or null - Keep existing year: omit year field from request - Update multiple fields: provide only the fields to change đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../java/com/mediamanager/model/Album.java | 36 ++++++++++ .../service/album/AlbumService.java | 71 ++++++++++++------- .../handler/album/UpdateAlbumHandler.java | 48 +++++++++---- src/main/proto/album.proto | 14 ++-- 4 files changed, 126 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/mediamanager/model/Album.java b/src/main/java/com/mediamanager/model/Album.java index 6a740fc..6bedc3a 100644 --- a/src/main/java/com/mediamanager/model/Album.java +++ b/src/main/java/com/mediamanager/model/Album.java @@ -2,6 +2,9 @@ package com.mediamanager.model; import jakarta.persistence.*; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @Entity @Table(name = "album") @@ -34,6 +37,10 @@ public class Album { @JoinColumn(name = "fk_albumart_id") private AlbumArt albumArt; + // Relacionamento ManyToMany com Artist atravĂ©s da tabela de junção + @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, orphanRemoval = true) + private List albumArtists = new ArrayList<>(); + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @@ -53,6 +60,27 @@ public class Album { public Album() {} + // MĂ©todos helper para Artist (ManyToMany) + public void addArtist(Artist artist) { + AlbumHasArtist albumHasArtist = new AlbumHasArtist(); + albumHasArtist.setAlbum(this); + albumHasArtist.setArtist(artist); + albumArtists.add(albumHasArtist); + } + + public void removeArtist(Artist artist) { + albumArtists.removeIf(aa -> + aa.getArtist() != null && aa.getArtist().getId().equals(artist.getId()) + ); + } + + // MĂ©todo conveniente para pegar sĂł os artistas + public List getArtists() { + return albumArtists.stream() + .map(AlbumHasArtist::getArtist) + .collect(Collectors.toList()); + } + // Getters and Setters public Integer getId() { return id; @@ -118,6 +146,14 @@ public class Album { this.albumArt = albumArt; } + public List getAlbumArtists() { + return albumArtists; + } + + public void setAlbumArtists(List albumArtists) { + this.albumArtists = albumArtists; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/com/mediamanager/service/album/AlbumService.java b/src/main/java/com/mediamanager/service/album/AlbumService.java index 82d1664..bf41a46 100644 --- a/src/main/java/com/mediamanager/service/album/AlbumService.java +++ b/src/main/java/com/mediamanager/service/album/AlbumService.java @@ -71,50 +71,73 @@ public class AlbumService { return repository.findById(id); } - public Optional updateAlbum(Integer id, String name, Integer year, Integer numberOfDiscs, String code, Boolean isCompilation, Integer albumTypeId, Integer albumArtId) { + public Optional updateAlbum(Integer id, String name, Integer year, + Integer numberOfDiscs, String code, + Boolean isCompilation, Integer albumTypeId, + Integer albumArtId, + boolean updateYear, // ← Novo! + boolean updateNumberOfDiscs, // ← Novo! + boolean updateAlbumType, // ← Novo! + boolean updateAlbumArt) { // ← Novo! if (id == null) { throw new IllegalArgumentException("ID cannot be null"); } - logger.info("Updating album:{}", name); + logger.info("Updating album: {}", name); if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("Album name cannot be null or empty"); } Optional existingAlbum = repository.findById(id); - if(existingAlbum.isEmpty()) { - logger.warn("Album not found with id:{}", id); + if (existingAlbum.isEmpty()) { + logger.warn("Album not found with id: {}", id); return Optional.empty(); } Album album = existingAlbum.get(); album.setName(name); - album.setYear(year); - album.setNumberOfDiscs(numberOfDiscs); album.setCode(code); album.setIsCompilation(isCompilation); - // Update AlbumType if provided - if (albumTypeId != null && albumTypeId > 0) { - Optional albumType = albumTypeRepository.findById(albumTypeId); - if (albumType.isEmpty()) { - throw new IllegalArgumentException("AlbumType not found with id: " + albumTypeId); - } - album.setAlbumType(albumType.get()); - } else { - album.setAlbumType(null); + // Atualiza year SOMENTE se o campo foi fornecido + if (updateYear) { + album.setYear(year); } - // Update AlbumArt if provided - if (albumArtId != null && albumArtId > 0) { - Optional albumArt = albumArtRepository.findById(albumArtId); - if (albumArt.isEmpty()) { - throw new IllegalArgumentException("AlbumArt not found with id: " + albumArtId); - } - album.setAlbumArt(albumArt.get()); - } else { - album.setAlbumArt(null); + // Atualiza numberOfDiscs SOMENTE se o campo foi fornecido + if (updateNumberOfDiscs) { + album.setNumberOfDiscs(numberOfDiscs); } + // Update AlbumType SOMENTE se o campo foi fornecido + if (updateAlbumType) { + if (albumTypeId != null && albumTypeId > 0) { + Optional albumType = albumTypeRepository.findById(albumTypeId); + if (albumType.isEmpty()) { + throw new IllegalArgumentException("AlbumType not found with id: " + albumTypeId); + } + album.setAlbumType(albumType.get()); + } else { + // Explicitamente passado como 0 ou null = remover a relação + album.setAlbumType(null); + } + } + // Se nĂŁo foi fornecido, mantĂ©m o existente + + // Update AlbumArt SOMENTE se o campo foi fornecido + if (updateAlbumArt) { + if (albumArtId != null && albumArtId > 0) { + Optional albumArt = albumArtRepository.findById(albumArtId); + if (albumArt.isEmpty()) { + throw new IllegalArgumentException("AlbumArt not found with id: " + albumArtId); + } + album.setAlbumArt(albumArt.get()); + } else { + // Explicitamente passado como 0 ou null = remover a relação + album.setAlbumArt(null); + } + } + // Se nĂŁo foi fornecido, mantĂ©m o existente + Album updatedAlbum = repository.update(album); return Optional.of(updatedAlbum); } diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java index ac10f55..d9f81b4 100644 --- a/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java @@ -24,24 +24,47 @@ public class UpdateAlbumHandler implements ActionHandler { } @Override - public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { - try{ + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException { + try { AlbumMessages.UpdateAlbumRequest updateRequest = AlbumMessages.UpdateAlbumRequest.parseFrom(requestPayload); int id = updateRequest.getId(); + + // Extrai valores dos wrappers - null se nĂŁo foi fornecido + Integer year = updateRequest.hasYear() + ? updateRequest.getYear().getValue() + : null; + + Integer numberOfDiscs = updateRequest.hasNumberOfDiscs() + ? updateRequest.getNumberOfDiscs().getValue() + : null; + + Integer albumTypeId = updateRequest.hasFkAlbumtypeId() + ? updateRequest.getFkAlbumtypeId().getValue() + : null; + + Integer albumArtId = updateRequest.hasFkAlbumartId() + ? updateRequest.getFkAlbumartId().getValue() + : null; + Optional albumOpt = albumService.updateAlbum( id, updateRequest.getName(), - updateRequest.getYear() > 0 ? updateRequest.getYear() : null, - updateRequest.getNumberOfDiscs() > 0 ? updateRequest.getNumberOfDiscs() : null, + year, + numberOfDiscs, updateRequest.getCode().isEmpty() ? null : updateRequest.getCode(), updateRequest.getIsCompilation(), - updateRequest.getFkAlbumtypeId() > 0 ? updateRequest.getFkAlbumtypeId() : null, - updateRequest.getFkAlbumartId() > 0 ? updateRequest.getFkAlbumartId() : null + albumTypeId, + albumArtId, + updateRequest.hasYear(), // ← Novo! + updateRequest.hasNumberOfDiscs(), // ← Novo! + updateRequest.hasFkAlbumtypeId(), // ← Novo! + updateRequest.hasFkAlbumartId() // ← Novo! ); - if(albumOpt.isEmpty()){ + if (albumOpt.isEmpty()) { logger.warn("Album not found with ID: {}", id); return TransportProtocol.Response.newBuilder() .setStatusCode(404) @@ -50,14 +73,15 @@ public class UpdateAlbumHandler implements ActionHandler { AlbumMessages.Album albumProto = AlbumMapper.toProtobuf(albumOpt.get()); - AlbumMessages.UpdateAlbumResponse updateResponse = AlbumMessages.UpdateAlbumResponse.newBuilder() - .setAlbum(albumProto) - .build(); + AlbumMessages.UpdateAlbumResponse updateResponse = + AlbumMessages.UpdateAlbumResponse.newBuilder() + .setAlbum(albumProto) + .build(); return TransportProtocol.Response.newBuilder() .setPayload(updateResponse.toByteString()); - } catch (IllegalArgumentException e){ + } catch (IllegalArgumentException e) { logger.error("Validation error", e); return TransportProtocol.Response.newBuilder() .setStatusCode(400) @@ -69,4 +93,4 @@ public class UpdateAlbumHandler implements ActionHandler { .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); } } -} +} \ No newline at end of file diff --git a/src/main/proto/album.proto b/src/main/proto/album.proto index 44e876e..e7d350a 100644 --- a/src/main/proto/album.proto +++ b/src/main/proto/album.proto @@ -1,5 +1,7 @@ syntax = "proto3"; +import "google/protobuf/wrappers.proto"; // ← Adicione esse import! + option java_package = "com.mediamanager.protocol.messages"; option java_outer_classname = "AlbumMessages"; @@ -13,11 +15,9 @@ message Album { string code = 5; bool is_compilation = 6; - int32 fk_albumtype_id = 7; int32 fk_albumart_id = 8; - int64 created_at = 9; int64 updated_at = 10; } @@ -55,12 +55,12 @@ message GetAlbumByIdResponse { message UpdateAlbumRequest { int32 id = 1; string name = 2; - int32 year = 3; - int32 number_of_discs = 4; + google.protobuf.Int32Value year = 3; // ← Mudou! + google.protobuf.Int32Value number_of_discs = 4; // ← Mudou! string code = 5; bool is_compilation = 6; - int32 fk_albumtype_id = 7; - int32 fk_albumart_id = 8; + google.protobuf.Int32Value fk_albumtype_id = 7; // ← Mudou! + google.protobuf.Int32Value fk_albumart_id = 8; // ← Mudou! } message UpdateAlbumResponse { @@ -74,4 +74,4 @@ message DeleteAlbumRequest { message DeleteAlbumResponse { bool success = 1; string message = 2; -} +} \ No newline at end of file From a102b24ecd5b52376d8723cf66407f560ef3cd0b Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 7 Dec 2025 23:24:52 -0300 Subject: [PATCH 4/7] Implement AlbumHasGenre relationship management with CRUD operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the AlbumHasGenre join table entity management, establishing the many-to-many relationship between Album and Genre entities. This implementation follows the established repository pattern used for other join table entities like AlbumHasArtist. Key components implemented: Model Layer: - AlbumHasGenre entity representing the join table - ManyToOne relationships to Album and Genre entities - JPA annotations with proper foreign key constraints (nullable = false) - Complete getters and setters - Custom toString method for debugging Repository Layer: - AlbumHasGenreRepository with EntityManager-based operations - Full transaction management with proper rollback handling - Methods: save, findAll, findById, deleteById - No update method (join tables typically only need create/delete) Service Layer: - AlbumHasGenreService with business logic and validation - Constructor injection of AlbumHasGenreRepository, AlbumRepository, GenreRepository - Relationship validation: ensures both Album and Genre exist before creating association - Input validation for null/invalid IDs - ID validation for all operations requiring entity lookup - Comprehensive logging using Log4j2 Mapper Layer: - AlbumHasGenreMapper for bidirectional entity/protobuf conversion - Foreign key mapping for Album and Genre relationships - Null safety checks and validation - Proper handling of optional ID field Action Handlers: - CreateAlbumHasGenreHandler (albumhasgenre.create) - GetAlbumHasGenreHandler (albumhasgenre.getAll) - GetAlbumHasGenreByIdHandler (albumhasgenre.getById) - DeleteAlbumHasGenreHandler (albumhasgenre.delete) - HTTP status code handling: 200 (success), 400 (validation), 404 (not found), 500 (server error) - No update handler as join tables typically only require create/delete operations Protocol Buffers: - Complete proto definition with AlbumHasGenreMessages - Messages support fk_album_id and fk_genre_id foreign keys - CRUD message definitions (Create, Get, GetById, Delete) - No Update messages as per join table requirements Service Registration: - AlbumHasGenreRepository initialized with EntityManagerFactory - AlbumHasGenreService registered with ServiceLocator with required dependencies - Ensures all AlbumHasGenre action handlers can resolve dependencies - Proper dependency injection of AlbumRepository and GenreRepository The implementation follows best practices with proper error handling, logging, validation, relationship integrity checks, and consistency with existing codebase patterns. This enables proper many-to-many relationship management between albums and genres, allowing albums to be associated with multiple genres and vice versa. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../mapper/AlbumHasGenreMapper.java | 47 ++++++++++ .../com/mediamanager/model/AlbumHasGenre.java | 56 ++++++++++++ .../repository/AlbumHasGenreRepository.java | 85 +++++++++++++++++++ .../albumhasgenre/AlbumHasGenreService.java | 77 +++++++++++++++++ .../delegate/DelegateActionManager.java | 5 ++ .../CreateAlbumHasGenreHandler.java | 54 ++++++++++++ .../DeleteAlbumHasGenreHandler.java | 62 ++++++++++++++ .../GetAlbumHasGenreByIdHandler.java | 56 ++++++++++++ .../GetAlbumHasGenreHandler.java | 48 +++++++++++ src/main/proto/albumhasgenre.proto | 46 ++++++++++ 10 files changed, 536 insertions(+) create mode 100644 src/main/java/com/mediamanager/mapper/AlbumHasGenreMapper.java create mode 100644 src/main/java/com/mediamanager/model/AlbumHasGenre.java create mode 100644 src/main/java/com/mediamanager/repository/AlbumHasGenreRepository.java create mode 100644 src/main/java/com/mediamanager/service/albumhasgenre/AlbumHasGenreService.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/CreateAlbumHasGenreHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/DeleteAlbumHasGenreHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreByIdHandler.java create mode 100644 src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreHandler.java create mode 100644 src/main/proto/albumhasgenre.proto diff --git a/src/main/java/com/mediamanager/mapper/AlbumHasGenreMapper.java b/src/main/java/com/mediamanager/mapper/AlbumHasGenreMapper.java new file mode 100644 index 0000000..43c34f3 --- /dev/null +++ b/src/main/java/com/mediamanager/mapper/AlbumHasGenreMapper.java @@ -0,0 +1,47 @@ +package com.mediamanager.mapper; + +import com.mediamanager.model.AlbumHasGenre; +import com.mediamanager.protocol.messages.AlbumHasGenreMessages; + +public class AlbumHasGenreMapper { + public static AlbumHasGenreMessages.AlbumHasGenre toProtobuf(AlbumHasGenre entity) { + if (entity == null) { + return null; + } + + AlbumHasGenreMessages.AlbumHasGenre.Builder builder = AlbumHasGenreMessages.AlbumHasGenre.newBuilder(); + + Integer id = entity.getId(); + if (id != null) { + builder.setId(id); + } + + // Map Album foreign key + if (entity.getAlbum() != null && entity.getAlbum().getId() != null) { + builder.setFkAlbumId(entity.getAlbum().getId()); + } + + // Map Genre foreign key + if (entity.getGenre() != null && entity.getGenre().getId() != null) { + builder.setFkGenreId(entity.getGenre().getId()); + } + + return builder.build(); + } + + public static AlbumHasGenre toEntity(AlbumHasGenreMessages.AlbumHasGenre protobuf) { + if (protobuf == null) { + return null; + } + + AlbumHasGenre entity = new AlbumHasGenre(); + + if (protobuf.getId() > 0) { + entity.setId(protobuf.getId()); + } + + // Note: Foreign key relationships (Album, Genre) are handled in the service layer + + return entity; + } +} diff --git a/src/main/java/com/mediamanager/model/AlbumHasGenre.java b/src/main/java/com/mediamanager/model/AlbumHasGenre.java new file mode 100644 index 0000000..c380a89 --- /dev/null +++ b/src/main/java/com/mediamanager/model/AlbumHasGenre.java @@ -0,0 +1,56 @@ +package com.mediamanager.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "albumshasgenre") +public class AlbumHasGenre { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fk_album_id", nullable = false) + private Album album; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fk_genre_id", nullable = false) + private Genre genre; + + public AlbumHasGenre() {} + + // Getters and Setters + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + public Genre getGenre() { + return genre; + } + + public void setGenre(Genre genre) { + this.genre = genre; + } + + @Override + public String toString() { + return "AlbumHasGenre{" + + "id=" + id + + ", albumId=" + (album != null ? album.getId() : null) + + ", genreId=" + (genre != null ? genre.getId() : null) + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/mediamanager/repository/AlbumHasGenreRepository.java b/src/main/java/com/mediamanager/repository/AlbumHasGenreRepository.java new file mode 100644 index 0000000..d842224 --- /dev/null +++ b/src/main/java/com/mediamanager/repository/AlbumHasGenreRepository.java @@ -0,0 +1,85 @@ +package com.mediamanager.repository; + + +import com.mediamanager.model.AlbumHasGenre; +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 AlbumHasGenreRepository { + private static final Logger logger = LogManager.getLogger(AlbumHasGenreRepository.class); + + private final EntityManagerFactory entityManagerFactory; + + public AlbumHasGenreRepository(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + public AlbumHasGenre save(AlbumHasGenre albumHasGenre) { + logger.debug("Saving AlbumHasGenre: {}", albumHasGenre); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try { + em.persist(albumHasGenre); + em.getTransaction().commit(); + logger.debug("AlbumHasGenre has been saved successfully"); + return albumHasGenre; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while saving AlbumHasGenre: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + + public List findAll() { + logger.debug("Finding All AlbumHasGenre"); + EntityManager em = entityManagerFactory.createEntityManager(); + try{ + return em.createQuery("select a from AlbumHasGenre a", AlbumHasGenre.class).getResultList(); + }finally { + if (em.isOpen()) em.close(); + } + } + + public Optional findById(Integer id) { + logger.debug("Finding AlbumHasGenre with id: {}", id); + EntityManager em = entityManagerFactory.createEntityManager(); + try{ + AlbumHasGenre albumHasGenre = em.find(AlbumHasGenre.class, id); + return Optional.ofNullable(albumHasGenre); + }finally { + if (em.isOpen()) em.close(); + } + } + + public boolean deleteById(Integer id){ + logger.debug("Deleting AlbumHasGenre with id: {}", id); + EntityManager em = entityManagerFactory.createEntityManager(); + em.getTransaction().begin(); + try{ + AlbumHasGenre albumHasGenre = em.find(AlbumHasGenre.class, id); + if (albumHasGenre == null) { + em.getTransaction().rollback(); + return false; + } + em.remove(albumHasGenre); + em.getTransaction().commit(); + logger.debug("AlbumHasGenre has been deleted successfully"); + return true; + } catch (Exception e) { + em.getTransaction().rollback(); + logger.error("Error while deleting AlbumHasGenre: {}", e.getMessage()); + throw e; + } finally { + if (em.isOpen()) em.close(); + } + } + +} diff --git a/src/main/java/com/mediamanager/service/albumhasgenre/AlbumHasGenreService.java b/src/main/java/com/mediamanager/service/albumhasgenre/AlbumHasGenreService.java new file mode 100644 index 0000000..d75c5e8 --- /dev/null +++ b/src/main/java/com/mediamanager/service/albumhasgenre/AlbumHasGenreService.java @@ -0,0 +1,77 @@ +package com.mediamanager.service.albumhasgenre; + +import com.mediamanager.model.Album; +import com.mediamanager.model.AlbumHasGenre; +import com.mediamanager.model.Genre; +import com.mediamanager.repository.AlbumHasGenreRepository; +import com.mediamanager.repository.AlbumRepository; +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 AlbumHasGenreService { + private static final Logger logger = LogManager.getLogger(AlbumHasGenreService.class); + private final AlbumHasGenreRepository repository; + private final AlbumRepository albumRepository; + private final GenreRepository genreRepository; + + public AlbumHasGenreService(AlbumHasGenreRepository repository, AlbumRepository albumRepository, GenreRepository genreRepository) { + this.repository = repository; + this.albumRepository = albumRepository; + this.genreRepository = genreRepository; + } + + public AlbumHasGenre createAlbumHasGenre(Integer albumId, Integer genreId) { + logger.debug("Creating album has genre relationship - albumId:{}, genreId:{}", albumId, genreId); + + if (albumId == null || albumId <= 0) { + throw new IllegalArgumentException("Album ID cannot be null or invalid"); + } + if (genreId == null || genreId <= 0) { + throw new IllegalArgumentException("Genre ID cannot be null or invalid"); + } + + // Verify Album exists + Optional album = albumRepository.findById(albumId); + if (album.isEmpty()) { + throw new IllegalArgumentException("Album not found with id: " + albumId); + } + + // Verify Genre exists + Optional genre = genreRepository.findById(genreId); + if (genre.isEmpty()) { + throw new IllegalArgumentException("Genre not found with id: " + genreId); + } + + AlbumHasGenre albumHasGenre = new AlbumHasGenre(); + albumHasGenre.setAlbum(album.get()); + albumHasGenre.setGenre(genre.get()); + + return repository.save(albumHasGenre); + } + + public List getAllAlbumHasGenres() { + logger.info("Getting all album has genre relationships"); + return repository.findAll(); + } + + public Optional getAlbumHasGenreById(Integer id) { + if (id == null) { + throw new IllegalArgumentException("ID cannot be null"); + } + logger.info("Getting album has genre by id:{}", id); + return repository.findById(id); + } + + public boolean deleteAlbumHasGenre(Integer id) { + if (id == null) { + throw new IllegalArgumentException("Album has genre id cannot be null"); + } + logger.info("Deleting album has genre:{}", id); + return repository.deleteById(id); + } + +} diff --git a/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java b/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java index 8fca373..1f1cb81 100644 --- a/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java +++ b/src/main/java/com/mediamanager/service/delegate/DelegateActionManager.java @@ -6,6 +6,7 @@ import com.mediamanager.repository.*; import com.mediamanager.service.album.AlbumService; import com.mediamanager.service.albumart.AlbumArtService; import com.mediamanager.service.albumhasartist.AlbumHasArtistService; +import com.mediamanager.service.albumhasgenre.AlbumHasGenreService; import com.mediamanager.service.albumtype.AlbumTypeService; import com.mediamanager.service.bitdepth.BitDepthService; import com.mediamanager.service.bitrate.BitRateService; @@ -95,6 +96,10 @@ public class DelegateActionManager { AlbumHasArtistService albumHasArtistService = new AlbumHasArtistService(albumHasArtistRepository, albumRepository, artistRepository); serviceLocator.register(AlbumHasArtistService.class, albumHasArtistService); + AlbumHasGenreRepository albumHasGenreRepository = new AlbumHasGenreRepository(entityManagerFactory); + AlbumHasGenreService albumHasGenreService = new AlbumHasGenreService(albumHasGenreRepository, albumRepository, genreRepository); + serviceLocator.register(AlbumHasGenreService.class, albumHasGenreService); + serviceLocator.logRegisteredServices(); logger.info("Services initialized successfully"); diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/CreateAlbumHasGenreHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/CreateAlbumHasGenreHandler.java new file mode 100644 index 0000000..4577a46 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/CreateAlbumHasGenreHandler.java @@ -0,0 +1,54 @@ +package com.mediamanager.service.delegate.handler.albumhasgenre; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumHasGenreMapper; +import com.mediamanager.model.AlbumHasGenre; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasGenreMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasgenre.AlbumHasGenreService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Action("albumhasgenre.create") +public class CreateAlbumHasGenreHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(CreateAlbumHasGenreHandler.class); + private final AlbumHasGenreService albumHasGenreService; + + public CreateAlbumHasGenreHandler(AlbumHasGenreService albumHasGenreService) { + this.albumHasGenreService = albumHasGenreService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + AlbumHasGenreMessages.CreateAlbumHasGenreRequest createRequest = + AlbumHasGenreMessages.CreateAlbumHasGenreRequest.parseFrom(requestPayload); + + AlbumHasGenre albumHasGenre = albumHasGenreService.createAlbumHasGenre( + createRequest.getFkAlbumId() > 0 ? createRequest.getFkAlbumId() : null, + createRequest.getFkGenreId() > 0 ? createRequest.getFkGenreId() : null + ); + + AlbumHasGenreMessages.AlbumHasGenre albumHasGenreProto = AlbumHasGenreMapper.toProtobuf(albumHasGenre); + AlbumHasGenreMessages.CreateAlbumHasGenreResponse createAlbumHasGenreResponse = AlbumHasGenreMessages.CreateAlbumHasGenreResponse.newBuilder() + .setAlbumhasgenre(albumHasGenreProto) + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(createAlbumHasGenreResponse.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 album has genre", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/DeleteAlbumHasGenreHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/DeleteAlbumHasGenreHandler.java new file mode 100644 index 0000000..149cc4c --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/DeleteAlbumHasGenreHandler.java @@ -0,0 +1,62 @@ +package com.mediamanager.service.delegate.handler.albumhasgenre; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasGenreMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasgenre.AlbumHasGenreService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Action("albumhasgenre.delete") +public class DeleteAlbumHasGenreHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(DeleteAlbumHasGenreHandler.class); + + private final AlbumHasGenreService albumHasGenreService; + + public DeleteAlbumHasGenreHandler(AlbumHasGenreService albumHasGenreService) { + this.albumHasGenreService = albumHasGenreService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException { + + try { + AlbumHasGenreMessages.DeleteAlbumHasGenreRequest deleteRequest = + AlbumHasGenreMessages.DeleteAlbumHasGenreRequest.parseFrom(requestPayload); + int id = deleteRequest.getId(); + boolean success = albumHasGenreService.deleteAlbumHasGenre(id); + AlbumHasGenreMessages.DeleteAlbumHasGenreResponse deleteResponse; + if (success) { + deleteResponse = AlbumHasGenreMessages.DeleteAlbumHasGenreResponse.newBuilder() + .setSuccess(true) + .setMessage("Album has genre deleted successfully") + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(deleteResponse.toByteString()); + } else { + deleteResponse = AlbumHasGenreMessages.DeleteAlbumHasGenreResponse.newBuilder() + .setSuccess(false) + .setMessage("Album has genre not found") + .build(); + + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(deleteResponse.toByteString()); + } + } catch (Exception e) { + logger.error("Error deleting album has genre", e); + AlbumHasGenreMessages.DeleteAlbumHasGenreResponse deleteResponse = + AlbumHasGenreMessages.DeleteAlbumHasGenreResponse.newBuilder() + .setSuccess(false) + .setMessage("Error: " + e.getMessage()) + .build(); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(deleteResponse.toByteString()); + } + } + } diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreByIdHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreByIdHandler.java new file mode 100644 index 0000000..a6835c0 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreByIdHandler.java @@ -0,0 +1,56 @@ +package com.mediamanager.service.delegate.handler.albumhasgenre; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumHasGenreMapper; +import com.mediamanager.model.AlbumHasGenre; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasGenreMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasgenre.AlbumHasGenreService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +@Action(value = "albumhasgenre.getById") +public class GetAlbumHasGenreByIdHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(GetAlbumHasGenreByIdHandler.class); + private final AlbumHasGenreService albumHasGenreService; + + public GetAlbumHasGenreByIdHandler(AlbumHasGenreService albumHasGenreService) { + this.albumHasGenreService = albumHasGenreService; + } + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException{ + + try{ + AlbumHasGenreMessages.GetAlbumHasGenreByIdRequest getByIdRequest = + AlbumHasGenreMessages.GetAlbumHasGenreByIdRequest.parseFrom(requestPayload); + int id = getByIdRequest.getId(); + + Optional albumHasGenreOpt = albumHasGenreService.getAlbumHasGenreById(id); + + if (albumHasGenreOpt.isEmpty()){ + logger.warn("AlbumHasGenre not found with ID: {}", id); + return TransportProtocol.Response.newBuilder() + .setStatusCode(404) + .setPayload(ByteString.copyFromUtf8("AlbumHasGenre not found")); + } + AlbumHasGenreMessages.AlbumHasGenre albumHasGenreProto = AlbumHasGenreMapper.toProtobuf(albumHasGenreOpt.get()); + AlbumHasGenreMessages.GetAlbumHasGenreByIdResponse getByIdResponse = AlbumHasGenreMessages.GetAlbumHasGenreByIdResponse.newBuilder() + .setAlbumhasgenre(albumHasGenreProto) + .build(); + return TransportProtocol.Response.newBuilder() + .setPayload(getByIdResponse.toByteString()); + } catch (Exception e) { + logger.error("Error getting album has genre by ID", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: "+ e.getMessage())); + } + } +} diff --git a/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreHandler.java new file mode 100644 index 0000000..d90dcc1 --- /dev/null +++ b/src/main/java/com/mediamanager/service/delegate/handler/albumhasgenre/GetAlbumHasGenreHandler.java @@ -0,0 +1,48 @@ +package com.mediamanager.service.delegate.handler.albumhasgenre; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.mediamanager.mapper.AlbumHasGenreMapper; +import com.mediamanager.model.AlbumHasGenre; +import com.mediamanager.protocol.TransportProtocol; +import com.mediamanager.protocol.messages.AlbumHasGenreMessages; +import com.mediamanager.service.delegate.ActionHandler; +import com.mediamanager.service.delegate.annotation.Action; +import com.mediamanager.service.albumhasgenre.AlbumHasGenreService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; + + +@Action("albumhasgenre.getAll") +public class GetAlbumHasGenreHandler implements ActionHandler { + private static final Logger logger = LogManager.getLogger(GetAlbumHasGenreHandler.class); + + private final AlbumHasGenreService albumHasGenreService; + + public GetAlbumHasGenreHandler(AlbumHasGenreService albumHasGenreService){this.albumHasGenreService = albumHasGenreService;} + + @Override + public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { + try{ + List albumHasGenres = albumHasGenreService.getAllAlbumHasGenres(); + AlbumHasGenreMessages.GetAlbumHasGenresResponse.Builder responseBuilder = AlbumHasGenreMessages.GetAlbumHasGenresResponse.newBuilder(); + + for (AlbumHasGenre albumHasGenre : albumHasGenres) { + AlbumHasGenreMessages.AlbumHasGenre albumHasGenreProto = AlbumHasGenreMapper.toProtobuf(albumHasGenre); + responseBuilder.addAlbumhasgenre(albumHasGenreProto); + } + AlbumHasGenreMessages.GetAlbumHasGenresResponse getAlbumHasGenresResponse = responseBuilder.build(); + + return TransportProtocol.Response.newBuilder() + .setPayload(getAlbumHasGenresResponse.toByteString()); + + }catch (Exception e){ + logger.error("Error getting album has genres", e); + return TransportProtocol.Response.newBuilder() + .setStatusCode(500) + .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); + } + } +} diff --git a/src/main/proto/albumhasgenre.proto b/src/main/proto/albumhasgenre.proto new file mode 100644 index 0000000..441de1d --- /dev/null +++ b/src/main/proto/albumhasgenre.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +option java_package = "com.mediamanager.protocol.messages"; +option java_outer_classname = "AlbumHasGenreMessages"; + +package mediamanager.messages; + +message AlbumHasGenre { + int32 id = 1; + int32 fk_album_id = 2; + int32 fk_genre_id = 3; +} + +message CreateAlbumHasGenreRequest { + int32 fk_album_id = 1; + int32 fk_genre_id = 2; +} + +message CreateAlbumHasGenreResponse { + AlbumHasGenre albumhasgenre = 1; +} + +message GetAlbumHasGenresRequest { + +} + +message GetAlbumHasGenresResponse { + repeated AlbumHasGenre albumhasgenre = 1; +} + +message GetAlbumHasGenreByIdRequest { + int32 id = 1; +} + +message GetAlbumHasGenreByIdResponse { + AlbumHasGenre albumhasgenre = 1; +} + +message DeleteAlbumHasGenreRequest { + int32 id = 1; +} + +message DeleteAlbumHasGenreResponse { + bool success = 1; + string message = 2; +} \ No newline at end of file From e51d5aa67839d5a5fff67455d237d238e7e1ef08 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 7 Dec 2025 23:31:07 -0300 Subject: [PATCH 5/7] Add bidirectional Genre relationship to Album entity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit extends the Album entity with bidirectional relationship management for the AlbumHasGenre join table, enabling many-to-many associations between albums and genres with convenient helper methods. Key enhancements: Relationship Mapping: - Added OneToMany relationship from Album to AlbumHasGenre * Bidirectional mapping with cascade ALL and orphan removal * Maintains collection of album-genre associations * Initialized as ArrayList to prevent null pointer exceptions * Follows the same pattern as albumArtists relationship Helper Methods for Genre Management: - addGenre(Genre genre): * Creates AlbumHasGenre join table entry * Sets bidirectional references to Album and Genre * Adds association to albumGenres collection * Simplifies adding genres to albums programmatically - removeGenre(Genre genre): * Removes association by comparing genre IDs * Safely handles null checks * Uses removeIf for clean collection manipulation * Maintains referential integrity - getGenres(): * Convenience method to extract Genre list from join table * Uses Java streams for clean transformation * Maps AlbumHasGenre → Genre * Returns List for easy consumption Collection Management: - Added getAlbumGenres() getter * Returns full List for direct access * Useful for repository-level operations - Added setAlbumGenres() setter * Allows bulk replacement of genre associations * Required by JPA for entity hydration Benefits: - Enables navigating from Album to associated Genres - Simplifies genre association management in service layer - Cascade operations ensure join table entries are managed automatically - Orphan removal prevents orphaned join table entries - Consistent API with existing Artist relationship methods - Type-safe genre management through helper methods Pattern Consistency: This implementation mirrors the albumArtists relationship structure, providing a consistent API for managing both artist and genre associations on the Album entity. Both relationships use the same cascade and orphan removal strategies for reliable data integrity. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 This commit message is ready to use in IntelliJ for the Album.java changes! --- .../java/com/mediamanager/model/Album.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/java/com/mediamanager/model/Album.java b/src/main/java/com/mediamanager/model/Album.java index 6bedc3a..d55621f 100644 --- a/src/main/java/com/mediamanager/model/Album.java +++ b/src/main/java/com/mediamanager/model/Album.java @@ -41,6 +41,10 @@ public class Album { @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, orphanRemoval = true) private List albumArtists = new ArrayList<>(); + // Relacionamento ManyToMany com Genre atravĂ©s da tabela de junção + @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, orphanRemoval = true) + private List albumGenres = new ArrayList<>(); // ← ADICIONE ESSA LINHA + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @@ -81,6 +85,31 @@ public class Album { .collect(Collectors.toList()); } + // ========== ADICIONE ESSES MÉTODOS PARA GENRE ========== + + // MĂ©todos helper para Genre (ManyToMany) + public void addGenre(Genre genre) { + AlbumHasGenre albumHasGenre = new AlbumHasGenre(); + albumHasGenre.setAlbum(this); + albumHasGenre.setGenre(genre); + albumGenres.add(albumHasGenre); + } + + public void removeGenre(Genre genre) { + albumGenres.removeIf(ag -> + ag.getGenre() != null && ag.getGenre().getId().equals(genre.getId()) + ); + } + + // MĂ©todo conveniente para pegar sĂł os gĂȘneros + public List getGenres() { + return albumGenres.stream() + .map(AlbumHasGenre::getGenre) + .collect(Collectors.toList()); + } + + // ========== FIM DOS MÉTODOS DE GENRE ========== + // Getters and Setters public Integer getId() { return id; @@ -154,6 +183,18 @@ public class Album { this.albumArtists = albumArtists; } + // ========== ADICIONE ESSES GETTERS/SETTERS ========== + + public List getAlbumGenres() { + return albumGenres; + } + + public void setAlbumGenres(List albumGenres) { + this.albumGenres = albumGenres; + } + + // ========== FIM DOS GETTERS/SETTERS DE GENRE ========== + public LocalDateTime getCreatedAt() { return createdAt; } From 7b7d1e7348d4bf310126e91527933e48f058ea99 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Miranda Date: Mon, 8 Dec 2025 00:11:47 -0300 Subject: [PATCH 6/7] Update src/main/java/com/mediamanager/model/Album.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/main/java/com/mediamanager/model/Album.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mediamanager/model/Album.java b/src/main/java/com/mediamanager/model/Album.java index d55621f..4eca747 100644 --- a/src/main/java/com/mediamanager/model/Album.java +++ b/src/main/java/com/mediamanager/model/Album.java @@ -74,7 +74,9 @@ public class Album { public void removeArtist(Artist artist) { albumArtists.removeIf(aa -> - aa.getArtist() != null && aa.getArtist().getId().equals(artist.getId()) + aa.getArtist() != null && + aa.getArtist().getId() != null && + aa.getArtist().getId().equals(artist.getId()) ); } From 9e3516aacb7d2f0b41ea2ecfe128c00a8c1e225d Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Miranda Date: Mon, 8 Dec 2025 00:14:51 -0300 Subject: [PATCH 7/7] Enhance genre removal logic in Album class Update removeGenre method to check for null IDs. --- src/main/java/com/mediamanager/model/Album.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mediamanager/model/Album.java b/src/main/java/com/mediamanager/model/Album.java index 4eca747..e3419b4 100644 --- a/src/main/java/com/mediamanager/model/Album.java +++ b/src/main/java/com/mediamanager/model/Album.java @@ -98,9 +98,9 @@ public class Album { } public void removeGenre(Genre genre) { - albumGenres.removeIf(ag -> - ag.getGenre() != null && ag.getGenre().getId().equals(genre.getId()) - ); + ag.getGenre() != null && + ag.getGenre().getId() != null && + ag.getGenre().getId().equals(genre.getId()) } // MĂ©todo conveniente para pegar sĂł os gĂȘneros @@ -221,4 +221,4 @@ public class Album { ", year=" + year + '}'; } -} \ No newline at end of file +}