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 11:30:51 +01:00 committed by GitHub
parent 7f801157c8
commit 6e82f124a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 478 additions and 348 deletions

View File

@ -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

View File

@ -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();

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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) {

View File

@ -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);

View File

@ -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")) {

View File

@ -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);
}

View File

@ -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)

View File

@ -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();
}

View File

@ -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();

View File

@ -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 {

View File

@ -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);
}

View File

@ -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 =

View File

@ -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));
}
}
}

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -7,6 +7,7 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
@ -114,7 +115,7 @@ class ApplicationPropertiesLogicTest {
UnsupportedProviderException ex =
assertThrows(UnsupportedProviderException.class, () -> client.get("unknown"));
assertTrue(ex.getMessage().toLowerCase().contains("not supported"));
assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("not supported"));
}
@Test

View File

@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.time.Instant;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
@ -116,8 +117,11 @@ class ResourceMonitorTest {
shouldQueue,
result,
String.format(
Locale.ROOT,
"For weight %d and status %s, shouldQueue should be %s",
weight, status, shouldQueue));
weight,
status,
shouldQueue));
}
@Test

View File

@ -12,6 +12,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Locale;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -773,20 +774,33 @@ class EmlToPdfTest {
private String createSimpleTextEmailWithCharset(
String from, String to, String subject, String body, String charset) {
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/plain; charset=%s\nContent-Transfer-Encoding: 8bit\n\n%s",
from, to, subject, getTimestamp(), charset, body);
from,
to,
subject,
getTimestamp(),
charset,
body);
}
private String createEmailWithCustomHeaders() {
return String.format(
Locale.ROOT,
"From: sender@example.com\nDate: %s\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
getTimestamp(), "This is an email body with some headers missing.");
getTimestamp(),
"This is an email body with some headers missing.");
}
private String createHtmlEmail(String from, String to, String subject, String htmlBody) {
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
from, to, subject, getTimestamp(), htmlBody);
from,
to,
subject,
getTimestamp(),
htmlBody);
}
private String createMultipartEmailWithAttachment(
@ -801,6 +815,7 @@ class EmlToPdfTest {
Base64.getEncoder()
.encodeToString(attachmentContent.getBytes(StandardCharsets.UTF_8));
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
@ -840,6 +855,7 @@ class EmlToPdfTest {
Base64.getEncoder()
.encodeToString(attachmentEmlContent.getBytes(StandardCharsets.UTF_8));
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
@ -878,6 +894,7 @@ class EmlToPdfTest {
private String createMultipartAlternativeEmail(
String textBody, String htmlBody, String boundary) {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
@ -913,6 +930,7 @@ class EmlToPdfTest {
private String createQuotedPrintableEmail() {
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n%s",
"sender@example.com",
"recipient@example.com",
@ -925,6 +943,7 @@ class EmlToPdfTest {
String encodedBody =
Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8));
return String.format(
Locale.ROOT,
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n%s",
"sender@example.com",
"recipient@example.com",
@ -936,6 +955,7 @@ class EmlToPdfTest {
private String createEmailWithInlineImage(
String htmlBody, String boundary, String contentId, String base64Image) {
return String.format(
Locale.ROOT,
"""
From: %s
To: %s
@ -980,6 +1000,7 @@ class EmlToPdfTest {
String encodedAttachment =
Base64.getEncoder().encodeToString(attachmentBody.getBytes(StandardCharsets.UTF_8));
return String.format(
Locale.ROOT,
"""
From: %s
To: %s

View File

@ -7,6 +7,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
@ -155,7 +156,7 @@ public class SPDFApplication {
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String os = System.getProperty("os.name").toLowerCase();
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -204,7 +205,7 @@ public class RearrangePagesPDFController {
private List<Integer> processSortTypes(String sortTypes, int totalPages, String pageOrder) {
try {
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase(Locale.ROOT));
return switch (mode) {
case REVERSE_ORDER -> reverseOrder(totalPages);
case DUPLEX_SORT -> duplexSort(totalPages);
@ -247,7 +248,7 @@ public class RearrangePagesPDFController {
List<Integer> newPageOrder;
if (sortType != null
&& !sortType.isEmpty()
&& !"custom".equals(sortType.toLowerCase())) {
&& !"custom".equals(sortType.toLowerCase(Locale.ROOT))) {
newPageOrder = processSortTypes(sortType, totalPages, pageOrder);
} else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);

View File

@ -5,6 +5,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -242,7 +243,7 @@ public class SplitPdfByChaptersController {
// split files will be named as "[FILE_NUMBER] [BOOKMARK_TITLE].pdf"
String fileName =
String.format(fileNumberFormatter, i)
String.format(Locale.ROOT, fileNumberFormatter, i)
+ bookmarks.get(i).getTitle()
+ ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);

View File

@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
@ -45,9 +46,9 @@ public class ConvertEmlToPDF {
summary = "Convert EML to PDF",
description =
"This endpoint converts EML (email) files to PDF format with extensive"
+ " customization options. Features include font settings, image constraints, display modes, attachment handling,"
+ " and HTML debug output. Input: EML file, Output: PDF"
+ " or HTML file. Type: SISO")
+ " customization options. Features include font settings, image"
+ " constraints, display modes, attachment handling, and HTML debug output."
+ " Input: EML file, Output: PDF or HTML file. Type: SISO")
public ResponseEntity<byte[]> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) {
MultipartFile inputFile = request.getFileInput();
@ -67,7 +68,7 @@ public class ConvertEmlToPDF {
}
// Validate file type - support EML
String lowerFilename = originalFilename.toLowerCase();
String lowerFilename = originalFilename.toLowerCase(Locale.ROOT);
if (!lowerFilename.endsWith(".eml")) {
log.error("Invalid file type for EML to PDF: {}", originalFilename);
return ResponseEntity.badRequest()

View File

@ -8,6 +8,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@ -118,7 +119,7 @@ public class ConvertImgPDFController {
newPdfBytes,
"webp".equalsIgnoreCase(imageFormat)
? "png"
: imageFormat.toUpperCase(),
: imageFormat.toUpperCase(Locale.ROOT),
colorTypeResult,
singleImage,
dpi,

View File

@ -8,6 +8,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
@ -67,7 +68,7 @@ public class ConvertOfficeController {
if (extension == null || !isValidFileExtension(extension)) {
throw new IllegalArgumentException("Invalid file extension");
}
String extensionLower = extension.toLowerCase();
String extensionLower = extension.toLowerCase(Locale.ROOT);
String baseName = FilenameUtils.getBaseName(originalFilename);
if (baseName == null || baseName.isBlank()) {
@ -141,7 +142,7 @@ public class ConvertOfficeController {
p ->
p.getFileName()
.toString()
.toLowerCase()
.toLowerCase(Locale.ROOT)
.endsWith(".pdf"))
.findFirst()
.orElse(null);

View File

@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -124,7 +125,7 @@ public class ExtractCSVController {
}
private String generateEntryName(String baseName, int pageNum, int tableIndex) {
return String.format("%s_p%d_t%d.csv", baseName, pageNum, tableIndex);
return String.format(Locale.ROOT, "%s_p%d_t%d.csv", baseName, pageNum, tableIndex);
}
private String getBaseName(String filename) {

View File

@ -5,6 +5,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -68,7 +69,11 @@ public class BlankPageController {
}
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
log.info(
String.format(
Locale.ROOT,
"Page has white pixel percent of %.2f%%",
whitePixelPercentage));
return whitePixelPercentage >= whitePercent;
}

View File

@ -293,7 +293,7 @@ public class CompressController {
imageIdentity,
GeneralUtils.formatBytes(originalSize),
GeneralUtils.formatBytes(compressedSize),
String.format("%.1f", reductionPercentage));
String.format(Locale.ROOT, "%.1f", reductionPercentage));
} else {
log.info(
"Image identity {}: Not suitable for compression, skipping", imageIdentity);
@ -386,7 +386,7 @@ public class CompressController {
"Overall PDF compression: {} → {} (reduced by {}%)",
GeneralUtils.formatBytes(originalFileSize),
GeneralUtils.formatBytes(compressedFileSize),
String.format("%.1f", overallReduction));
String.format(Locale.ROOT, "%.1f", overallReduction));
return newCompressedPDF;
} catch (Exception e) {
newCompressedPDF.close();
@ -481,7 +481,7 @@ public class CompressController {
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
sb.append(String.format(Locale.ROOT, "%02x", b));
}
return sb.toString();
}
@ -538,7 +538,7 @@ public class CompressController {
"Total original image size: {}, compressed: {} (reduced by {}%)",
GeneralUtils.formatBytes(stats.totalOriginalBytes),
GeneralUtils.formatBytes(stats.totalCompressedBytes),
String.format("%.1f", overallImageReduction));
String.format(Locale.ROOT, "%.1f", overallImageReduction));
}
private static BufferedImage convertToGrayscale(BufferedImage image) {
@ -848,6 +848,7 @@ public class CompressController {
@Override
public String toString() {
return String.format(
Locale.ROOT,
"%s_%s_%d_%dx%d_%s_%s_%d_%s_%s_%s",
pixelHash.substring(0, Math.min(8, pixelHash.length())),
colorSpace,
@ -911,7 +912,7 @@ public class CompressController {
// Increment optimization level if we need more compression
private static int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
double currentRatio = currentSize / (double) targetSize;
log.info("Current compression ratio: {}", String.format("%.2f", currentRatio));
log.info("Current compression ratio: {}", String.format(Locale.ROOT, "%.2f", currentRatio));
if (currentRatio > 2.0) {
return Math.min(9, currentLevel + 3);
@ -1184,7 +1185,7 @@ public class CompressController {
log.info(
"Post-Ghostscript file size: {} (reduced by {}%)",
GeneralUtils.formatBytes(postGsSize),
String.format("%.1f", gsReduction));
String.format(Locale.ROOT, "%.1f", gsReduction));
} else {
log.warn(
"Ghostscript compression failed with return code: {}",
@ -1291,7 +1292,7 @@ public class CompressController {
log.info(
"Post-QPDF file size: {} (reduced by {}%)",
GeneralUtils.formatBytes(postQpdfSize),
String.format("%.1f", qpdfReduction));
String.format(Locale.ROOT, "%.1f", qpdfReduction));
} catch (IOException e) {
if (returnCode != null && returnCode.getRc() != 3) {

View File

@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -336,7 +337,9 @@ public class OCRController {
};
File pageOutputPath =
new File(tempOutputDir, String.format("page_%d.pdf", pageNum));
new File(
tempOutputDir,
String.format(Locale.ROOT, "page_%d.pdf", pageNum));
if (shouldOcr) {
// Convert page to image
@ -359,7 +362,9 @@ public class OCRController {
pageNum + 1, renderDpi, e);
}
File imagePath =
new File(tempImagesDir, String.format("page_%d.png", pageNum));
new File(
tempImagesDir,
String.format(Locale.ROOT, "page_%d.png", pageNum));
ImageIO.write(image, "png", imagePath);
// Build OCR command
@ -367,7 +372,9 @@ public class OCRController {
command.add("tesseract");
command.add(imagePath.toString());
command.add(
new File(tempOutputDir, String.format("page_%d", pageNum))
new File(
tempOutputDir,
String.format(Locale.ROOT, "page_%d", pageNum))
.toString());
command.add("-l");
command.add(String.join("+", selectedLanguages));

View File

@ -9,6 +9,7 @@ import java.awt.print.PrinterJob;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
import javax.imageio.ImageIO;
import javax.print.PrintService;
@ -57,11 +58,14 @@ public class PrintFileController {
try {
// Find matching printer
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
String normalizedPrinterName = printerName.toLowerCase(Locale.ROOT);
PrintService selectedService =
Arrays.stream(services)
.filter(
service ->
service.getName().toLowerCase().contains(printerName))
service.getName()
.toLowerCase(Locale.ROOT)
.contains(normalizedPrinterName))
.findFirst()
.orElseThrow(
() ->

View File

@ -11,6 +11,7 @@ import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import javax.imageio.ImageIO;
@ -117,7 +118,7 @@ public class StampController {
String customColor = request.getCustomColor();
float marginFactor =
switch (request.getCustomMargin().toLowerCase()) {
switch (request.getCustomMargin().toLowerCase(Locale.ROOT)) {
case "small" -> 0.02f;
case "medium" -> 0.035f;
case "large" -> 0.05f;

View File

@ -19,6 +19,7 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
@ -203,7 +204,7 @@ public class PipelineDirectoryProcessor {
? filename.substring(
filename.lastIndexOf(".")
+ 1)
.toLowerCase()
.toLowerCase(Locale.ROOT)
: "";
// Check against allowed extensions
@ -213,7 +214,8 @@ public class PipelineDirectoryProcessor {
extension.toLowerCase());
if (!isAllowed) {
log.info(
"Skipping file with unsupported extension: {} ({})",
"Skipping file with unsupported extension: {}"
+ " ({})",
filename,
extension);
}
@ -226,7 +228,8 @@ public class PipelineDirectoryProcessor {
fileMonitor.isFileReadyForProcessing(path);
if (!isReady) {
log.info(
"File not ready for processing (locked/created last 5s): {}",
"File not ready for processing (locked/created"
+ " last 5s): {}",
path);
}
return isReady;

View File

@ -8,6 +8,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
@ -121,7 +122,9 @@ public class PipelineProcessor {
boolean hasInputFileType = false;
for (String extension : inputFileTypes) {
if ("ALL".equals(extension)
|| file.getFilename().toLowerCase().endsWith(extension)) {
|| file.getFilename()
.toLowerCase(Locale.ROOT)
.endsWith(extension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
@ -159,7 +162,8 @@ public class PipelineProcessor {
String providedExtension = "no extension";
if (filename != null && filename.contains(".")) {
providedExtension =
filename.substring(filename.lastIndexOf(".")).toLowerCase();
filename.substring(filename.lastIndexOf("."))
.toLowerCase(Locale.ROOT);
}
logPrintStream.println(
@ -187,7 +191,10 @@ public class PipelineProcessor {
file ->
finalinputFileTypes.stream()
.anyMatch(
file.getFilename().toLowerCase()
file.getFilename()
.toLowerCase(
Locale
.ROOT)
::endsWith))
.toList();
}
@ -228,7 +235,7 @@ public class PipelineProcessor {
if (filename != null && filename.contains(".")) {
return filename.substring(
filename.lastIndexOf("."))
.toLowerCase();
.toLowerCase(Locale.ROOT);
}
return "no extension";
})

View File

@ -410,12 +410,12 @@ public class GetInfoOnPDF {
float widthInCm = widthInInches * 2.54f;
float heightInCm = heightInInches * 2.54f;
dimensionInfo.put("Width (px)", String.format("%.2f", width));
dimensionInfo.put("Height (px)", String.format("%.2f", height));
dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches));
dimensionInfo.put("Height (in)", String.format("%.2f", heightInInches));
dimensionInfo.put("Width (cm)", String.format("%.2f", widthInCm));
dimensionInfo.put("Height (cm)", String.format("%.2f", heightInCm));
dimensionInfo.put("Width (px)", String.format(Locale.ROOT, "%.2f", width));
dimensionInfo.put("Height (px)", String.format(Locale.ROOT, "%.2f", height));
dimensionInfo.put("Width (in)", String.format(Locale.ROOT, "%.2f", widthInInches));
dimensionInfo.put("Height (in)", String.format(Locale.ROOT, "%.2f", heightInInches));
dimensionInfo.put("Width (cm)", String.format(Locale.ROOT, "%.2f", widthInCm));
dimensionInfo.put("Height (cm)", String.format(Locale.ROOT, "%.2f", heightInCm));
}
private static ArrayNode exploreStructureTree(List<Object> nodes) {

View File

@ -361,7 +361,7 @@ public class MetricsController {
long hours = duration.toHoursPart();
long minutes = duration.toMinutesPart();
long seconds = duration.toSecondsPart();
return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds);
return String.format(Locale.ROOT, "%dd %dh %dm %ds", days, hours, minutes, seconds);
}
@Setter

View File

@ -65,6 +65,6 @@ public class UploadLimitService {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String pre = "KMGTPE".charAt(exp - 1) + "B";
return String.format(Locale.US, "%.1f %s", bytes / Math.pow(1024, exp), pre);
return String.format(Locale.ROOT, "%.1f %s", bytes / Math.pow(1024, exp), pre);
}
}

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.pdf;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -204,12 +205,14 @@ public class TextFinder extends PDFTextStripper {
TextPosition pos = i < pageTextPositions.size() ? pageTextPositions.get(i) : null;
debug.append(
String.format(
Locale.ROOT,
" [%d] '%c' (0x%02X) -> %s\n",
i,
c,
(int) c,
pos != null
? String.format("(%.1f,%.1f)", pos.getX(), pos.getY())
? String.format(
Locale.ROOT, "(%.1f,%.1f)", pos.getX(), pos.getY())
: "null"));
}

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.service;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
@ -88,7 +89,7 @@ public class ApiDocService {
: RegexPatternUtils.getInstance().getApiDocInputTypePattern())
.matcher(description);
while (matcher.find()) {
String type = matcher.group(1).toUpperCase();
String type = matcher.group(1).toUpperCase(Locale.ROOT);
if (outputToFileTypes.containsKey(type)) {
return outputToFileTypes.get(type);
}

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.service;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -70,7 +71,10 @@ public class MetricsAggregatorService {
String key =
String.format(
"http_requests_%s_%s", method, uri.replace("/", "_"));
Locale.ROOT,
"http_requests_%s_%s",
method,
uri.replace("/", "_"));
double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
double difference = currentCount - lastCount;

View File

@ -7,6 +7,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import org.springframework.stereotype.Service;
@ -92,7 +93,7 @@ public class SignatureService {
}
private boolean isImageFile(Path path) {
String fileName = path.getFileName().toString().toLowerCase();
String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
return fileName.endsWith(".jpg")
|| fileName.endsWith(".jpeg")
|| fileName.endsWith(".png")

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.utils.text;
import java.io.IOException;
import java.util.Locale;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
@ -24,7 +25,8 @@ public class TextEncodingHelper {
byte[] encoded = font.encode(text);
if (encoded.length > 0) {
log.debug(
"Text '{}' has good full-string encoding for font {} - permissively allowing",
"Text '{}' has good full-string encoding for font {} - permissively"
+ " allowing",
text,
font.getName() != null ? font.getName() : "Unknown");
return true;
@ -61,6 +63,7 @@ public class TextEncodingHelper {
for (int i = 0; i < text.length(); ) {
int codePoint = text.codePointAt(i);
String charStr = new String(Character.toChars(codePoint));
String hex = String.format(Locale.ROOT, "%04X", codePoint); // U+%04X
totalCodePoints++;
try {
@ -71,28 +74,23 @@ public class TextEncodingHelper {
if (charWidth >= 0) {
successfulCodePoints++;
log.debug(
"Code point '{}' (U+{}) encoded successfully",
charStr,
Integer.toHexString(codePoint).toUpperCase());
log.debug("Code point '{}' (U+{}) encoded successfully", charStr, hex);
} else {
log.debug(
"Code point '{}' (U+{}) has invalid width: {}",
charStr,
Integer.toHexString(codePoint).toUpperCase(),
hex,
charWidth);
}
} else {
log.debug(
"Code point '{}' (U+{}) encoding failed - empty result",
charStr,
Integer.toHexString(codePoint).toUpperCase());
"Code point '{}' (U+{}) encoding failed - empty result", charStr, hex);
}
} catch (IOException | IllegalArgumentException e) {
log.debug(
"Code point '{}' (U+{}) validation failed: {}",
charStr,
Integer.toHexString(codePoint).toUpperCase(),
hex,
e.getMessage());
}
@ -100,16 +98,20 @@ public class TextEncodingHelper {
}
double successRate =
totalCodePoints > 0 ? (double) successfulCodePoints / totalCodePoints : 0;
totalCodePoints > 0 ? (double) successfulCodePoints / totalCodePoints : 0.0;
String pct =
String.format(
Locale.ROOT, "%.1f%%", successRate * 100); // Pre-formatting percentage!
boolean isAcceptable = successRate >= 0.95;
log.debug(
"Array validation for '{}': {}/{} code points successful ({:.1f}%) - {}",
"Array validation for '{}': {}/{} code points successful ({}) - {}",
text,
successfulCodePoints,
totalCodePoints,
successRate * 100,
isAcceptable ? "ALLOWING" : "rejecting");
pct,
(isAcceptable ? "ALLOWING" : "rejecting"));
return isAcceptable;
}

View File

@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -49,13 +50,16 @@ class TextFinderTest {
expectedCount,
foundTexts.size(),
String.format(
"Expected %d matches for search term '%s'", expectedCount, searchTerm));
Locale.ROOT,
"Expected %d matches for search term '%s'",
expectedCount,
searchTerm));
if (expectedTexts != null) {
for (String expectedText : expectedTexts) {
assertTrue(
foundTexts.stream().anyMatch(text -> text.getText().equals(expectedText)),
String.format("Expected to find text: '%s'", expectedText));
String.format(Locale.ROOT, "Expected to find text: '%s'", expectedText));
}
}
@ -271,7 +275,10 @@ class TextFinderTest {
// Each pattern should find at least one match in our test content
assertFalse(
foundTexts.isEmpty(),
String.format("Pattern '%s' should find at least one match", regexPattern));
String.format(
Locale.ROOT,
"Pattern '%s' should find at least one match",
regexPattern));
}
@Test

View File

@ -5,6 +5,7 @@ import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -307,8 +308,8 @@ public class AuditUtils {
// For HTTP methods, infer based on controller and path
if (httpMethod != null && path != null) {
String cls = controller.getSimpleName().toLowerCase();
String pkg = controller.getPackage().getName().toLowerCase();
String cls = controller.getSimpleName().toLowerCase(Locale.ROOT);
String pkg = controller.getPackage().getName().toLowerCase(Locale.ROOT);
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
@ -374,8 +375,8 @@ public class AuditUtils {
}
// Otherwise infer from controller and path
String cls = controller.getSimpleName().toLowerCase();
String pkg = controller.getPackage().getName().toLowerCase();
String cls = controller.getSimpleName().toLowerCase(Locale.ROOT);
String pkg = controller.getPackage().getName().toLowerCase(Locale.ROOT);
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;

View File

@ -5,6 +5,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -145,7 +146,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
registrationId = oAuthToken.getAuthorizedClientRegistrationId();
// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
switch (registrationId.toLowerCase(Locale.ROOT)) {
case "keycloak" -> {
KeycloakProvider keycloak = oauth.getClient().getKeycloak();

View File

@ -142,7 +142,7 @@ public class InitialSecuritySetup {
if (internalApiUserOpt.isPresent()) {
User internalApiUser = internalApiUserOpt.get();
// move to team internal API user
if (!internalApiUser.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
if (!TeamService.INTERNAL_TEAM_NAME.equals(internalApiUser.getTeam().getName())) {
log.info(
"Moving internal API user to team: {}", TeamService.INTERNAL_TEAM_NAME);
Team internalTeam = teamService.getOrCreateInternalTeam();

View File

@ -7,6 +7,7 @@ import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@ -89,7 +90,8 @@ public class AccountWebController {
if (oauth.isSettingsValid()) {
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
String clientName =
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
oauth.getProvider()
.replaceFirst(firstChar, firstChar.toUpperCase(Locale.ROOT));
providerList.put(OAUTH_2_AUTHORIZATION + oauth.getProvider(), clientName);
}

View File

@ -1,5 +1,7 @@
package stirling.software.proprietary.security.configuration;
import java.util.Locale;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
@ -132,7 +134,7 @@ public class DatabaseConfig {
private String getDriverClassName(String driverName) throws UnsupportedProviderException {
try {
ApplicationProperties.Driver driver =
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
ApplicationProperties.Driver.valueOf(driverName.toUpperCase(Locale.ROOT));
return switch (driver) {
case H2 -> {

View File

@ -5,6 +5,7 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import java.util.Locale;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
@ -185,7 +186,7 @@ public class KeygenLicenseVerifier {
byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature);
// Create the signing data format - prefix with "license/"
String signingData = String.format("license/%s", encryptedData);
String signingData = String.format(Locale.ROOT, "license/%s", encryptedData);
byte[] signingDataBytes = signingData.getBytes();
log.info("Signing data length: {} bytes", signingDataBytes.length);
@ -335,7 +336,7 @@ public class KeygenLicenseVerifier {
.decode(encodedSignature.replace('-', '+').replace('_', '/'));
// For ED25519_SIGN format, the signing data is "key/" + encodedPayload
String signingData = String.format("key/%s", encodedPayload);
String signingData = String.format(Locale.ROOT, "key/%s", encodedPayload);
byte[] dataBytes = signingData.getBytes();
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
@ -510,8 +511,10 @@ public class KeygenLicenseVerifier {
String licenseKey, String machineFingerprint, LicenseContext context) throws Exception {
String requestBody =
String.format(
Locale.ROOT,
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
licenseKey, machineFingerprint);
licenseKey,
machineFingerprint);
HttpRequest request =
HttpRequest.newBuilder()
.uri(

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -185,6 +186,7 @@ public class AdminSettingsController {
return ResponseEntity.ok(
String.format(
Locale.ROOT,
"Successfully updated %d setting(s). Changes will take effect on"
+ " application restart.",
updatedCount));
@ -297,9 +299,11 @@ public class AdminSettingsController {
String escapedSectionName = HtmlUtils.htmlEscape(sectionName);
return ResponseEntity.ok(
String.format(
Locale.ROOT,
"Successfully updated %d setting(s) in section '%s'. Changes will take"
+ " effect on application restart.",
updatedCount, escapedSectionName));
updatedCount,
escapedSectionName));
} catch (IOException e) {
log.error("Failed to save section settings to file: {}", e.getMessage(), e);
@ -388,6 +392,7 @@ public class AdminSettingsController {
String escapedKey = HtmlUtils.htmlEscape(key);
return ResponseEntity.ok(
String.format(
Locale.ROOT,
"Successfully updated setting '%s'. Changes will take effect on"
+ " application restart.",
escapedKey));
@ -410,7 +415,7 @@ public class AdminSettingsController {
return null;
}
return switch (sectionName.toLowerCase()) {
return switch (sectionName.toLowerCase(Locale.ROOT)) {
case "security" -> applicationProperties.getSecurity();
case "system" -> applicationProperties.getSystem();
case "ui" -> applicationProperties.getUi();
@ -473,7 +478,7 @@ public class AdminSettingsController {
}
// Ensure first part is a valid section name
if (parts.length > 0 && !VALID_SECTION_NAMES.contains(parts[0].toLowerCase())) {
if (parts.length > 0 && !VALID_SECTION_NAMES.contains(parts[0].toLowerCase(Locale.ROOT))) {
return false;
}
@ -578,8 +583,8 @@ public class AdminSettingsController {
/** Check if a field name indicates sensitive data with full path context */
private boolean isSensitiveFieldWithPath(String fieldName, String fullPath) {
String lowerField = fieldName.toLowerCase();
String lowerPath = fullPath.toLowerCase();
String lowerField = fieldName.toLowerCase(Locale.ROOT);
String lowerPath = fullPath.toLowerCase(Locale.ROOT);
// Don't mask premium.key specifically
if ("key".equals(lowerField) && "premium.key".equals(lowerPath)) {

View File

@ -3,6 +3,7 @@ package stirling.software.proprietary.security.model;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -84,7 +85,7 @@ public class User implements UserDetails, Serializable {
}
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType.toString().toLowerCase();
this.authenticationType = authenticationType.toString().toLowerCase(Locale.ROOT);
}
public void addAuthorities(Set<Authority> authorities) {

View File

@ -7,6 +7,7 @@ import static stirling.software.common.util.ValidationUtils.isStringEmpty;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@ -177,7 +178,7 @@ public class OAuth2Configuration {
String name = oauth.getProvider();
String firstChar = String.valueOf(name.charAt(0));
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase());
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase(Locale.ROOT));
Provider oidcProvider =
new Provider(
@ -187,7 +188,8 @@ public class OAuth2Configuration {
oauth.getClientId(),
oauth.getClientSecret(),
oauth.getScopes(),
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
UsernameAttribute.valueOf(
oauth.getUseAsUsername().toUpperCase(Locale.ROOT)),
null,
null,
null);

View File

@ -1,5 +1,6 @@
package stirling.software.proprietary.security.service;
import java.util.Locale;
import java.util.Optional;
import org.springframework.security.authentication.LockedException;
@ -44,7 +45,7 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
OidcUser user = delegate.loadUser(userRequest);
OAUTH2 oauth2 = securityProperties.getOauth2();
UsernameAttribute usernameAttribute =
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase(Locale.ROOT));
String usernameAttributeKey = usernameAttribute.getName();
// todo: save user by OIDC ID instead of username

View File

@ -1,5 +1,7 @@
package stirling.software.proprietary.security.service;
import java.util.Locale;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@ -59,7 +61,7 @@ public class CustomUserDetailsService implements UserDetailsService {
}
AuthenticationType userAuthenticationType =
AuthenticationType.valueOf(authTypeStr.toUpperCase());
AuthenticationType.valueOf(authTypeStr.toUpperCase(Locale.ROOT));
if (!user.hasPassword() && userAuthenticationType == AuthenticationType.WEB) {
throw new IllegalArgumentException("Password must not be null");
}

View File

@ -1,5 +1,6 @@
package stirling.software.proprietary.security.service;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@ -45,17 +46,19 @@ public class LoginAttemptService {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return;
}
attemptsCache.remove(key.toLowerCase());
String normalizedKey = key.toLowerCase(Locale.ROOT);
attemptsCache.remove(normalizedKey);
}
public void loginFailed(String key) {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return;
}
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
String normalizedKey = key.toLowerCase(Locale.ROOT);
AttemptCounter attemptCounter = attemptsCache.get(normalizedKey);
if (attemptCounter == null) {
attemptCounter = new AttemptCounter();
attemptsCache.put(key.toLowerCase(), attemptCounter);
attemptsCache.put(normalizedKey, attemptCounter);
} else {
if (attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
attemptCounter.reset();
@ -68,7 +71,8 @@ public class LoginAttemptService {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return false;
}
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
String normalizedKey = key.toLowerCase(Locale.ROOT);
AttemptCounter attemptCounter = attemptsCache.get(normalizedKey);
if (attemptCounter == null) {
return false;
}
@ -80,7 +84,8 @@ public class LoginAttemptService {
// Arbitrarily high number if tracking is disabled
return Integer.MAX_VALUE;
}
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
String normalizedKey = key.toLowerCase(Locale.ROOT);
AttemptCounter attemptCounter = attemptsCache.get(normalizedKey);
if (attemptCounter == null) {
return MAX_ATTEMPT;
}

View File

@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@ -118,6 +119,7 @@ public class UserService implements UserServiceInterface {
return addApiKeyToUser(username);
}
@Override
public String getApiKeyForUser(String username) {
User user =
findByUsernameIgnoreCase(username)
@ -495,9 +497,10 @@ public class UserService implements UserServiceInterface {
.matches();
List<String> notAllowedUserList = new ArrayList<>();
notAllowedUserList.add("ALL_USERS".toLowerCase());
notAllowedUserList.add("ALL_USERS".toLowerCase(Locale.ROOT));
notAllowedUserList.add("anonymoususer");
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
String normalizedUsername = username.toLowerCase(Locale.ROOT);
boolean notAllowedUser = notAllowedUserList.contains(normalizedUsername);
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
}
@ -545,6 +548,7 @@ public class UserService implements UserServiceInterface {
}
}
@Override
public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
@ -601,6 +605,7 @@ public class UserService implements UserServiceInterface {
}
}
@Override
public long getTotalUsersCount() {
// Count all users in the database
long userCount = userRepository.count();