feat: Add RegexPatternUtils for centralized regex management, file naming funcs, UtilityClass annotation (#4218)

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
Balázs Szücs
2025-09-28 16:56:35 +01:00
committed by GitHub
co-authored by Copilot Anthony Stirling
parent 133e6d3de6
commit 045f4cc591
78 changed files with 1947 additions and 617 deletions
@@ -14,6 +14,7 @@ import stirling.software.common.model.ApplicationProperties.Driver;
import stirling.software.common.model.ApplicationProperties.Premium;
import stirling.software.common.model.ApplicationProperties.Security;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.common.util.RegexPatternUtils;
class ApplicationPropertiesLogicTest {
@@ -38,7 +39,10 @@ class ApplicationPropertiesLogicTest {
new ApplicationProperties.TempFileManagement();
String expectedBase =
java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
RegexPatternUtils.getInstance()
.getTrailingSlashesPattern()
.matcher(java.lang.System.getProperty("java.io.tmpdir"))
.replaceAll("")
+ "/stirling-pdf";
assertEquals(expectedBase, normalize.apply(tfm.getBaseTmpDir()));
@@ -1,8 +1,15 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
@@ -154,4 +161,223 @@ public class GeneralUtilsTest {
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, false);
assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly.");
}
@Test
void testRemoveExtension() {
// Test common cases (should use fast string operations)
assertEquals("document", GeneralUtils.removeExtension("document.pdf"));
assertEquals("image", GeneralUtils.removeExtension("image.jpg"));
assertEquals("file.backup", GeneralUtils.removeExtension("file.backup.zip"));
assertEquals("complex.file.name", GeneralUtils.removeExtension("complex.file.name.txt"));
// Test edge cases (should fall back to regex)
assertEquals("default", GeneralUtils.removeExtension(null));
assertEquals("noextension", GeneralUtils.removeExtension("noextension"));
assertEquals(
".hidden", GeneralUtils.removeExtension(".hidden")); // Hidden file, no extension
assertEquals("trailing.", GeneralUtils.removeExtension("trailing.")); // Trailing dot
assertEquals("", GeneralUtils.removeExtension(""));
assertEquals("a", GeneralUtils.removeExtension("a"));
// Test multiple dots
assertEquals("file.with.multiple", GeneralUtils.removeExtension("file.with.multiple.dots"));
assertEquals("path/to/file", GeneralUtils.removeExtension("path/to/file.ext"));
}
@Test
void testAppendSuffix() {
// Normal cases
assertEquals("document_processed", GeneralUtils.appendSuffix("document", "_processed"));
assertEquals("file.txt", GeneralUtils.appendSuffix("file", ".txt"));
// Null handling
assertEquals("default_suffix", GeneralUtils.appendSuffix(null, "_suffix"));
assertEquals("basename", GeneralUtils.appendSuffix("basename", null));
assertEquals("default", GeneralUtils.appendSuffix(null, null));
// Empty strings
assertEquals("_suffix", GeneralUtils.appendSuffix("", "_suffix"));
assertEquals("basename", GeneralUtils.appendSuffix("basename", ""));
}
@Test
void testProcessFilenames() {
List<String> filenames = new ArrayList<>();
filenames.add("document.pdf");
filenames.add("image.jpg");
filenames.add("spreadsheet.xlsx");
filenames.add("presentation.pptx");
filenames.add(null); // Should handle null gracefully
filenames.add("noextension");
List<String> results = new ArrayList<>();
GeneralUtils.processFilenames(filenames, "_processed", results::add);
List<String> expected =
List.of(
"document_processed",
"image_processed",
"spreadsheet_processed",
"presentation_processed",
"default_processed",
"noextension_processed");
assertEquals(expected, results);
}
@Test
void testProcessFilenamesNullHandling() {
List<String> results = new ArrayList<>();
// Null filenames list
GeneralUtils.processFilenames(null, "_suffix", results::add);
assertTrue(results.isEmpty(), "Should handle null filenames list");
// Null processor
List<String> filenames = List.of("test.txt");
GeneralUtils.processFilenames(filenames, "_suffix", null); // Should not throw
}
@Test
void testRemoveExtensionThreadSafety() throws InterruptedException {
final int threadCount = 50;
final int operationsPerThread = 100;
final String[] testFilenames = {
"document.pdf", "image.jpg", "data.csv", "presentation.pptx",
"archive.zip", "music.mp3", "video.mp4", "text.txt"
};
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < threadCount; i++) {
executor.submit(
() -> {
try {
for (int j = 0; j < operationsPerThread; j++) {
String filename = testFilenames[j % testFilenames.length];
String result = GeneralUtils.removeExtension(filename);
// Verify result is correct
assertFalse(
result.contains("."),
"Result should not contain extension: " + result);
assertTrue(
filename.startsWith(result),
"Original should start with result: "
+ filename
+ " -> "
+ result);
}
successCount.incrementAndGet();
} catch (Exception e) {
exceptions.add(e);
} finally {
latch.countDown();
}
});
}
assertTrue(latch.await(10, TimeUnit.SECONDS), "All threads should complete");
if (!exceptions.isEmpty()) {
fail("Thread safety test failed with exceptions: " + exceptions);
}
assertEquals(threadCount, successCount.get(), "All threads should succeed");
executor.shutdown();
}
@Test
void testBatchProcessingPerformance() {
List<String> filenames = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
filenames.add("file" + i + ".pdf");
filenames.add("document" + i + ".docx");
filenames.add("image" + i + ".jpg");
}
List<String> results = new ArrayList<>();
GeneralUtils.processFilenames(filenames, "_processed", results::add);
assertEquals(filenames.size(), results.size(), "Should process all filenames");
assertTrue(results.contains("file0_processed"), "Should contain processed filename");
assertTrue(results.contains("document500_processed"), "Should contain processed filename");
assertTrue(results.contains("image999_processed"), "Should contain processed filename");
}
@Test
void testHybridStringRegexApproach() {
String[] edgeCases = {
"", // Empty string
".", // Just a dot
"..", // Two dots
"...", // Three dots
".hidden", // Hidden file
"file.", // Trailing dot
"a.b.c.d.e.f.g", // Many extensions
"no-extension-here", // No extension
"file..double.dot" // Double dots
};
for (String edgeCase : edgeCases) {
String result = GeneralUtils.removeExtension(edgeCase);
assertNotNull(result, "Result should not be null for: " + edgeCase);
// For specific edge cases, verify expected behavior
switch (edgeCase) {
case "" -> assertEquals("", result, "Empty string should remain empty");
case "." -> assertEquals(".", result, "Single dot should remain unchanged");
case ".." -> assertEquals("..", result, "Double dots should remain unchanged");
case "..." -> assertEquals("...", result, "Triple dots should remain unchanged");
case ".hidden" ->
assertEquals(".hidden", result, "Hidden file should remain unchanged");
case "file." ->
assertEquals("file.", result, "Trailing dot should remain unchanged");
case "no-extension-here" ->
assertEquals(
"no-extension-here",
result,
"No extension should remain unchanged");
case "a.b.c.d.e.f.g" ->
assertEquals(
"a.b.c.d.e.f",
result,
"Multiple extensions should remove last one");
case "file..double.dot" ->
assertEquals(
"file..double",
result,
"Double dot case should remove last extension");
}
}
}
@Test
void testGetTitleFromFilename() {
// Test normal cases
assertEquals("document", GeneralUtils.getTitleFromFilename("document.pdf"));
assertEquals("presentation", GeneralUtils.getTitleFromFilename("presentation.pptx"));
assertEquals("file.backup", GeneralUtils.getTitleFromFilename("file.backup.zip"));
// Test null and empty handling
assertEquals("Untitled", GeneralUtils.getTitleFromFilename(null));
assertEquals("Untitled", GeneralUtils.getTitleFromFilename(""));
// Test edge cases
assertEquals(".hidden", GeneralUtils.getTitleFromFilename(".hidden"));
assertEquals("file.", GeneralUtils.getTitleFromFilename("file."));
assertEquals("noextension", GeneralUtils.getTitleFromFilename("noextension"));
// Test complex cases
assertEquals(
"complex.file.name", GeneralUtils.getTitleFromFilename("complex.file.name.txt"));
assertEquals("path/to/file", GeneralUtils.getTitleFromFilename("path/to/file.ext"));
}
}
@@ -65,23 +65,22 @@ public class PdfUtilsTest {
doc1.addPage(new PDPage());
doc1.addPage(new PDPage());
doc1.addPage(new PDPage());
PdfUtils utils = new PdfUtils();
assertTrue(utils.pageCount(doc1, 2, "greater"));
assertTrue(PdfUtils.pageCount(doc1, 2, "greater"));
PDDocument doc2 = new PDDocument();
doc2.addPage(new PDPage());
doc2.addPage(new PDPage());
doc2.addPage(new PDPage());
assertTrue(utils.pageCount(doc2, 3, "equal"));
assertTrue(PdfUtils.pageCount(doc2, 3, "equal"));
PDDocument doc3 = new PDDocument();
doc3.addPage(new PDPage());
doc3.addPage(new PDPage());
assertTrue(utils.pageCount(doc3, 5, "less"));
assertTrue(PdfUtils.pageCount(doc3, 5, "less"));
PDDocument doc4 = new PDDocument();
doc4.addPage(new PDPage());
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
assertThrows(IllegalArgumentException.class, () -> PdfUtils.pageCount(doc4, 1, "bad"));
}
@Test
@@ -91,8 +90,7 @@ public class PdfUtilsTest {
doc.addPage(page);
PDRectangle rect = page.getMediaBox();
String expected = rect.getWidth() + "x" + rect.getHeight();
PdfUtils utils = new PdfUtils();
assertTrue(utils.pageSize(doc, expected));
assertTrue(PdfUtils.pageSize(doc, expected));
}
@Test
@@ -0,0 +1,115 @@
package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.*;
import java.util.regex.Pattern;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class RegexPatternUtilsTest {
private RegexPatternUtils utils;
@BeforeEach
void setUp() {
utils = RegexPatternUtils.getInstance();
utils.clearCache(); // Start with clean cache for each test
}
@Test
void testPatternCaching() {
String regex = "test\\d+";
Pattern pattern1 = utils.getPattern(regex);
assertNotNull(pattern1);
assertTrue(utils.isCached(regex));
assertEquals(
1, utils.getCacheSize()); // Should have at least 1 pattern (plus precompiled ones
// are cleared)
Pattern pattern2 = utils.getPattern(regex);
assertSame(pattern1, pattern2); // Should be the exact same object
}
@Test
void testPatternWithFlags() {
String regex = "test";
int flags = Pattern.CASE_INSENSITIVE;
Pattern pattern1 = utils.getPattern(regex, flags);
Pattern pattern2 = utils.getPattern(regex); // No flags
assertNotSame(pattern1, pattern2); // Different flags = different cached patterns
assertTrue(utils.isCached(regex, flags));
assertTrue(utils.isCached(regex, 0));
}
@Test
void testCacheEviction() {
String regex = "evict\\d+";
utils.getPattern(regex);
assertTrue(utils.isCached(regex));
boolean removed = utils.removeFromCache(regex);
assertTrue(removed);
assertFalse(utils.isCached(regex));
boolean removedAgain = utils.removeFromCache(regex);
assertFalse(removedAgain);
}
@Test
void testNullRegexHandling() {
assertThrows(
IllegalArgumentException.class,
() -> {
utils.getPattern(null);
});
assertThrows(
IllegalArgumentException.class,
() -> {
utils.getPattern(null, Pattern.CASE_INSENSITIVE);
});
assertFalse(utils.isCached(null));
assertFalse(utils.removeFromCache(null));
}
@Test
void testCommonPatterns() {
Pattern whitespace = utils.getWhitespacePattern();
assertTrue(whitespace.matcher(" \t ").matches());
Pattern trailing = utils.getTrailingSlashesPattern();
assertTrue(trailing.matcher("/path/to/dir///").find());
Pattern filename = utils.getSafeFilenamePattern();
assertTrue(filename.matcher("bad<file>name").find());
}
@Test
void testCreateSearchPattern() {
String regex = "Hello";
Pattern caseSensitive = utils.createSearchPattern(regex, false);
Pattern caseInsensitive = utils.createSearchPattern(regex, true);
assertTrue(caseSensitive.matcher("Hello").matches());
assertFalse(caseSensitive.matcher("hello").matches());
assertTrue(caseInsensitive.matcher("Hello").matches());
assertTrue(caseInsensitive.matcher("hello").matches());
assertTrue(caseInsensitive.matcher("HELLO").matches());
}
@Test
void testSingletonBehavior() {
RegexPatternUtils instance1 = RegexPatternUtils.getInstance();
RegexPatternUtils instance2 = RegexPatternUtils.getInstance();
assertSame(instance1, instance2);
}
}