This commit is contained in:
Anthony Stirling 2025-09-17 12:11:31 +01:00
parent 5930e26a83
commit 1c2280a29b
13 changed files with 224 additions and 446 deletions

View File

@ -0,0 +1,188 @@
package stirling.software.SPDF.config;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.stereotype.Component;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
/**
* Global OpenAPI customizer that adds standard error responses (400, 413, 422, 500) to all API
* operations under /api/v1/** paths.
*/
@Component
public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
@Override
public void customise(OpenAPI openApi) {
if (openApi.getPaths() == null) {
return;
}
openApi.getPaths()
.forEach(
(path, pathItem) -> {
if (path.startsWith("/api/v1/")) {
addErrorResponsesToPathItem(pathItem);
}
});
}
private void addErrorResponsesToPathItem(PathItem pathItem) {
if (pathItem.getPost() != null) {
addStandardErrorResponses(pathItem.getPost());
}
if (pathItem.getPut() != null) {
addStandardErrorResponses(pathItem.getPut());
}
if (pathItem.getPatch() != null) {
addStandardErrorResponses(pathItem.getPatch());
}
if (pathItem.getDelete() != null) {
addStandardErrorResponses(pathItem.getDelete());
}
if (pathItem.getGet() != null) {
addStandardErrorResponses(pathItem.getGet());
}
}
private void addStandardErrorResponses(Operation operation) {
if (operation.getResponses() == null) {
return;
}
// Only add error responses if they don't already exist
if (!operation.getResponses().containsKey("400")) {
operation.getResponses().addApiResponse("400", create400Response());
}
if (!operation.getResponses().containsKey("413")) {
operation.getResponses().addApiResponse("413", create413Response());
}
if (!operation.getResponses().containsKey("422")) {
operation.getResponses().addApiResponse("422", create422Response());
}
if (!operation.getResponses().containsKey("500")) {
operation.getResponses().addApiResponse("500", create500Response());
}
}
private ApiResponse create400Response() {
return new ApiResponse()
.description(
"Bad request - Invalid input parameters, unsupported format, or corrupted file")
.content(
new Content()
.addMediaType(
"application/json",
new MediaType()
.schema(
createErrorSchema(
400,
"Invalid input parameters or corrupted file",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
400,
"Invalid input parameters or corrupted file",
"/api/v1/example/endpoint"))));
}
private ApiResponse create413Response() {
return new ApiResponse()
.description("Payload too large - File exceeds maximum allowed size")
.content(
new Content()
.addMediaType(
"application/json",
new MediaType()
.schema(
createErrorSchema(
413,
"File size exceeds maximum allowed limit",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
413,
"File size exceeds maximum allowed limit",
"/api/v1/example/endpoint"))));
}
private ApiResponse create422Response() {
return new ApiResponse()
.description("Unprocessable entity - File is valid but cannot be processed")
.content(
new Content()
.addMediaType(
"application/json",
new MediaType()
.schema(
createErrorSchema(
422,
"File is valid but cannot be processed",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
422,
"File is valid but cannot be processed",
"/api/v1/example/endpoint"))));
}
private ApiResponse create500Response() {
return new ApiResponse()
.description("Internal server error - Unexpected error during processing")
.content(
new Content()
.addMediaType(
"application/json",
new MediaType()
.schema(
createErrorSchema(
500,
"Unexpected error during processing",
"/api/v1/example/endpoint"))
.example(
createErrorExample(
500,
"Unexpected error during processing",
"/api/v1/example/endpoint"))));
}
private Schema<?> createErrorSchema(int status, String message, String path) {
return new Schema<>()
.type("object")
.addProperty("status", new Schema<>().type("integer").example(status))
.addProperty("error", new Schema<>().type("string").example(getErrorType(status)))
.addProperty("message", new Schema<>().type("string").example(message))
.addProperty(
"timestamp",
new Schema<>()
.type("string")
.format("date-time")
.example("2024-01-15T10:30:00Z"))
.addProperty("path", new Schema<>().type("string").example(path));
}
private Object createErrorExample(int status, String message, String path) {
return java.util.Map.of(
"status", status,
"error", getErrorType(status),
"message", message,
"timestamp", "2024-01-15T10:30:00Z",
"path", path);
}
private String getErrorType(int status) {
return switch (status) {
case 400 -> "Bad Request";
case 413 -> "Payload Too Large";
case 422 -> "Unprocessable Entity";
case 500 -> "Internal Server Error";
default -> "Error";
};
}
}

