BandListingController.java
package com.wavii.controller;
import com.wavii.dto.band.BandListingResponse;
import com.wavii.dto.band.CreateBandListingRequest;
import com.wavii.model.BandListing;
import com.wavii.model.BandListingReport;
import com.wavii.model.User;
import com.wavii.repository.BandListingReportRepository;
import com.wavii.repository.BandListingRepository;
import com.wavii.service.BandListingService;
import com.wavii.service.OdooService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Controlador REST para la gestión de anuncios de bandas y músicos.
*
* @author danielrguezh
*/
@RestController
@RequestMapping({ "/api/band-listings", "/api/bands" })
@RequiredArgsConstructor
@Slf4j
public class BandListingController {
private final BandListingService service;
private final BandListingRepository bandListingRepository;
private final BandListingReportRepository bandListingReportRepository;
private final OdooService odooService;
@Value("${wavii.app.base-url:http://localhost:8080}")
private String appBaseUrl;
@Value("${pdf.storage.path:./uploads/pdfs}")
private String pdfStoragePath;
/**
* Obtiene un listado paginado de anuncios de bandas.
*
* @param genre Género musical (opcional).
* @param city Ciudad (opcional).
* @param role Rol del músico (opcional).
* @param page Número de página.
* @return Página de resultados.
*/
@GetMapping
public ResponseEntity<Page<BandListingResponse>> getListings(
@RequestParam(required = false) String genre,
@RequestParam(required = false) String city,
@RequestParam(required = false) String role,
@RequestParam(defaultValue = "0") int page
) {
return ResponseEntity.ok(service.getListings(genre, city, role, page));
}
/**
* Obtiene los anuncios creados por el usuario actual.
*
* @param currentUser Usuario autenticado.
* @return Lista de anuncios del usuario.
*/
@GetMapping("/my")
public ResponseEntity<List<BandListingResponse>> getMyListings(
@AuthenticationPrincipal User currentUser
) {
if (currentUser == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).<List<BandListingResponse>>build();
}
try {
return ResponseEntity.ok(service.getMyListings(currentUser));
} catch (Exception e) {
log.error("Error fetching listings for user: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).<List<BandListingResponse>>build();
}
}
/**
* Obtiene un anuncio por su ID.
*
* @param id ID del anuncio.
* @return El anuncio encontrado.
*/
@GetMapping("/{id}")
public ResponseEntity<BandListingResponse> getListing(@PathVariable UUID id) {
try {
return ResponseEntity.ok(service.getById(id));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).<BandListingResponse>build();
}
}
/**
* Crea un nuevo anuncio de banda.
*
* @param request Datos del anuncio.
* @param currentUser Usuario que crea el anuncio.
* @return El anuncio creado.
*/
@PostMapping
public ResponseEntity<BandListingResponse> create(
@RequestBody CreateBandListingRequest request,
@AuthenticationPrincipal User currentUser
) {
if (currentUser == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).<BandListingResponse>build();
}
try {
return ResponseEntity.status(201).body(service.create(request, currentUser));
} catch (Exception e) {
log.error("Error creating band listing: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).<BandListingResponse>build();
}
}
/**
* Sube una imagen para un anuncio de banda.
*
* @param file Archivo de imagen.
* @return URL de la imagen subida.
*/
@PostMapping("/images")
public ResponseEntity<?> uploadBandImage(@RequestParam("file") MultipartFile file) {
if (file == null || file.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "El archivo no puede estar vacio"));
}
String contentType = file.getContentType();
boolean validImageContentType = contentType != null && contentType.startsWith("image/");
boolean validImageExtension = hasImageExtension(file.getOriginalFilename());
if (!validImageContentType && !validImageExtension) {
return ResponseEntity.badRequest().body(Map.of("message", "Solo se permiten imagenes"));
}
try {
Path uploadPath = uploadRoot().resolve("bands");
Files.createDirectories(uploadPath);
String storedName = UUID.randomUUID() + extensionOf(file.getOriginalFilename());
log.info("Subiendo imagen de banda: contentType={}, size={} bytes", contentType, file.getSize());
Files.copy(file.getInputStream(), uploadPath.resolve(storedName), StandardCopyOption.REPLACE_EXISTING);
return ResponseEntity.ok(Map.of(
"url", appBaseUrl + "/uploads/bands/" + storedName,
"fileName", storedName
));
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message", "No se pudo guardar la imagen"));
}
}
@PostMapping("/{id}/report")
public ResponseEntity<?> reportBandListing(
@PathVariable UUID id,
@AuthenticationPrincipal User currentUser,
@RequestBody ReportBandListingRequest request
) {
if (currentUser == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Sesion no valida"));
}
if (request.reason() == null || request.reason().isBlank()) {
return ResponseEntity.badRequest().body(Map.of("message", "Debes indicar un motivo"));
}
BandListing listing = bandListingRepository.findById(id).orElse(null);
if (listing == null || listing.isRemovedByModeration()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Anuncio no encontrado"));
}
BandListingReport report = BandListingReport.builder()
.listing(listing)
.reporter(currentUser)
.reason(request.reason().trim())
.details(request.details() != null ? request.details().trim() : null)
.build();
bandListingReportRepository.save(report);
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("listingTitle", listing.getTitle());
snapshot.put("listingDescription", listing.getDescription());
snapshot.put("listingType", listing.getType().name());
snapshot.put("listingGenre", listing.getGenre().name());
snapshot.put("listingCity", listing.getCity());
snapshot.put("listingRoles", listing.getRoles().stream().map(Enum::name).toList());
snapshot.put("creatorId", listing.getCreator().getId().toString());
snapshot.put("creatorName", listing.getCreator().getName());
snapshot.put("creatorEmail", listing.getCreator().getEmail());
snapshot.put("reporterId", currentUser.getId().toString());
snapshot.put("reportId", report.getId().toString());
odooService.createModerationReport(
"band_listing",
currentUser.getName(),
currentUser.getEmail(),
listing.getTitle(),
"band_listing:" + listing.getId(),
report.getReason(),
report.getDetails(),
snapshot
);
return ResponseEntity.ok(Map.of("message", "Reporte enviado"));
}
/**
* Elimina un anuncio de banda.
*
* @param id ID del anuncio a eliminar.
* @param currentUser Usuario autenticado.
* @return 204 No Content si se eliminó correctamente.
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(
@PathVariable UUID id,
@AuthenticationPrincipal User currentUser
) {
if (currentUser == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
try {
service.delete(id, currentUser);
return ResponseEntity.noContent().build();
} catch (Exception e) {
log.error("Error deleting band listing {}: {}", id, e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@ExceptionHandler(MultipartException.class)
public ResponseEntity<Map<String, String>> handleMultipartException(MultipartException ex) {
log.warn("No se pudo procesar la subida de imagen de banda: {}", ex.getMessage());
return ResponseEntity.badRequest().body(Map.of(
"message", "No se pudo leer la imagen. Prueba con otra foto o vuelve a intentarlo."
));
}
/**
* Obtiene la extensión de un nombre de archivo.
*
* @param originalFilename Nombre original.
* @return Extensión (por defecto .jpg).
*/
private String extensionOf(String originalFilename) {
if (originalFilename == null || !originalFilename.contains(".")) {
return ".jpg";
}
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
return extension.isBlank() ? ".jpg" : extension;
}
private boolean hasImageExtension(String originalFilename) {
if (originalFilename == null || !originalFilename.contains(".")) {
return false;
}
String extension = originalFilename.substring(originalFilename.lastIndexOf('.') + 1).toLowerCase();
return extension.equals("jpg")
|| extension.equals("jpeg")
|| extension.equals("png")
|| extension.equals("webp")
|| extension.equals("gif")
|| extension.equals("heic");
}
/**
* Obtiene la ruta raíz para las subidas.
*
* @return Path de la raíz de subidas.
*/
private Path uploadRoot() {
Path root = Paths.get(pdfStoragePath).getParent();
return root != null ? root : Paths.get("uploads");
}
record ReportBandListingRequest(String reason, String details) {
}
}