package com.aiteacher.book; import com.aiteacher.document.FigureEntity; import com.aiteacher.document.FigureRepository; import com.aiteacher.document.MarkdownStorageService; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.UUID; @RestController @RequestMapping("/api/v1/books") public class BookController { private final BookService bookService; private final FigureRepository figureRepository; private final MarkdownStorageService markdownStorageService; @Value("${app.features.upload-enabled:true}") private boolean uploadEnabled; @Value("${app.features.delete-enabled:true}") private boolean deleteEnabled; public BookController(BookService bookService, FigureRepository figureRepository, MarkdownStorageService markdownStorageService) { this.bookService = bookService; this.figureRepository = figureRepository; this.markdownStorageService = markdownStorageService; } @PostMapping(consumes = "multipart/form-data") public ResponseEntity upload(@RequestParam("file") MultipartFile file) throws IOException { if (!uploadEnabled) return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build(); Book book = bookService.upload(file); return ResponseEntity.status(HttpStatus.ACCEPTED).body(toSummaryResponse(book)); } @GetMapping public ResponseEntity>> list() { List> books = bookService.listAll().stream() .map(this::toFullResponse) .toList(); return ResponseEntity.ok(books); } @GetMapping("/{id}") public ResponseEntity> get(@PathVariable UUID id) { Book book = bookService.getById(id); return ResponseEntity.ok(toFullResponse(book)); } @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable UUID id) { if (!deleteEnabled) return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build(); bookService.delete(id); return ResponseEntity.noContent().build(); } @PostMapping("/{id}/reembed") public ResponseEntity> reembed(@PathVariable UUID id) { Book book = bookService.reembed(id); return ResponseEntity.accepted().body(Map.of( "bookId", book.getId(), "status", BookStatus.PROCESSING.name() )); } @GetMapping(value = "/{id}/pages/{pageNumber}/html", produces = MediaType.TEXT_HTML_VALUE) public ResponseEntity getPageHtml(@PathVariable UUID id, @PathVariable int pageNumber) { bookService.getById(id); // 404 if not found try { return ResponseEntity.ok(markdownStorageService.getText(id, pageNumber)); } catch (Exception e) { return ResponseEntity.notFound().build(); } } @GetMapping("/{id}/figures") public ResponseEntity> figures(@PathVariable UUID id) { bookService.getById(id); // 404 if not found List responses = figureRepository.findAllByBookId(id) .stream() .map(f -> toFigureResponse(id, f)) .toList(); return ResponseEntity.ok(responses); } private FigureResponse toFigureResponse(UUID bookId, FigureEntity f) { String filename = f.getImagePath().substring(f.getImagePath().lastIndexOf('/') + 1); String imageUrl = "/api/v1/figures/" + bookId + "/" + filename; return new FigureResponse( f.getId(), f.getLabel(), f.getCaption(), f.getFigureType().name(), f.getPage(), imageUrl, f.getSectionId(), null // section title not eagerly loaded here ); } private Map toSummaryResponse(Book book) { return Map.of( "id", book.getId(), "title", book.getTitle(), "fileName", book.getFileName(), "status", book.getStatus().name(), "uploadedAt", book.getUploadedAt() ); } private Map toFullResponse(Book book) { var map = new java.util.LinkedHashMap(); map.put("id", book.getId()); map.put("title", book.getTitle()); map.put("fileName", book.getFileName()); map.put("fileSizeBytes", book.getFileSizeBytes()); map.put("pageCount", book.getPageCount()); map.put("status", book.getStatus().name()); map.put("uploadedAt", book.getUploadedAt()); map.put("processedAt", book.getProcessedAt()); if (book.getErrorMessage() != null) { map.put("errorMessage", book.getErrorMessage()); } return map; } }