View File

@ -8,6 +8,7 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
@ -60,15 +61,40 @@ public class OpenApiConfig {
openAPI.addServersItem(server);
}
// Add ErrorResponse schema to components
Schema<?> errorResponseSchema =
new Schema<>()
.type("object")
.addProperty(
"timestamp",
new Schema<>()
.type("string")
.format("date-time")
.description("Error timestamp"))
.addProperty(
"status",
new Schema<>().type("integer").description("HTTP status code"))
.addProperty(
"error", new Schema<>().type("string").description("Error type"))
.addProperty(
"message",
new Schema<>().type("string").description("Error message"))
.addProperty(
"path", new Schema<>().type("string").description("Request path"))
.description("Standard error response format");
Components components = new Components().addSchemas("ErrorResponse", errorResponseSchema);
if (!applicationProperties.getSecurity().getEnableLogin()) {
return openAPI.components(new Components());
return openAPI.components(components);
} else {
SecurityScheme apiKeyScheme =
new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-KEY");
return openAPI.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
components.addSecuritySchemes("apiKey", apiKeyScheme);
return openAPI.components(components)
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
}
}

View File

@ -1,69 +0,0 @@
package stirling.software.SPDF.config.swagger;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* API response annotation for conversion operations that output non-PDF formats. Use for PDF to
* Word, Excel, PowerPoint, text, HTML, CSV, etc.
*
* <p>Specify the output formats this endpoint supports.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ConversionResponse {
/**
* The output formats this conversion endpoint supports. Use OutputFormat enum values to specify
* supported formats.
*/
OutputFormat[] value() default {OutputFormat.DOCX, OutputFormat.TXT, OutputFormat.RTF};
/** Supported output formats for conversion operations */
enum OutputFormat {
DOCX(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"Microsoft Word document (DOCX)",
"binary"),
DOC("application/msword", "Microsoft Word document (DOC)", "binary"),
XLSX(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Microsoft Excel document (XLSX)",
"binary"),
PPTX(
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"Microsoft PowerPoint document (PPTX)",
"binary"),
RTF("application/rtf", "Rich Text Format document", "binary"),
TXT("text/plain", "Plain text content", "string"),
HTML("text/html", "HTML content", "string"),
CSV("text/csv", "CSV data", "string"),
XML("application/xml", "XML document", "string"),
JSON("application/json", "JSON data", "string"),
BINARY("application/octet-stream", "Binary file output", "binary");
private final String mediaType;
private final String description;
private final String schemaFormat;
OutputFormat(String mediaType, String description, String schemaFormat) {
this.mediaType = mediaType;
this.description = description;
this.schemaFormat = schemaFormat;
}
public String getMediaType() {
return mediaType;
}
public String getDescription() {
return description;
}
public String getSchemaFormat() {
return schemaFormat;
}
}
}

View File

@ -29,36 +29,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"CSV file containing extracted table data"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but contains no extractable table data",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error - Unexpected error during CSV extraction",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
"CSV file containing extracted table data")))
})
public @interface CsvConversionResponse {}

View File

@ -28,36 +28,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "string",
format = "binary",
description = "HTML file converted from PDF"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be converted to HTML format",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error - Unexpected error during HTML conversion",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
description = "HTML file converted from PDF")))
})
public @interface HtmlConversionResponse {}

View File

@ -29,37 +29,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"JavaScript code extracted from PDF"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but contains no JavaScript code",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description =
"Internal server error - Unexpected error during JavaScript extraction",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
"JavaScript code extracted from PDF")))
})
public @interface JavaScriptResponse {}

View File

@ -28,36 +28,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "object",
description =
"JSON object containing the requested data or analysis results"))),
@ApiResponse(
responseCode = "400",
description = "Bad request - Invalid input parameters or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be analyzed or processed",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description =
"Internal server error - Unexpected error during analysis or processing",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
"JSON object containing the requested data or analysis results")))
})
public @interface JsonDataResponse {}

View File

