mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-03 20:04:28 +01:00
fix(api): return 204 No Content on failed PDF filters; add OpenAPI responses and safe resource handling (#4406)
# Description of Changes - **What was changed** - Added explicit `@ApiResponses` for filter endpoints with `200` (PDF passed) and `204` (did not pass) including `@Content` and media types. - Replaced ambiguous `null` returns with `ResponseEntity.noContent().build()` when a filter condition is not met. - Ensured `PDDocument` is properly closed using try-with-resources in relevant endpoints. - Consolidated comparison logic into a reusable, type-safe `compare<T extends Comparable<T>>()` helper for page count, page size, file size, and rotation checks. - Minor cleanup and consistency improvements across filter endpoints. - **Why the change was made** - To return correct HTTP semantics (avoid ambiguous `null` responses) and improve API reliability for clients consuming these endpoints. - To document expected responses clearly in the OpenAPI spec for better consumer tooling and DX. - To prevent potential resource leaks by consistently closing `PDDocument`. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
parent
5e281fa002
commit
f48f80927a
@ -15,6 +15,9 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -42,6 +45,16 @@ public class FilterController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF contains set text, returns true if does",
|
summary = "Checks if a PDF contains set text, returns true if does",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "PDF passed filter",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "PDF did not pass filter",
|
||||||
|
content = @Content())
|
||||||
|
})
|
||||||
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
|
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
@ -54,148 +67,184 @@ public class FilterController {
|
|||||||
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
|
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF contains an image",
|
summary = "Checks if a PDF contains an image",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "PDF passed filter",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "PDF did not pass filter",
|
||||||
|
content = @Content())
|
||||||
|
})
|
||||||
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
|
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
String pageNumber = request.getPageNumbers();
|
String pageNumber = request.getPageNumbers();
|
||||||
|
|
||||||
PDDocument pdfDocument = pdfDocumentFactory.load(inputFile);
|
try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) {
|
||||||
if (PdfUtils.hasImages(pdfDocument, pageNumber))
|
if (PdfUtils.hasImages(pdfDocument, pageNumber)) {
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
|
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
|
||||||
return null;
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "PDF passed filter",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "PDF did not pass filter",
|
||||||
|
content = @Content())
|
||||||
|
})
|
||||||
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
|
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
int pageCount = request.getPageCount();
|
int pageCount = request.getPageCount();
|
||||||
String comparator = request.getComparator();
|
String comparator = request.getComparator();
|
||||||
// Load the PDF
|
|
||||||
PDDocument document = pdfDocumentFactory.load(inputFile);
|
|
||||||
int actualPageCount = document.getNumberOfPages();
|
|
||||||
// Perform the comparison
|
|
||||||
boolean valid =
|
|
||||||
switch (comparator) {
|
|
||||||
case "Greater" -> actualPageCount > pageCount;
|
|
||||||
case "Equal" -> actualPageCount == pageCount;
|
|
||||||
case "Less" -> actualPageCount < pageCount;
|
|
||||||
default ->
|
|
||||||
throw ExceptionUtils.createInvalidArgumentException(
|
|
||||||
"comparator", comparator);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
boolean valid;
|
||||||
return null;
|
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
|
||||||
|
int actualPageCount = document.getNumberOfPages();
|
||||||
|
valid = compare(actualPageCount, pageCount, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
? WebResponseUtils.multiPartFileToWebResponse(inputFile)
|
||||||
|
: ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is of a certain size",
|
summary = "Checks if a PDF is of a certain size",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "PDF passed filter",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "PDF did not pass filter",
|
||||||
|
content = @Content())
|
||||||
|
})
|
||||||
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
|
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
String standardPageSize = request.getStandardPageSize();
|
String standardPageSize = request.getStandardPageSize();
|
||||||
String comparator = request.getComparator();
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
// Load the PDF
|
final boolean valid;
|
||||||
PDDocument document = pdfDocumentFactory.load(inputFile);
|
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
|
||||||
|
PDPage firstPage = document.getPage(0);
|
||||||
|
PDRectangle actualPageSize = firstPage.getMediaBox();
|
||||||
|
|
||||||
PDPage firstPage = document.getPage(0);
|
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
|
||||||
PDRectangle actualPageSize = firstPage.getMediaBox();
|
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
|
||||||
|
float standardArea = standardSize.getWidth() * standardSize.getHeight();
|
||||||
|
|
||||||
// Calculate the area of the actual page size
|
valid = compare(actualArea, standardArea, comparator);
|
||||||
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
|
}
|
||||||
|
|
||||||
// Get the standard size and calculate its area
|
return valid
|
||||||
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
|
? WebResponseUtils.multiPartFileToWebResponse(inputFile)
|
||||||
float standardArea = standardSize.getWidth() * standardSize.getHeight();
|
: ResponseEntity.noContent().build();
|
||||||
|
|
||||||
// Perform the comparison
|
|
||||||
boolean valid =
|
|
||||||
switch (comparator) {
|
|
||||||
case "Greater" -> actualArea > standardArea;
|
|
||||||
case "Equal" -> actualArea == standardArea;
|
|
||||||
case "Less" -> actualArea < standardArea;
|
|
||||||
default ->
|
|
||||||
throw ExceptionUtils.createInvalidArgumentException(
|
|
||||||
"comparator", comparator);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is a set file size",
|
summary = "Checks if a PDF is a set file size",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "PDF passed filter",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "PDF did not pass filter",
|
||||||
|
content = @Content())
|
||||||
|
})
|
||||||
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
|
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
long fileSize = request.getFileSize();
|
long fileSize = request.getFileSize();
|
||||||
String comparator = request.getComparator();
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
// Get the file size
|
|
||||||
long actualFileSize = inputFile.getSize();
|
long actualFileSize = inputFile.getSize();
|
||||||
|
boolean valid = compare(actualFileSize, fileSize, comparator);
|
||||||
|
|
||||||
// Perform the comparison
|
return valid
|
||||||
boolean valid =
|
? WebResponseUtils.multiPartFileToWebResponse(inputFile)
|
||||||
switch (comparator) {
|
: ResponseEntity.noContent().build();
|
||||||
case "Greater" -> actualFileSize > fileSize;
|
|
||||||
case "Equal" -> actualFileSize == fileSize;
|
|
||||||
case "Less" -> actualFileSize < fileSize;
|
|
||||||
default ->
|
|
||||||
throw ExceptionUtils.createInvalidArgumentException(
|
|
||||||
"comparator", comparator);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is of a certain rotation",
|
summary = "Checks if a PDF is of a certain rotation",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "PDF passed filter",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "PDF did not pass filter",
|
||||||
|
content = @Content())
|
||||||
|
})
|
||||||
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
|
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
int rotation = request.getRotation();
|
int rotation = request.getRotation();
|
||||||
String comparator = request.getComparator();
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
// Load the PDF
|
boolean valid;
|
||||||
PDDocument document = pdfDocumentFactory.load(inputFile);
|
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
|
||||||
|
PDPage firstPage = document.getPage(0);
|
||||||
|
int actualRotation = firstPage.getRotation();
|
||||||
|
valid = compare(actualRotation, rotation, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the rotation of the first page
|
return valid
|
||||||
PDPage firstPage = document.getPage(0);
|
? WebResponseUtils.multiPartFileToWebResponse(inputFile)
|
||||||
int actualRotation = firstPage.getRotation();
|
: ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the comparison
|
/**
|
||||||
boolean valid =
|
* Compares two values based on the provided comparator.
|
||||||
switch (comparator) {
|
*
|
||||||
case "Greater" -> actualRotation > rotation;
|
* @param <T> The type of the values being compared.
|
||||||
case "Equal" -> actualRotation == rotation;
|
* @param actual The actual value.
|
||||||
case "Less" -> actualRotation < rotation;
|
* @param expected The expected value.
|
||||||
default ->
|
* @param comparator The comparator to use (e.g., "Greater", "Less", "Equal").
|
||||||
throw ExceptionUtils.createInvalidArgumentException(
|
* @return True if the comparison is valid, false otherwise.
|
||||||
"comparator", comparator);
|
*/
|
||||||
};
|
private static <T extends Comparable<T>> boolean compare(
|
||||||
|
T actual, T expected, String comparator) {
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
return switch (comparator) {
|
||||||
return null;
|
case "Greater" -> actual.compareTo(expected) > 0;
|
||||||
|
case "Equal" -> actual.compareTo(expected) == 0;
|
||||||
|
case "Less" -> actual.compareTo(expected) < 0;
|
||||||
|
default ->
|
||||||
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user