remove stuff

This commit is contained in:
Anthony Stirling 2025-07-02 15:57:20 +01:00
parent ce417469d4
commit 4c2c40dc4f
8 changed files with 48 additions and 500 deletions

View File

@ -5,8 +5,8 @@ import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
/**
* Utility class for handling exceptions with consistent English error messages. Frontend will
* handle translation to user's language.
* Utility class for handling exceptions with internationalized error messages. Provides consistent
* error handling and user-friendly messages across the application.
*/
@Slf4j
public class ExceptionUtils {
@ -32,12 +32,15 @@ public class ExceptionUtils {
String message;
if (context != null && !context.isEmpty()) {
message =
String.format(
"Error %s: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.",
I18nUtils.getMessage(
"error.pdfCorruptedDuring",
"Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.",
context);
} else {
message =
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.";
I18nUtils.getMessage(
"error.pdfCorrupted",
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.");
}
return new IOException(message, cause);
}
@ -50,7 +53,9 @@ public class ExceptionUtils {
*/
public static IOException createMultiplePdfCorruptedException(Exception cause) {
String message =
"One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.";
I18nUtils.getMessage(
"error.pdfCorruptedMultiple",
"One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.");
return new IOException(message, cause);
}
@ -62,7 +67,10 @@ public class ExceptionUtils {
*/
public static IOException createPdfEncryptionException(Exception cause) {
String message =
"The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.";
I18nUtils.getMessage(
"error.pdfEncryption",
"The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.",
cause.getMessage());
return new IOException(message, cause);
}
@ -74,7 +82,9 @@ public class ExceptionUtils {
*/
public static IOException createPdfPasswordException(Exception cause) {
String message =
"The PDF Document is passworded and either the password was not provided or was incorrect";
I18nUtils.getMessage(
"error.pdfPassword",
"The PDF Document is passworded and either the password was not provided or was incorrect");
return new IOException(message, cause);
}
@ -87,74 +97,57 @@ public class ExceptionUtils {
*/
public static IOException createFileProcessingException(String operation, Exception cause) {
String message =
String.format(
"An error occurred while processing the file during %s operation: %s",
operation, cause.getMessage());
I18nUtils.getMessage(
"error.fileProcessing",
"An error occurred while processing the file during {0} operation: {1}",
operation,
cause.getMessage());
return new IOException(message, cause);
}
/**
* Create a generic IOException with readable English message.
* Create a generic IOException with internationalized message.
*
* @param messageKey the i18n message key for frontend translation
* @param defaultMessage the English message template
* @param messageKey the i18n message key
* @param defaultMessage the default message if i18n is not available
* @param cause the original exception
* @param args arguments for message formatting
* @return IOException with readable English message
* @param args optional arguments for the message
* @return IOException with user-friendly message
*/
public static IOException createIOException(
String messageKey, String defaultMessage, Exception cause, Object... args) {
String message = String.format(defaultMessage, args);
String message = I18nUtils.getMessage(messageKey, defaultMessage, args);
return new IOException(message, cause);
}
/**
* Create a generic RuntimeException with readable English message.
* Create a generic RuntimeException with internationalized message.
*
* @param messageKey the i18n message key for frontend translation
* @param defaultMessage the English message template
* @param messageKey the i18n message key
* @param defaultMessage the default message if i18n is not available
* @param cause the original exception
* @param args arguments for message formatting
* @return RuntimeException with readable English message
* @param args optional arguments for the message
* @return RuntimeException with user-friendly message
*/
public static RuntimeException createRuntimeException(
String messageKey, String defaultMessage, Exception cause, Object... args) {
String message = String.format(defaultMessage, args);
if (messageKey != null) {
return new TranslatableException(message, messageKey, args);
}
String message = I18nUtils.getMessage(messageKey, defaultMessage, args);
return new RuntimeException(message, cause);
}
/**
* Create an IllegalArgumentException with readable English message.
* Create an IllegalArgumentException with internationalized message.
*
* @param messageKey the i18n message key for frontend translation
* @param defaultMessage the English message template
* @param args arguments for message formatting
* @return IllegalArgumentException with readable English message
* @param messageKey the i18n message key
* @param defaultMessage the default message if i18n is not available
* @param args optional arguments for the message
* @return IllegalArgumentException with user-friendly message
*/
public static IllegalArgumentException createIllegalArgumentException(
String messageKey, String defaultMessage, Object... args) {
String message = String.format(defaultMessage, args);
return new TranslatableException(message, messageKey, args);
}
/** Create common validation exceptions with translation support. */
public static IllegalArgumentException createInvalidArgumentException(String argumentName) {
return createIllegalArgumentException(
"error.invalidArgument", "Invalid argument: {0}", argumentName);
}
public static IllegalArgumentException createInvalidArgumentException(
String argumentName, String value) {
return createIllegalArgumentException(
"error.invalidFormat", "Invalid {0} format: {1}", argumentName, value);
}
public static IllegalArgumentException createNullArgumentException(String argumentName) {
return createIllegalArgumentException(
"error.argumentRequired", "{0} must not be null", argumentName);
String message = I18nUtils.getMessage(messageKey, defaultMessage, args);
System.out.println("######## Test " + message);
return new IllegalArgumentException(message);
}
/** Create file validation exceptions. */
@ -319,7 +312,7 @@ public class ExceptionUtils {
public static void logException(String operation, Exception e) {
if (e instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) e)) {
log.warn("PDF corruption detected during {}: {}", operation, e.getMessage());
} else if (e instanceof IOException io && (isEncryptionError(io) || isPasswordError(io))) {
} else if (isEncryptionError((IOException) e) || isPasswordError((IOException) e)) {
log.info("PDF security issue during {}: {}", operation, e.getMessage());
} else {
log.error("Unexpected error during {}", operation, e);

View File

@ -1,25 +0,0 @@
package stirling.software.common.util;
/**
* Exception that carries translation information for frontend internationalization. The
* GlobalExceptionHandler extracts this info to create structured error responses.
*/
public class TranslatableException extends IllegalArgumentException {
private final String translationKey;
private final Object[] translationArgs;
public TranslatableException(String message, String translationKey, Object... translationArgs) {
super(message);
this.translationKey = translationKey;
this.translationArgs = translationArgs;
}
public String getTranslationKey() {
return translationKey;
}
public Object[] getTranslationArgs() {
return translationArgs;
}
}

View File

@ -1,196 +0,0 @@
# Exception Handling Guide
This guide shows how to use the centralized exception handling utilities for consistent error messages with frontend translation support.
## Architecture Overview
The system uses a **backend-frontend translation split**:
- **Backend**: Creates structured JSON error responses with translation keys and English fallbacks
- **Frontend**: Translates error messages to user's language using JavaScript
## New Utilities
### 1. ExceptionUtils
Creates `TranslatableException` instances with structured translation data for frontend.
Backend uses hardcoded English strings for readability.
### 2. GlobalExceptionHandler
Converts exceptions to structured JSON responses with translation information.
### 3. MessageFormatter.js
Frontend utility for translating error messages with placeholder replacement.
### 4. GlobalModelAdvice
Loads error translations using Spring's MessageSource and adds them to all templates.
## Usage Examples
### Basic PDF Exception Handling
**Before:**
```java
try {
// PDF operation
} catch (IOException e) {
if (PdfErrorUtils.isCorruptedPdfError(e)) {
throw new IOException("PDF file is corrupted...", e);
}
throw e;
}
```
**After:**
```java
try {
// PDF operation
} catch (IOException e) {
ExceptionUtils.logException("operation name", e);
throw ExceptionUtils.handlePdfException(e);
}
```
### Creating Specific Exception Types
```java
// PDF corruption
throw ExceptionUtils.createPdfCorruptedException(originalException);
// PDF corruption with context
throw ExceptionUtils.createPdfCorruptedException("during merge", originalException);
// Multiple PDF corruption (for merge operations)
throw ExceptionUtils.createMultiplePdfCorruptedException(originalException);
// PDF encryption issues
throw ExceptionUtils.createPdfEncryptionException(originalException);
// File processing errors
throw ExceptionUtils.createFileProcessingException("merge", originalException);
// Generic exceptions with i18n
throw ExceptionUtils.createIOException("error.customKey", "Default message", originalException, arg1, arg2);
```
### JSON Error Response Format
The system returns structured JSON error responses with translation support:
```json
{
"error": "Bad Request",
"message": "DPI value 500 exceeds maximum safe limit of 300. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
"trace": "java.lang.IllegalArgumentException: ...",
"translationKey": "error.dpiExceedsLimit",
"translationArgs": ["500", "300"]
}
```
**Key Features:**
- `message`: Readable English hardcoded in backend for API consumers
- `translationKey`: Frontend translation key
- `translationArgs`: Arguments for placeholder replacement
- Backend sends English, frontend translates to user's language
### Frontend Translation with MessageFormatter
```javascript
// Frontend translates using Spring's loaded translation data
const displayMessage = window.MessageFormatter.translate(
json.translationKey,
json.translationArgs,
json.message // English fallback
);
```
## Controller Pattern
```java
@RestController
public class MyController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@PostMapping("/process")
public ResponseEntity<byte[]> processFile(@ModelAttribute FileRequest request) throws IOException {
try {
PDDocument document = pdfDocumentFactory.load(request.getFileInput());
// Process document...
return WebResponseUtils.pdfDocToWebResponse(document, "output.pdf");
} catch (IOException e) {
ExceptionUtils.logException("file processing", e);
throw ExceptionUtils.handlePdfException(e, "during processing");
}
}
}
```
## Error Message Keys
When creating new exception messages, add the corresponding i18n keys to `messages_en_GB.properties` only. The translation scripts will automatically propagate them to other language files during merge.
### Key Categories Available:
**Core PDF Operations:**
- `error.pdfCorrupted` - General PDF corruption
- `error.pdfCorruptedDuring` - Corruption with context (takes operation parameter)
- `error.pdfEncryption` - Encryption/decryption issues
- `error.pdfPassword` - Password-related errors
**File Processing:**
- `error.fileProcessing` - Generic file operation errors (takes operation and error message)
- `error.commandFailed` - External tool failures (takes tool name)
**Validation:**
- `error.invalidArgument` - Invalid parameters (takes argument description)
- `error.invalidFormat` - Invalid file formats (takes format type)
- `error.optionsNotSpecified` - Missing required options (takes option type)
**System Requirements:**
- `error.toolNotInstalled` - Missing tools (takes tool name)
- `error.toolRequired` - Tool requirements (takes tool and operation)
### Creating New Keys:
When adding new error scenarios, follow the naming pattern:
- `error.[category].[specific]` (e.g., `error.ocr.languageRequired`)
- Keep parameter placeholders simple and translatable
- Avoid putting full sentences in `{0}` parameters
### Parameter Best Practices:
**✅ Good Examples:**
```java
// Simple identifiers or values
ExceptionUtils.createIllegalArgumentException("error.invalidArgument", "Invalid argument: {0}", "angle");
ExceptionUtils.createRuntimeException("error.commandFailed", "{0} command failed", null, "Tesseract");
ExceptionUtils.createIllegalArgumentException("error.invalidFormat", "Invalid {0} format", "PDF");
```
**❌ Bad Examples:**
```java
// Full sentences that can't be translated
ExceptionUtils.createIllegalArgumentException("error.invalidArgument", "Invalid argument: {0}", "angle must be multiple of 90");
```
**Solution for Complex Messages:**
Create specific i18n keys instead:
```java
// Instead of complex parameters, create specific keys
ExceptionUtils.createIllegalArgumentException("error.angleNotMultipleOf90", "Angle must be a multiple of 90");
```
## Testing Error Messages
```java
@Test
public void testErrorMessageLocalization() {
// Test with different locales
LocaleContextHolder.setLocale(Locale.FRENCH);
IOException exception = ExceptionUtils.createPdfCorruptedException(new RuntimeException("test"));
// Verify message is in French
assertThat(exception.getMessage()).contains("PDF");
}
```

View File

@ -1,94 +0,0 @@
package stirling.software.SPDF.config;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import lombok.extern.slf4j.Slf4j;
/**
* Global exception handler that creates structured error responses with translation information for
* frontend internationalization support.
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
public static class ErrorResponse {
public String error;
public String message;
public String trace;
public String translationKey;
public List<String> translationArgs;
public ErrorResponse(
String error,
String message,
String trace,
String translationKey,
List<String> translationArgs) {
this.error = error;
this.message = message;
this.trace = trace;
this.translationKey = translationKey;
this.translationArgs = translationArgs;
}
}
@ExceptionHandler(stirling.software.common.util.TranslatableException.class)
public ResponseEntity<ErrorResponse> handleTranslatableException(
stirling.software.common.util.TranslatableException e) {
List<String> translationArgs = null;
if (e.getTranslationArgs() != null) {
translationArgs = Arrays.stream(e.getTranslationArgs()).map(String::valueOf).toList();
}
ErrorResponse errorResponse =
new ErrorResponse(
"Bad Request",
e.getMessage(),
getStackTrace(e),
e.getTranslationKey(),
translationArgs);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(
IllegalArgumentException e) {
ErrorResponse errorResponse =
new ErrorResponse("Bad Request", e.getMessage(), getStackTrace(e), null, null);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e) {
ErrorResponse errorResponse =
new ErrorResponse(
"Internal Server Error", e.getMessage(), getStackTrace(e), null, null);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
ErrorResponse errorResponse =
new ErrorResponse(
"Internal Server Error", e.getMessage(), getStackTrace(e), null, null);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
private String getStackTrace(Exception e) {
if (e.getCause() != null) {
return e.getCause().toString();
}
return e.getClass().getSimpleName() + ": " + e.getMessage();
}
}

View File

@ -1,61 +0,0 @@
package stirling.software.SPDF.controller.api;
import java.util.Arrays;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* API endpoint for on-demand error message translation.
* Provides translations for error messages when needed instead of pre-loading all translations.
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class TranslationController {
private final MessageSource messageSource;
/**
* Get translated error message for user's locale.
*
* @param key the translation key (e.g. "error.dpiExceedsLimit")
* @param args comma-separated arguments for message formatting
* @return translated message in user's locale
*/
@GetMapping("/translate")
public ResponseEntity<String> translate(
@RequestParam String key,
@RequestParam(required = false) String args) {
try {
Object[] messageArgs = null;
if (args != null && !args.trim().isEmpty()) {
messageArgs = Arrays.stream(args.split(","))
.map(String::trim)
.toArray();
}
String translatedMessage = messageSource.getMessage(
key,
messageArgs,
LocaleContextHolder.getLocale()
);
return ResponseEntity.ok(translatedMessage);
} catch (Exception e) {
log.debug("Translation failed for key '{}': {}", key, e.getMessage());
return ResponseEntity.notFound().build();
}
}
}

View File

@ -330,9 +330,7 @@
async function handleJsonResponse(response) {
const json = await response.json();
// Check for password-related errors first
const errorMessage = json.message || '';
const errorMessage = JSON.stringify(json, null, 2);
if (
errorMessage.toLowerCase().includes('the password is incorrect') ||
errorMessage.toLowerCase().includes('Password is not provided') ||
@ -342,24 +340,9 @@
firstErrorOccurred = true;
alert(pdfPasswordPrompt);
}
return;
}
// Handle structured error response with async translation
let displayMessage = 'Loading...'; // Brief loading state
if (json.translationKey && window.MessageFormatter) {
// Async translation with timeout and English fallback
displayMessage = await window.MessageFormatter.translateAsync(
json.translationKey,
json.translationArgs,
json.message // English fallback
);
} else {
displayMessage = json.message; // Direct English message
showErrorBanner(json.error + ':' + json.message, json.trace);
}
showErrorBanner((json.error || 'Error') + ': ' + displayMessage, json.trace || '');
}
async function handleResponse(blob, filename, considerViewOptions = false, isZip = false) {

View File

@ -1,51 +0,0 @@
/**
* Async translation utility with timeout and English fallback.
* Fetches translations on-demand from API with brief loading state.
*/
window.MessageFormatter = (function() {
'use strict';
/**
* Translate error message with async API call and fallback to English.
* Shows brief loading, attempts translation, falls back to English on timeout/error.
*
* @param {string} translationKey - The translation key
* @param {Array} translationArgs - Arguments for message formatting
* @param {string} fallbackMessage - English fallback message
* @param {number} timeout - Timeout in milliseconds (default: 500ms)
* @returns {Promise<string>} - Translated message or English fallback
*/
async function translateAsync(translationKey, translationArgs, fallbackMessage, timeout = 500) {
if (!translationKey) {
return fallbackMessage;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const params = new URLSearchParams({ key: translationKey });
if (translationArgs && translationArgs.length > 0) {
params.append('args', translationArgs.join(','));
}
const response = await fetch(`${window.stirlingPDF.translationApiUrl}?${params}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Translation API returned ${response.status}`);
}
return await response.text();
} catch (error) {
clearTimeout(timeoutId);
console.debug('Translation failed, using English fallback:', error.message);
return fallbackMessage;
}
}
return { translateAsync };
})();

View File

@ -86,7 +86,6 @@
<script th:src="@{'/js/tab-container.js'}"></script>
<script th:src="@{'/js/darkmode.js'}"></script>
<script th:src="@{'/js/csrf.js'}"></script>
<script th:src="@{'/js/messageFormatter.js'}"></script>
<script th:inline="javascript">
function UpdatePosthogConsent(){
@ -251,8 +250,8 @@
window.stirlingPDF.uploadLimit = /*[[${@uploadLimitService.getUploadLimit()}]]*/ 0;
window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is';
window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is';
// Translation API base URL for async error message translation
window.stirlingPDF.translationApiUrl = '/api/translate';
window.stirlingPDF.pdfCorruptedMessage = /*[[#{error.pdfInvalid}]]*/ 'The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the \'Repair PDF\' feature to fix the file before proceeding.';
window.stirlingPDF.tryRepairMessage = /*[[#{error.tryRepair}]]*/ 'Try using the Repair PDF feature to fix corrupted files.';
})();
</script>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>