refactor(common,core,proprietary): standardize Locale.ROOT usage for case/format & safer string handling (#4628)

# Description of Changes

- Standardized all locale-sensitive operations to use `Locale.ROOT`:
- Replaced `toLowerCase()/toUpperCase()` and `String.format(...)` with
`Locale.ROOT` variants across services, controllers, utils, and tests
(e.g., `InstallationPathConfig`, `ApplicationProperties`,
`ResourceMonitor`, `ChecksumUtils`, `PdfUtils`, `UploadLimitService`).
- Hardened comparisons and parsing:
- Normalized host/domain and file-extension checks with
`toLowerCase(Locale.ROOT)`; switched several `equals` calls to
constant-first style (e.g., content types, security domain checks).
- Logging & formatting improvements:
- Ensured percent/size values and hex formatting use root-locale
formatting to avoid locale-dependent output.
- Code quality & readability:
  - Converted multiple if/else ladders to modern `switch` expressions.
- Minor refactors (method references, early returns), removed redundant
returns, and clarified log messages.
- Minor fixes/behavioral nits:
- Normalized printer selection by lowercasing the searched name once;
made some equality checks null-safe/constant-first; added missing
`@Override` annotations where appropriate.

## Why

- Consistent use of `Locale.ROOT` avoids surprises in different
user/system locales (e.g., Turkish-I issues), makes string comparisons
deterministic, and keeps numeric/hex formatting stable across
environments.

---

## 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.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
Ludy
2025-11-04 10:30:51 +00:00
committed by GitHub
co-authored by Anthony Stirling James Brunton
parent 7f801157c8
commit 6e82f124a4
61 changed files with 478 additions and 348 deletions
@@ -2,6 +2,7 @@ package stirling.software.common.configuration;
import java.io.File;
import java.nio.file.Paths;
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
@@ -59,7 +60,7 @@ public class InstallationPathConfig {
private static String initializeBasePath() {
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
String os = System.getProperty("os.name").toLowerCase();
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
if (os.contains("win")) {
return Paths.get(
System.getenv("APPDATA"), // parent path
@@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@@ -286,7 +287,7 @@ public class ApplicationProperties {
private KeycloakProvider keycloak = new KeycloakProvider();
public Provider get(String registrationId) throws UnsupportedProviderException {
return switch (registrationId.toLowerCase()) {
return switch (registrationId.toLowerCase(Locale.ROOT)) {
case "google" -> getGoogle();
case "github" -> getGithub();
case "keycloak" -> getKeycloak();
@@ -30,11 +30,11 @@ public class FileInfo {
// Formats the file size into a human-readable string.
public String getFormattedFileSize() {
if (fileSize >= 1024 * 1024 * 1024) {
return String.format(Locale.US, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
return String.format(Locale.ROOT, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
} else if (fileSize >= 1024 * 1024) {
return String.format(Locale.US, "%.2f MB", fileSize / (1024.0 * 1024));
return String.format(Locale.ROOT, "%.2f MB", fileSize / (1024.0 * 1024));
} else if (fileSize >= 1024) {
return String.format(Locale.US, "%.2f KB", fileSize / 1024.0);
return String.format(Locale.ROOT, "%.2f KB", fileSize / 1024.0);
} else {
return String.format("%d Bytes", fileSize);
}
@@ -7,6 +7,7 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.pdfbox.Loader;
@@ -249,7 +250,7 @@ public class CustomPDFDocumentFactory {
log.debug(
"Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB",
actualFreeMemory / (1024 * 1024),
String.format("%.2f", freeMemoryPercent),
String.format(Locale.ROOT, "%.2f", freeMemoryPercent),
usedMemory / (1024 * 1024),
maxMemory / (1024 * 1024));
@@ -258,7 +259,7 @@ public class CustomPDFDocumentFactory {
|| actualFreeMemory < MIN_FREE_MEMORY_BYTES) {
log.debug(
"Low memory detected ({}%), forcing file-based cache",
String.format("%.2f", freeMemoryPercent));
String.format(Locale.ROOT, "%.2f", freeMemoryPercent));
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
} else if (contentSize < SMALL_FILE_THRESHOLD) {
log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024);
@@ -1,6 +1,7 @@
package stirling.software.common.service;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -440,7 +441,7 @@ public class JobExecutorService {
double numericValue = Double.parseDouble(value);
return switch (unit.toLowerCase()) {
return switch (unit.toLowerCase(Locale.ROOT)) {
case "s" -> (long) (numericValue * 1000);
case "m" -> (long) (numericValue * 60 * 1000);
case "h" -> (long) (numericValue * 60 * 60 * 1000);
@@ -397,7 +397,7 @@ public class PostHogService {
if (hardwareAddress != null) {
String[] hexadecimal = new String[hardwareAddress.length];
for (int i = 0; i < hardwareAddress.length; i++) {
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
hexadecimal[i] = String.format(Locale.ROOT, "%02X", hardwareAddress[i]);
}
return String.join("-", hexadecimal);
}
@@ -5,6 +5,7 @@ import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.time.Duration;
import java.time.Instant;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -173,8 +174,8 @@ public class ResourceMonitor {
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
log.info(
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
String.format("%.1f", cpuUsage * 100),
String.format("%.1f", memoryUsage * 100),
String.format(Locale.ROOT, "%.1f", cpuUsage * 100),
String.format(Locale.ROOT, "%.1f", memoryUsage * 100),
freeMemory / (1024 * 1024));
}
} catch (Exception e) {
@@ -5,6 +5,7 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
@@ -83,7 +84,7 @@ public class SsrfProtectionService {
return false;
}
return config.getAllowedDomains().contains(host.toLowerCase());
return config.getAllowedDomains().contains(host.toLowerCase(Locale.ROOT));
} catch (Exception e) {
log.debug("Failed to parse URL for MAX security check: {}", url, e);
@@ -101,7 +102,7 @@ public class SsrfProtectionService {
return false;
}
String hostLower = host.toLowerCase();
String hostLower = host.toLowerCase(Locale.ROOT);
// Check explicit blocked domains
if (config.getBlockedDomains().contains(hostLower)) {
@@ -111,7 +112,7 @@ public class SsrfProtectionService {
// Check internal TLD patterns
for (String tld : config.getInternalTlds()) {
if (hostLower.endsWith(tld.toLowerCase())) {
if (hostLower.endsWith(tld.toLowerCase(Locale.ROOT))) {
log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url);
return false;
}
@@ -123,9 +124,11 @@ public class SsrfProtectionService {
config.getAllowedDomains().stream()
.anyMatch(
domain ->
hostLower.equals(domain.toLowerCase())
hostLower.equals(domain.toLowerCase(Locale.ROOT))
|| hostLower.endsWith(
"." + domain.toLowerCase()));
"."
+ domain.toLowerCase(
Locale.ROOT)));
if (!isAllowed) {
log.debug("URL not in allowed domains list: {}", url);
@@ -7,6 +7,7 @@ import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
@@ -101,14 +102,16 @@ public class TaskManager {
if (!extractedFiles.isEmpty()) {
jobResult.completeWithFiles(extractedFiles);
log.debug(
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
"Set multiple file results for job ID: {} with {} files extracted from"
+ " ZIP",
jobId,
extractedFiles.size());
return;
}
} catch (Exception e) {
log.warn(
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
"Failed to extract ZIP file for job {}: {}. Falling back to single file"
+ " result.",
jobId,
e.getMessage());
}
@@ -342,12 +345,12 @@ public class TaskManager {
/** Check if a file is a ZIP file based on content type and filename */
private boolean isZipFile(String contentType, String fileName) {
if (contentType != null
&& (contentType.equals("application/zip")
|| contentType.equals("application/x-zip-compressed"))) {
&& ("application/zip".equals(contentType)
|| "application/x-zip-compressed".equals(contentType))) {
return true;
}
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
if (fileName != null && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
return true;
}
@@ -414,7 +417,7 @@ public class TaskManager {
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
String lowerName = fileName.toLowerCase();
String lowerName = fileName.toLowerCase(Locale.ROOT);
if (lowerName.endsWith(".pdf")) {
return MediaType.APPLICATION_PDF_VALUE;
} else if (lowerName.endsWith(".txt")) {
@@ -6,6 +6,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import org.apache.commons.io.FilenameUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -60,10 +61,9 @@ public class CbrUtils {
e.getMessage());
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat",
"Invalid or corrupted CBR/RAR archive. "
+ "The file may be corrupted, use an unsupported RAR format (RAR5+), "
+ "or may not be a valid RAR archive. "
+ "Please ensure the file is a valid RAR archive.");
"Invalid or corrupted CBR/RAR archive. The file may be corrupted, use"
+ " an unsupported RAR format (RAR5+), or may not be a valid RAR"
+ " archive. Please ensure the file is a valid RAR archive.");
} catch (RarException e) {
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
String errorMessage;
@@ -73,13 +73,14 @@ public class CbrUtils {
errorMessage = "Encrypted CBR/RAR archives are not supported.";
} else if (exMessage.isEmpty()) {
errorMessage =
"Invalid CBR/RAR archive. "
+ "The file may be encrypted, corrupted, or use an unsupported format.";
"Invalid CBR/RAR archive. The file may be encrypted, corrupted, or"
+ " use an unsupported format.";
} else {
errorMessage =
"Invalid CBR/RAR archive: "
+ exMessage
+ ". The file may be encrypted, corrupted, or use an unsupported format.";
+ ". The file may be encrypted, corrupted, or use an"
+ " unsupported format.";
}
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat", errorMessage);
@@ -121,7 +122,8 @@ public class CbrUtils {
if (imageEntries.isEmpty()) {
throw ExceptionUtils.createIllegalArgumentException(
"error.fileProcessing",
"No valid images found in the CBR file. The archive may be empty or contain no supported image formats.");
"No valid images found in the CBR file. The archive may be empty or"
+ " contain no supported image formats.");
}
for (ImageEntryData imageEntry : imageEntries) {
@@ -146,7 +148,8 @@ public class CbrUtils {
if (document.getNumberOfPages() == 0) {
throw ExceptionUtils.createIllegalArgumentException(
"error.fileProcessing",
"No images could be processed from the CBR file. All images may be corrupted or in unsupported formats.");
"No images could be processed from the CBR file. All images may be"
+ " corrupted or in unsupported formats.");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -159,7 +162,6 @@ public class CbrUtils {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
return pdfBytes;
}
}
@@ -178,7 +180,7 @@ public class CbrUtils {
throw new IllegalArgumentException("File must have a name");
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
throw new IllegalArgumentException("File must be a CBR or RAR archive");
}
@@ -190,7 +192,7 @@ public class CbrUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "cbr".equals(extension) || "rar".equals(extension);
}
@@ -8,6 +8,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -119,7 +120,6 @@ public class CbzUtils {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
return pdfBytes;
}
}
@@ -138,7 +138,7 @@ public class CbzUtils {
throw new IllegalArgumentException("File must have a name");
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
}
@@ -150,7 +150,7 @@ public class CbzUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "cbz".equals(extension) || "zip".equals(extension);
}
@@ -160,7 +160,7 @@ public class CbzUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "cbz".equals(extension)
|| "zip".equals(extension)
|| "cbr".equals(extension)
@@ -58,14 +58,11 @@ public class ChecksumUtils {
* @throws IOException if reading from the stream fails
*/
public static String checksum(InputStream is, String algorithm) throws IOException {
switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32":
return checksumChecksum(is, new CRC32());
case "ADLER32":
return checksumChecksum(is, new Adler32());
default:
return toHex(checksumBytes(is, algorithm));
}
return switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32" -> checksumChecksum(is, new CRC32());
case "ADLER32" -> checksumChecksum(is, new Adler32());
default -> toHex(checksumBytes(is, algorithm));
};
}
/**
@@ -98,14 +95,13 @@ public class ChecksumUtils {
* @throws IOException if reading from the stream fails
*/
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32":
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
case "ADLER32":
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
default:
return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
}
return switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32" ->
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
case "ADLER32" ->
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
default -> Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
};
}
/**
@@ -179,7 +175,7 @@ public class ChecksumUtils {
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
// Keep value as long and mask to ensure unsigned hex formatting.
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
results.put(entry.getKey(), String.format("%08x", unsigned32));
results.put(entry.getKey(), String.format(Locale.ROOT, "%08x", unsigned32));
}
return results;
}
@@ -258,7 +254,7 @@ public class ChecksumUtils {
}
// Keep as long and mask to ensure correct unsigned representation.
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
return String.format("%08x", unsigned32);
return String.format(Locale.ROOT, "%08x", unsigned32);
}
/**
@@ -294,7 +290,7 @@ public class ChecksumUtils {
private static String toHex(byte[] hash) {
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(String.format("%02x", b));
sb.append(String.format(Locale.ROOT, "%02x", b));
}
return sb.toString();
}
@@ -11,6 +11,7 @@ import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Pattern;
@@ -229,7 +230,8 @@ public class EmlParser {
Method getContentType = message.getClass().getMethod("getContentType");
String contentType = (String) getContentType.invoke(message);
if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) {
if (contentType != null
&& contentType.toLowerCase(Locale.ROOT).contains(TEXT_HTML)) {
content.setHtmlBody(stringContent);
} else {
content.setTextBody(stringContent);
@@ -296,7 +298,7 @@ public class EmlParser {
String contentType = (String) getContentType.invoke(part);
String normalizedDisposition =
disposition != null ? ((String) disposition).toLowerCase() : null;
disposition != null ? ((String) disposition).toLowerCase(Locale.ROOT) : null;
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
Object partContent = getContent.invoke(part);
@@ -422,7 +424,7 @@ public class EmlParser {
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
if (line.toLowerCase(Locale.ROOT).startsWith(headerName.toLowerCase(Locale.ROOT))) {
StringBuilder value =
new StringBuilder(line.substring(headerName.length()).trim());
for (int j = i + 1; j < lines.length; j++) {
@@ -444,7 +446,7 @@ public class EmlParser {
private static String extractHtmlBody(String emlContent) {
try {
String lowerContent = emlContent.toLowerCase();
String lowerContent = emlContent.toLowerCase(Locale.ROOT);
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
if (htmlStart == -1) return null;
@@ -463,7 +465,7 @@ public class EmlParser {
private static String extractTextBody(String emlContent) {
try {
String lowerContent = emlContent.toLowerCase();
String lowerContent = emlContent.toLowerCase(Locale.ROOT);
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
if (textStart == -1) {
int bodyStart = emlContent.indexOf("\r\n\r\n");
@@ -516,7 +518,7 @@ public class EmlParser {
String currentEncoding = "";
for (String line : lines) {
String lowerLine = line.toLowerCase().trim();
String lowerLine = line.toLowerCase(Locale.ROOT).trim();
if (line.trim().isEmpty()) {
inHeaders = false;
@@ -554,9 +556,12 @@ public class EmlParser {
}
private static boolean isAttachment(String disposition, String filename, String contentType) {
return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty())
|| (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/"))
|| (contentType.toLowerCase().contains("application/") && !filename.isEmpty());
return (disposition.toLowerCase(Locale.ROOT).contains(DISPOSITION_ATTACHMENT)
&& !filename.isEmpty())
|| (!filename.isEmpty()
&& !contentType.toLowerCase(Locale.ROOT).startsWith("text/"))
|| (contentType.toLowerCase(Locale.ROOT).contains("application/")
&& !filename.isEmpty());
}
private static String extractFilenameFromDisposition(String disposition) {
@@ -565,8 +570,8 @@ public class EmlParser {
}
// Handle filename*= (RFC 2231 encoded filename)
if (disposition.toLowerCase().contains("filename*=")) {
int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10;
if (disposition.toLowerCase(Locale.ROOT).contains("filename*=")) {
int filenameStarStart = disposition.toLowerCase(Locale.ROOT).indexOf("filename*=") + 10;
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
String extendedFilename =
@@ -586,7 +591,7 @@ public class EmlParser {
}
// Handle regular filename=
int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9;
int filenameStart = disposition.toLowerCase(Locale.ROOT).indexOf("filename=") + 9;
int filenameEnd = disposition.indexOf(";", filenameStart);
if (filenameEnd == -1) filenameEnd = disposition.length();
String filename = disposition.substring(filenameStart, filenameEnd).trim();
@@ -109,12 +109,13 @@ public class EmlProcessingUtils {
html.append(
String.format(
Locale.ROOT,
"""
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<title>%s</title>
<style>
""",
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<title>%s</title>
<style>
""",
sanitizeText(content.getSubject(), customHtmlSanitizer)));
appendEnhancedStyles(html);
@@ -127,14 +128,15 @@ public class EmlProcessingUtils {
html.append(
String.format(
Locale.ROOT,
"""
<div class="email-container">
<div class="email-header">
<h1>%s</h1>
<div class="email-meta">
<div><strong>From:</strong> %s</div>
<div><strong>To:</strong> %s</div>
""",
<div class="email-container">
<div class="email-header">
<h1>%s</h1>
<div class="email-meta">
<div><strong>From:</strong> %s</div>
<div><strong>To:</strong> %s</div>
""",
sanitizeText(content.getSubject(), customHtmlSanitizer),
sanitizeText(content.getFrom(), customHtmlSanitizer),
sanitizeText(content.getTo(), customHtmlSanitizer)));
@@ -142,6 +144,7 @@ public class EmlProcessingUtils {
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
html.append(
String.format(
Locale.ROOT,
"<div><strong>CC:</strong> %s</div>\n",
sanitizeText(content.getCc(), customHtmlSanitizer)));
}
@@ -149,6 +152,7 @@ public class EmlProcessingUtils {
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
html.append(
String.format(
Locale.ROOT,
"<div><strong>BCC:</strong> %s</div>\n",
sanitizeText(content.getBcc(), customHtmlSanitizer)));
}
@@ -156,11 +160,13 @@ public class EmlProcessingUtils {
if (content.getDate() != null) {
html.append(
String.format(
Locale.ROOT,
"<div><strong>Date:</strong> %s</div>\n",
PdfAttachmentHandler.formatEmailDate(content.getDate())));
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
html.append(
String.format(
Locale.ROOT,
"<div><strong>Date:</strong> %s</div>\n",
sanitizeText(content.getDateString(), customHtmlSanitizer)));
}
@@ -175,6 +181,7 @@ public class EmlProcessingUtils {
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
html.append(
String.format(
Locale.ROOT,
"<div class=\"text-body\">%s</div>",
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
} else {
@@ -234,14 +241,16 @@ public class EmlProcessingUtils {
.getUrlLinkPattern()
.matcher(html)
.replaceAll(
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration:"
+ " underline;\">$1</a>");
html =
RegexPatternUtils.getInstance()
.getEmailLinkPattern()
.matcher(html)
.replaceAll(
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration:"
+ " underline;\">$1</a>");
return html;
}
@@ -249,127 +258,128 @@ public class EmlProcessingUtils {
private static void appendEnhancedStyles(StringBuilder html) {
String css =
String.format(
Locale.ROOT,
"""
body {
font-family: %s;
font-size: %dpx;
line-height: %s;
color: %s;
margin: 0;
padding: 16px;
background-color: %s;
}
body {
font-family: %s;
font-size: %dpx;
line-height: %s;
color: %s;
margin: 0;
padding: 16px;
background-color: %s;
}
.email-container {
width: 100%%;
max-width: 100%%;
margin: 0 auto;
}
.email-container {
width: 100%%;
max-width: 100%%;
margin: 0 auto;
}
.email-header {
padding-bottom: 10px;
border-bottom: 1px solid %s;
margin-bottom: 10px;
}
.email-header {
padding-bottom: 10px;
border-bottom: 1px solid %s;
margin-bottom: 10px;
}
.email-header h1 {
margin: 0 0 10px 0;
font-size: %dpx;
font-weight: bold;
}
.email-header h1 {
margin: 0 0 10px 0;
font-size: %dpx;
font-weight: bold;
}
.email-meta div {
margin-bottom: 2px;
font-size: %dpx;
}
.email-meta div {
margin-bottom: 2px;
font-size: %dpx;
}
.email-body {
word-wrap: break-word;
}
.email-body {
word-wrap: break-word;
}
.attachment-section {
margin-top: 15px;
padding: 10px;
background-color: %s;
border: 1px solid %s;
border-radius: 3px;
}
.attachment-section {
margin-top: 15px;
padding: 10px;
background-color: %s;
border: 1px solid %s;
border-radius: 3px;
}
.attachment-section h3 {
margin: 0 0 8px 0;
font-size: %dpx;
}
.attachment-section h3 {
margin: 0 0 8px 0;
font-size: %dpx;
}
.attachment-item {
padding: 5px 0;
}
.attachment-item {
padding: 5px 0;
}
.attachment-icon {
margin-right: 5px;
}
.attachment-icon {
margin-right: 5px;
}
.attachment-details, .attachment-type {
font-size: %dpx;
color: #555555;
}
.attachment-details, .attachment-type {
font-size: %dpx;
color: #555555;
}
.attachment-inclusion-note, .attachment-info-note {
margin-top: 8px;
padding: 6px;
font-size: %dpx;
border-radius: 3px;
}
.attachment-inclusion-note, .attachment-info-note {
margin-top: 8px;
padding: 6px;
font-size: %dpx;
border-radius: 3px;
}
.attachment-inclusion-note {
background-color: #e6ffed;
border: 1px solid #d4f7dc;
color: #006420;
}
.attachment-inclusion-note {
background-color: #e6ffed;
border: 1px solid #d4f7dc;
color: #006420;
}
.attachment-info-note {
background-color: #fff9e6;
border: 1px solid #fff0c2;
color: #664d00;
}
.attachment-info-note {
background-color: #fff9e6;
border: 1px solid #fff0c2;
color: #664d00;
}
.attachment-link-container {
display: flex;
align-items: center;
padding: 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
margin: 4px 0;
}
.attachment-link-container {
display: flex;
align-items: center;
padding: 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
margin: 4px 0;
}
.attachment-link-container:hover {
background-color: #e9ecef;
}
.attachment-link-container:hover {
background-color: #e9ecef;
}
.attachment-note {
font-size: %dpx;
color: #6c757d;
font-style: italic;
margin-left: 8px;
}
.attachment-note {
font-size: %dpx;
color: #6c757d;
font-style: italic;
margin-left: 8px;
}
.no-content {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
.no-content {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
.text-body {
white-space: pre-wrap;
}
.text-body {
white-space: pre-wrap;
}
img {
max-width: 100%%;
height: auto;
display: block;
}
""",
img {
max-width: 100%%;
height: auto;
display: block;
}
""",
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_LINE_HEIGHT,
@@ -420,13 +430,14 @@ public class EmlProcessingUtils {
String attachmentId = "attachment_" + i;
html.append(
String.format(
Locale.ROOT,
"""
<div class="attachment-item" id="%s">
<span class="attachment-icon" data-filename="%s">@</span>
<span class="attachment-name">%s</span>
<span class="attachment-details">(%s%s)</span>
</div>
""",
<div class="attachment-item" id="%s">
<span class="attachment-icon" data-filename="%s">@</span>
<span class="attachment-name">%s</span>
<span class="attachment-details">(%s%s)</span>
</div>
""",
attachmentId,
escapeHtml(embeddedFilename),
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
@@ -470,7 +481,7 @@ public class EmlProcessingUtils {
}
if (filename != null) {
String lowerFilename = filename.toLowerCase();
String lowerFilename = filename.toLowerCase(Locale.ROOT);
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
if (lowerFilename.endsWith(entry.getKey())) {
return entry.getValue();
@@ -516,7 +527,7 @@ public class EmlProcessingUtils {
result.append(processedText, lastEnd, matcher.start());
String charset = matcher.group(1);
String encoding = matcher.group(2).toUpperCase();
String encoding = matcher.group(2).toUpperCase(Locale.ROOT);
String encodedValue = matcher.group(3);
try {
@@ -2,6 +2,7 @@ package stirling.software.common.util;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
@@ -34,9 +35,8 @@ public class ExceptionUtils {
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.",
Locale.ROOT,
"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.",
context);
} else {
message =
@@ -97,8 +97,10 @@ public class ExceptionUtils {
public static IOException createFileProcessingException(String operation, Exception cause) {
String message =
String.format(
Locale.ROOT,
"An error occurred while processing the file during %s operation: %s",
operation, cause.getMessage());
operation,
cause.getMessage());
return new IOException(message, cause);
}
@@ -12,6 +12,7 @@ import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -37,9 +38,11 @@ public class FileToPdf {
try (TempFile tempInputFile =
new TempFile(
tempFileManager,
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
fileName.toLowerCase(Locale.ROOT).endsWith(".html")
? ".html"
: ".zip")) {
if (fileName.toLowerCase().endsWith(".html")) {
if (fileName.toLowerCase(Locale.ROOT).endsWith(".html")) {
String sanitizedHtml =
sanitizeHtmlContent(
new String(fileBytes, StandardCharsets.UTF_8),
@@ -47,7 +50,7 @@ public class FileToPdf {
Files.write(
tempInputFile.getPath(),
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
} else if (fileName.toLowerCase().endsWith(".zip")) {
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
Files.write(tempInputFile.getPath(), fileBytes);
sanitizeHtmlFilesInZip(
tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
@@ -102,8 +105,8 @@ public class FileToPdf {
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase().endsWith(".html")
|| entry.getName().toLowerCase().endsWith(".htm")) {
if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html")
|| entry.getName().toLowerCase(Locale.ROOT).endsWith(".htm")) {
String content =
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
String sanitizedContent =
@@ -545,7 +545,7 @@ public class GeneralUtils {
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
}
sizeStr = sizeStr.trim().toUpperCase();
sizeStr = sizeStr.trim().toUpperCase(Locale.ROOT);
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
try {
@@ -574,7 +574,7 @@ public class GeneralUtils {
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
} else {
// Use provided default unit or fall back to MB
String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB";
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
double value = Double.parseDouble(sizeStr);
return switch (unit) {
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
@@ -616,13 +616,14 @@ public class GeneralUtils {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024L * 1024L) {
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
return String.format(Locale.ROOT, "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024L * 1024L * 1024L) {
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
return String.format(Locale.ROOT, "%.2f MB", bytes / (1024.0 * 1024.0));
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
return String.format(Locale.ROOT, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
} else {
return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
return String.format(
Locale.ROOT, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
}
}
@@ -882,7 +883,7 @@ public class GeneralUtils {
byte[] mac = net.getHardwareAddress();
if (mac != null && mac.length > 0) {
for (byte b : mac) {
sb.append(String.format("%02X", b));
sb.append(String.format(Locale.ROOT, "%02X", b));
}
break; // Use the first valid network interface
}
@@ -892,7 +893,7 @@ public class GeneralUtils {
byte[] mac = network.getHardwareAddress();
if (mac != null) {
for (byte b : mac) {
sb.append(String.format("%02X", b));
sb.append(String.format(Locale.ROOT, "%02X", b));
}
}
}
@@ -6,6 +6,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.Locale;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
@@ -118,7 +119,7 @@ public class ImageProcessingUtils {
BufferedImage image = null;
String filename = file.getOriginalFilename();
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
if (filename != null && filename.toLowerCase(Locale.ROOT).endsWith(".psd")) {
// For PSD files, try explicit ImageReader
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
if (readers.hasNext()) {
@@ -134,7 +135,8 @@ public class ImageProcessingUtils {
throw new IOException(
"Unable to read image from file: "
+ filename
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP"
+ " compression");
}
} else {
// For non-PSD files, use standard ImageIO
@@ -318,7 +318,7 @@ public class PdfAttachmentHandler {
private static String normalizeFilename(String filename) {
if (filename == null) return "";
String normalized = filename.toLowerCase().trim();
String normalized = filename.toLowerCase(Locale.ROOT).trim();
normalized =
RegexPatternUtils.getInstance()
.getWhitespacePattern()
@@ -560,7 +560,7 @@ public class PdfAttachmentHandler {
@Override
protected void writeString(String string, List<TextPosition> textPositions)
throws IOException {
String lowerString = string.toLowerCase();
String lowerString = string.toLowerCase(Locale.ROOT);
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
isInAttachmentSection = true;
@@ -9,6 +9,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import javax.imageio.ImageIO;
@@ -51,7 +52,7 @@ public class PdfToCbrUtils {
throw new IllegalArgumentException("File must have a name");
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"pdf".equals(extension)) {
throw new IllegalArgumentException("File must be a PDF");
}
@@ -70,7 +71,8 @@ public class PdfToCbrUtils {
BufferedImage image =
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", pageIndex + 1);
Path imagePath = tempDir.resolve(imageFilename);
ImageIO.write(image, "PNG", imagePath.toFile());
@@ -167,7 +169,7 @@ public class PdfToCbrUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "pdf".equals(extension);
}
}
@@ -3,6 +3,7 @@ package stirling.software.common.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -46,7 +47,7 @@ public class PdfToCbzUtils {
throw new IllegalArgumentException("File must have a name");
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"pdf".equals(extension)) {
throw new IllegalArgumentException("File must be a PDF");
}
@@ -65,7 +66,8 @@ public class PdfToCbzUtils {
BufferedImage image =
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", pageIndex + 1);
ZipEntry zipEntry = new ZipEntry(imageFilename);
zipOut.putNextEntry(zipEntry);
@@ -93,7 +95,7 @@ public class PdfToCbzUtils {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
return "pdf".equals(extension);
}
}
@@ -8,6 +8,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@@ -50,36 +51,18 @@ public class PdfUtils {
public PDRectangle textToPageSize(String size) {
switch (size.toUpperCase()) {
case "A0" -> {
return PDRectangle.A0;
}
case "A1" -> {
return PDRectangle.A1;
}
case "A2" -> {
return PDRectangle.A2;
}
case "A3" -> {
return PDRectangle.A3;
}
case "A4" -> {
return PDRectangle.A4;
}
case "A5" -> {
return PDRectangle.A5;
}
case "A6" -> {
return PDRectangle.A6;
}
case "LETTER" -> {
return PDRectangle.LETTER;
}
case "LEGAL" -> {
return PDRectangle.LEGAL;
}
return switch (size.toUpperCase(Locale.ROOT)) {
case "A0" -> PDRectangle.A0;
case "A1" -> PDRectangle.A1;
case "A2" -> PDRectangle.A2;
case "A3" -> PDRectangle.A3;
case "A4" -> PDRectangle.A4;
case "A5" -> PDRectangle.A5;
case "A6" -> PDRectangle.A6;
case "LETTER" -> PDRectangle.LETTER;
case "LEGAL" -> PDRectangle.LEGAL;
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
}
};
}
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
@@ -182,8 +165,8 @@ public class PdfUtils {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (singleImage) {
if ("tiff".equals(imageType.toLowerCase())
|| "tif".equals(imageType.toLowerCase())) {
if ("tiff".equals(imageType.toLowerCase(Locale.ROOT))
|| "tif".equals(imageType.toLowerCase(Locale.ROOT))) {
// Write the images to the output stream as a TIFF with multiple frames
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
ImageWriteParam param = writer.getDefaultWriteParam();
@@ -361,9 +344,10 @@ public class PdfUtils {
zos.putNextEntry(
new ZipEntry(
String.format(
Locale.ROOT,
filename + "_%d.%s",
i + 1,
imageType.toLowerCase())));
imageType.toLowerCase(Locale.ROOT))));
zos.write(baosImage.toByteArray());
}
}
@@ -463,8 +447,8 @@ public class PdfUtils {
String contentType = file.getContentType();
String originalFilename = Filenames.toSimpleFileName(file.getOriginalFilename());
if (originalFilename != null
&& (originalFilename.toLowerCase().endsWith(".tiff")
|| originalFilename.toLowerCase().endsWith(".tif"))) {
&& (originalFilename.toLowerCase(Locale.ROOT).endsWith(".tiff")
|| originalFilename.toLowerCase(Locale.ROOT).endsWith(".tif"))) {
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
reader.setInput(ImageIO.createImageInputStream(file.getInputStream()));
int numPages = reader.getNumImages(true);
@@ -631,7 +615,7 @@ public class PdfUtils {
int actualPageCount = pdfDocument.getNumberOfPages();
pdfDocument.close();
return switch (comparator.toLowerCase()) {
return switch (comparator.toLowerCase(Locale.ROOT)) {
case "greater" -> actualPageCount > pageCount;
case "equal" -> actualPageCount == pageCount;
case "less" -> actualPageCount < pageCount;