@ -29,37 +29,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description =
"Markdown file converted from PDF"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be converted to Markdown format",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description =
"Internal server error - Unexpected error during Markdown conversion",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
"Markdown file converted from PDF")))
})
public @interface MarkdownConversionResponse {}

View File

@ -52,35 +52,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description = "Single image file (JPEG)"))
}),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported file format, or corrupted files",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - Files exceed maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description = "Unprocessable entity - Files are valid but cannot be processed",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error - Unexpected error during file processing",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
})
})
public @interface MultiFileResponse {}

View File

@ -1,72 +0,0 @@
package stirling.software.SPDF.config.swagger;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* API response annotation for PDF to Microsoft Office format conversions. Specify the exact output
* format(s) this endpoint supports.
*
* <p>Usage: @OfficeConversionResponse(OfficeFormat.DOCX) @OfficeConversionResponse({OfficeFormat.DOCX,
* OfficeFormat.DOC})
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OfficeConversionResponse {
/** The Office formats this endpoint supports. */
OfficeFormat[] value();
/** Supported Microsoft Office output formats */
enum OfficeFormat {
DOCX(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"Microsoft Word document (DOCX)",
"Word document"),
DOC("application/msword", "Microsoft Word document (DOC)", "Word document (legacy)"),
XLSX(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Microsoft Excel document (XLSX)",
"Excel spreadsheet"),
PPTX(
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"Microsoft PowerPoint document (PPTX)",
"PowerPoint presentation"),
ODT(
"application/vnd.oasis.opendocument.text",
"OpenDocument Text document (ODT)",
"OpenDocument text"),
ODS(
"application/vnd.oasis.opendocument.spreadsheet",
"OpenDocument Spreadsheet (ODS)",
"OpenDocument spreadsheet"),
ODP(
"application/vnd.oasis.opendocument.presentation",
"OpenDocument Presentation (ODP)",
"OpenDocument presentation");
private final String mediaType;
private final String description;
private final String shortDescription;
OfficeFormat(String mediaType, String description, String shortDescription) {
this.mediaType = mediaType;
this.description = description;
this.shortDescription = shortDescription;
}
public String getMediaType() {
return mediaType;
}
public String getDescription() {
return description;
}
public String getShortDescription() {
return shortDescription;
}
}
}

View File

@ -28,36 +28,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "string",
format = "binary",
description = "The processed PDF file"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported file format, or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be processed (e.g., encrypted, corrupted)",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error - Unexpected error during PDF processing",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
description = "The processed PDF file")))
})
public @interface StandardPdfResponse {}

View File

@ -1,54 +0,0 @@
package stirling.software.SPDF.config.swagger;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* API response annotation for PDF to text/document format conversions. Specify the exact output
* format(s) this endpoint supports.
*
* <p>Usage: @TextConversionResponse(TextFormat.TXT) @TextConversionResponse({TextFormat.TXT,
* TextFormat.RTF})
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TextConversionResponse {
/** The text/document formats this endpoint supports. */
TextFormat[] value();
/** Supported text and document output formats */
enum TextFormat {
TXT("text/plain", "Plain text file", "Plain text"),
RTF("application/rtf", "Rich Text Format document", "RTF document"),
HTML("text/html", "HTML document", "HTML file"),
XML("application/xml", "XML document", "XML file"),
CSV("text/csv", "Comma-separated values file", "CSV data"),
JSON("application/json", "JSON document", "JSON data"),
MARKDOWN("text/markdown", "Markdown document", "Markdown file");
private final String mediaType;
private final String description;
private final String shortDescription;
TextFormat(String mediaType, String description, String shortDescription) {
this.mediaType = mediaType;
this.description = description;
this.shortDescription = shortDescription;
}
public String getMediaType() {
return mediaType;
}
public String getDescription() {
return description;
}
public String getShortDescription() {
return shortDescription;
}
}
}

View File

@ -28,36 +28,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "string",
format = "binary",
description = "XML file converted from PDF"))),
@ApiResponse(
responseCode = "400",
description =
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "413",
description = "Payload too large - File exceeds maximum allowed size",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "422",
description =
"Unprocessable entity - PDF is valid but cannot be converted to XML format",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error - Unexpected error during XML conversion",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
description = "XML file converted from PDF")))
})
public @interface XmlConversionResponse {}