feat: modernize codebase using Java switch expressions and List#getFirst/getLast APIs (#6334)

# Description of Changes

This PR introduces a broad modernization of the codebase by adopting
newer Java language features and improving code readability and
maintainability.

## What was changed

- Replaced traditional `switch` statements with modern switch
expressions (`case ->`) across multiple classes.
- Replaced usages of `List#get(0)` and `List#get(size - 1)` with
`getFirst()` and `getLast()` respectively.
- Simplified conditional logic using pattern matching (e.g.,
`instanceof` and switch pattern matching).
- Refactored various utility and controller classes to reduce
boilerplate and improve clarity.
- Removed unused or redundant code (e.g., `parseClientFileIds` method in
`MergeController`).
- Improved type safety (e.g., using `Class::isInstance` instead of
`instanceof` checks in streams).
- Cleaned up Spring annotations by removing unnecessary `@Autowired`
where constructor injection is already used.
- Added a new test (`UIDataControllerTest`) to ensure correct handling
of identical JSON configs with different filenames.
- Minor formatting and style fixes (e.g., Spotless formatting
adjustment).

## Why the change was made

- To align the codebase with modern Java standards (Java 17+ features).
- To improve readability and maintainability by reducing verbosity.
- To eliminate common indexing patterns that are more error-prone.
- To standardize coding style across the project.
- To improve test coverage for edge cases discovered during refactoring.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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)
- [ ] I have performed a self-review of my own code
- [ ] 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)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### 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 run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.

---------

Signed-off-by: Ludy87 <Ludy87@users.noreply.github.com>
This commit is contained in:
Ludy
2026-08-12 15:13:29 +00:00
committed by GitHub
parent 84804d7ce3
commit a28a950aa4
56 changed files with 331 additions and 408 deletions
@@ -73,7 +73,7 @@ public class RuntimePathConfig {
defaultWatchedFolders,
watchedFoldersDirs,
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.get(0);
this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.getFirst();
this.pipelineFinishedFoldersPath =
resolvePath(
defaultFinishedFolders,
@@ -60,54 +60,40 @@ public class Provider {
}
private UsernameAttribute validateUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (name) {
case "google" -> {
return validateGoogleUsernameAttribute(usernameAttribute);
}
case "github" -> {
return validateGitHubUsernameAttribute(usernameAttribute);
}
case "keycloak" -> {
return validateKeycloakUsernameAttribute(usernameAttribute);
}
default -> {
return usernameAttribute;
}
}
return switch (name) {
case "google" -> validateGoogleUsernameAttribute(usernameAttribute);
case "github" -> validateGitHubUsernameAttribute(usernameAttribute);
case "keycloak" -> validateKeycloakUsernameAttribute(usernameAttribute);
default -> usernameAttribute;
};
}
private UsernameAttribute validateKeycloakUsernameAttribute(
UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> {
return usernameAttribute;
}
return switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> usernameAttribute;
default ->
throw new UnsupportedClaimException(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
};
}
private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> {
return usernameAttribute;
}
return switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> usernameAttribute;
default ->
throw new UnsupportedClaimException(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
};
}
private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case LOGIN, EMAIL, NAME -> {
return usernameAttribute;
}
return switch (usernameAttribute) {
case LOGIN, EMAIL, NAME -> usernameAttribute;
default ->
throw new UnsupportedClaimException(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
};
}
@Override
@@ -361,8 +361,8 @@ public class PdfMarkdownConverter {
if (xs.isEmpty()) {
return List.of(lines);
}
float minX = xs.get(0);
float maxX = xs.get(xs.size() - 1);
float minX = xs.getFirst();
float maxX = xs.getLast();
float splitAt = (minX + maxX) / 2f;
float biggestGap = 0;
for (int i = 1; i < xs.size(); i++) {
@@ -492,7 +492,7 @@ public class PdfMarkdownConverter {
List<List<Line>> anchorGroups = new ArrayList<>();
List<Line> current = new ArrayList<>();
current.add(cands.get(0));
current.add(cands.getFirst());
for (int i = 1; i < cands.size(); i++) {
float gap = cands.get(i - 1).y - cands.get(i).y;
if (gap > splitThreshold) {
@@ -513,8 +513,8 @@ public class PdfMarkdownConverter {
if (anchors.size() < 2) {
continue;
}
float top = anchors.get(0).y;
float bottom = anchors.get(anchors.size() - 1).y;
float top = anchors.getFirst().y;
float bottom = anchors.getLast().y;
// Each anchor seeds a row; absorb wrapped continuation lines (non-anchors within the
// run's vertical span, with a little slack below the last row) into the anchor above.
@@ -674,8 +674,8 @@ public class PdfMarkdownConverter {
float minGutter = Math.max(10f, charWidth * 2.5f);
List<float[]> merged = new ArrayList<>();
for (float[] band : columns) {
if (!merged.isEmpty() && band[0] - merged.get(merged.size() - 1)[1] < minGutter) {
merged.get(merged.size() - 1)[1] = band[1];
if (!merged.isEmpty() && band[0] - merged.getLast()[1] < minGutter) {
merged.getLast()[1] = band[1];
} else {
merged.add(new float[] {band[0], band[1]});
}
@@ -734,7 +734,7 @@ public class PdfMarkdownConverter {
}
}
StringBuilder sb = new StringBuilder();
sb.append(buildGfmRow(rows.get(0), widths, cols)).append('\n');
sb.append(buildGfmRow(rows.getFirst(), widths, cols)).append('\n');
sb.append('|');
for (int c = 0; c < cols; c++) {
sb.append('-').append("-".repeat(widths[c])).append('-').append('|');
@@ -910,8 +910,8 @@ public class PdfMarkdownConverter {
}
// Only merge a sentence continuation between two text paragraphs, never into/out of a
// table.
if (!(output.get(output.size() - 1) instanceof String last)
|| !(pageItems.get(0) instanceof String first)) {
if (!(output.getLast() instanceof String last)
|| !(pageItems.getFirst() instanceof String first)) {
return;
}
if (!first.isEmpty()
@@ -932,13 +932,13 @@ public class PdfMarkdownConverter {
for (Object e : elements) {
if (e instanceof TableBlock tb
&& !out.isEmpty()
&& out.get(out.size() - 1) instanceof TableBlock prev
&& out.getLast() instanceof TableBlock prev
&& columnsMatch(flatten(prev.rows()), flatten(tb.rows()))) {
List<List<Line>> merged = new ArrayList<>(prev.rows());
List<List<Line>> tail = tb.rows();
if (!tail.isEmpty()
&& !prev.rows().isEmpty()
&& rowText(tail.get(0)).equals(rowText(prev.rows().get(0)))) {
&& rowText(tail.getFirst()).equals(rowText(prev.rows().getFirst()))) {
tail = tail.subList(1, tail.size());
}
merged.addAll(tail);
@@ -971,7 +971,7 @@ public class PdfMarkdownConverter {
continue;
}
if (e instanceof TableBlock tb && !tb.rows().isEmpty()) {
return rowText(tb.rows().get(0));
return rowText(tb.rows().getFirst());
}
return null;
}
@@ -157,7 +157,7 @@ public class InternalApiClient {
boolean hasFilePart =
body.values().stream()
.flatMap(java.util.List::stream)
.anyMatch(v -> v instanceof Resource);
.anyMatch(Resource.class::isInstance);
if (isAiTool && !hasFilePart) {
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
}
@@ -140,20 +140,25 @@ public class ChecksumUtils {
for (String algorithm : algorithms) {
String key = algorithm; // keep original key for output
switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32":
checksums.put(key, new CRC32());
break;
case "ADLER32":
checksums.put(key, new Adler32());
break;
default:
try {
// For MessageDigest, pass the original name (case-insensitive per JCA)
digests.put(key, MessageDigest.getInstance(algorithm));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Unsupported algorithm: " + algorithm, e);
}
Object digestOrChecksum =
switch (algorithm.toUpperCase(Locale.ROOT)) {
case "CRC32" -> new CRC32();
case "ADLER32" -> new Adler32();
default -> {
try {
// For MessageDigest, pass the original name (case-insensitive
// per JCA)
yield MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(
"Unsupported algorithm: " + algorithm, e);
}
}
};
if (digestOrChecksum instanceof Checksum checksum) {
checksums.put(key, checksum);
} else {
digests.put(key, (MessageDigest) digestOrChecksum);
}
}
@@ -751,7 +751,7 @@ public class ExceptionUtils {
String targetDescription;
if (errorInfo.affectedPages() != null && !errorInfo.affectedPages().isEmpty()) {
if (errorInfo.affectedPages().size() == 1) {
targetDescription = "page " + errorInfo.affectedPages().get(0);
targetDescription = "page " + errorInfo.affectedPages().getFirst();
} else {
targetDescription =
"pages "
@@ -848,7 +848,7 @@ public class ExceptionUtils {
}
// Use the first page number, or null if none found
Integer pageNumber = affectedPages.isEmpty() ? null : affectedPages.get(0);
Integer pageNumber = affectedPages.isEmpty() ? null : affectedPages.getFirst();
return new GhostscriptErrorInfo(
ErrorCode.GHOSTSCRIPT_PAGE_DRAWING,
@@ -114,7 +114,7 @@ public enum FormFieldTypeSupport {
return;
}
PDAnnotationWidget widget = checkBox.getWidgets().get(0);
PDAnnotationWidget widget = checkBox.getWidgets().getFirst();
PDAppearanceCharacteristicsDictionary appearanceChars =
widget.getAppearanceCharacteristics();
@@ -88,28 +88,16 @@ public class FormUtils {
* text)
*/
public String detectFieldType(PDField field) {
if (field instanceof PDSignatureField) {
return FIELD_TYPE_SIGNATURE;
}
if (field instanceof PDPushButton) {
return FIELD_TYPE_BUTTON;
}
if (field instanceof PDTextField) {
return FIELD_TYPE_TEXT;
}
if (field instanceof PDCheckBox) {
return FIELD_TYPE_CHECKBOX;
}
if (field instanceof PDComboBox) {
return FIELD_TYPE_COMBOBOX;
}
if (field instanceof PDListBox) {
return FIELD_TYPE_LISTBOX;
}
if (field instanceof PDRadioButton) {
return FIELD_TYPE_RADIO;
}
return FIELD_TYPE_TEXT;
return switch (field) {
case PDSignatureField ignored -> FIELD_TYPE_SIGNATURE;
case PDPushButton ignored -> FIELD_TYPE_BUTTON;
case PDTextField ignored -> FIELD_TYPE_TEXT;
case PDCheckBox ignored -> FIELD_TYPE_CHECKBOX;
case PDComboBox ignored -> FIELD_TYPE_COMBOBOX;
case PDListBox ignored -> FIELD_TYPE_LISTBOX;
case PDRadioButton ignored -> FIELD_TYPE_RADIO;
case null, default -> FIELD_TYPE_TEXT;
};
}
public List<FormFieldInfo> extractFormFields(PDDocument document) {
@@ -583,22 +571,17 @@ public class FormUtils {
continue;
}
String type = info.type();
Object value;
switch (type) {
case FIELD_TYPE_CHECKBOX:
value = isChecked(info.value()) ? Boolean.TRUE : Boolean.FALSE;
break;
case FIELD_TYPE_LISTBOX:
if (info.multiSelect()) {
value = new ArrayList<>();
} else {
value = safeDefault(info.value());
}
break;
case FIELD_TYPE_BUTTON, FIELD_TYPE_SIGNATURE:
continue; // skip non-fillable
default:
value = safeDefault(info.value());
Object value =
switch (type) {
case FIELD_TYPE_CHECKBOX ->
isChecked(info.value()) ? Boolean.TRUE : Boolean.FALSE;
case FIELD_TYPE_LISTBOX ->
info.multiSelect() ? new ArrayList<>() : safeDefault(info.value());
case FIELD_TYPE_BUTTON, FIELD_TYPE_SIGNATURE -> null;
default -> safeDefault(info.value());
};
if (value == null) {
continue; // skip non-fillable
}
record.put(info.name(), value);
}
@@ -949,44 +932,44 @@ public class FormUtils {
if (selection == null || selection.trim().isEmpty()) return null;
List<String> filtered =
filterChoiceSelections(List.of(selection), allowedOptions, fieldName);
return filtered.isEmpty() ? null : filtered.get(0);
return filtered.isEmpty() ? null : filtered.getFirst();
}
private void applyValueToField(PDField field, String value, boolean strict) throws IOException {
try {
if (field instanceof PDTextField textField) {
setTextValue(textField, value);
} else if (field instanceof PDCheckBox checkBox) {
LinkedHashSet<String> candidateStates = collectCheckBoxStates(checkBox);
boolean shouldCheck = shouldCheckBoxBeChecked(value, candidateStates);
try {
if (shouldCheck) {
checkBox.check();
} else {
checkBox.unCheck();
}
} catch (IOException checkProblem) {
log.warn(
"Failed to set checkbox state for '{}': {}",
field.getFullyQualifiedName(),
checkProblem.getMessage(),
checkProblem);
if (strict) {
throw checkProblem;
switch (field) {
case PDTextField textField -> setTextValue(textField, value);
case PDCheckBox checkBox -> {
LinkedHashSet<String> candidateStates = collectCheckBoxStates(checkBox);
boolean shouldCheck = shouldCheckBoxBeChecked(value, candidateStates);
try {
if (shouldCheck) {
checkBox.check();
} else {
checkBox.unCheck();
}
} catch (IOException checkProblem) {
log.warn(
"Failed to set checkbox state for '{}': {}",
field.getFullyQualifiedName(),
checkProblem.getMessage(),
checkProblem);
if (strict) {
throw checkProblem;
}
}
}
} else if (field instanceof PDRadioButton radioButton) {
if (value != null && !value.isBlank()) {
radioButton.setValue(value);
case PDRadioButton radioButton -> {
if (value != null && !value.isBlank()) {
radioButton.setValue(value);
}
}
} else if (field instanceof PDChoice choiceField) {
applyChoiceValue(choiceField, value);
} else if (field instanceof PDPushButton) {
log.debug("Ignore Push button");
} else if (field instanceof PDSignatureField) {
log.debug("Skipping signature field '{}'", field.getFullyQualifiedName());
} else {
field.setValue(value != null ? value : "");
case PDChoice choiceField -> applyChoiceValue(choiceField, value);
case PDPushButton ignored -> log.debug("Ignore Push button");
case PDSignatureField ignored ->
log.debug("Skipping signature field '{}'", field.getFullyQualifiedName());
case null -> log.warn("Attempted to set value on null field");
default -> field.setValue(value != null ? value : "");
}
} catch (Exception e) {
log.warn(
@@ -1306,37 +1289,42 @@ public class FormUtils {
List<String> resolveOptions(PDTerminalField field) {
try {
if (field instanceof PDChoice choice) {
LinkedHashSet<String> allowed = new LinkedHashSet<>();
List<String> exportValues = choice.getOptionsExportValues();
List<String> displayValues = choice.getOptionsDisplayValues();
return switch (field) {
case PDChoice choice -> {
LinkedHashSet<String> allowed = new LinkedHashSet<>();
List<String> exportValues = choice.getOptionsExportValues();
List<String> displayValues = choice.getOptionsDisplayValues();
if (exportValues != null) {
exportValues.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(allowed::add);
if (exportValues != null) {
exportValues.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(allowed::add);
}
if (displayValues != null) {
displayValues.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(allowed::add);
}
yield new ArrayList<>(allowed);
}
if (displayValues != null) {
displayValues.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(allowed::add);
case PDRadioButton radio -> {
List<String> exports = radio.getExportValues();
yield exports != null && !exports.isEmpty()
? new ArrayList<>(exports)
: Collections.emptyList();
}
return new ArrayList<>(allowed);
} else if (field instanceof PDRadioButton radio) {
List<String> exports = radio.getExportValues();
if (exports != null && !exports.isEmpty()) {
return new ArrayList<>(exports);
case PDCheckBox checkBox -> {
List<String> exports = checkBox.getExportValues();
yield exports != null && !exports.isEmpty()
? new ArrayList<>(exports)
: Collections.emptyList();
}
} else if (field instanceof PDCheckBox checkBox) {
List<String> exports = checkBox.getExportValues();
if (exports != null && !exports.isEmpty()) {
return new ArrayList<>(exports);
}
}
case null, default -> Collections.emptyList();
};
} catch (Exception e) {
log.debug(
"Failed to resolve options for field '{}': {}",
@@ -1465,7 +1453,7 @@ public class FormUtils {
// Only check options for choice-type fields (combobox, listbox, radio)
if (CHOICE_FIELD_TYPES.contains(type) && options != null && !options.isEmpty()) {
String optionCandidate = cleanLabel(options.get(0));
String optionCandidate = cleanLabel(options.getFirst());
if (optionCandidate != null && !looksGeneric(optionCandidate)) {
return optionCandidate;
}
@@ -1557,7 +1545,7 @@ public class FormUtils {
continue;
}
PDAnnotationWidget widget = widgets.get(0);
PDAnnotationWidget widget = widgets.getFirst();
PDRectangle originalRectangle = cloneRectangle(widget.getRectangle());
PDPage page = resolveWidgetPage(document, widget, null);
if (page == null || originalRectangle == null) {
@@ -2446,19 +2434,19 @@ public class FormUtils {
private static int firstWidgetPageIndex(FormFieldWithCoordinates f) {
return (f.getWidgets() != null && !f.getWidgets().isEmpty())
? f.getWidgets().get(0).getPageIndex()
? f.getWidgets().getFirst().getPageIndex()
: -1;
}
private static float firstWidgetY(FormFieldWithCoordinates f) {
return (f.getWidgets() != null && !f.getWidgets().isEmpty())
? f.getWidgets().get(0).getY()
? f.getWidgets().getFirst().getY()
: 0;
}
private static float firstWidgetX(FormFieldWithCoordinates f) {
return (f.getWidgets() != null && !f.getWidgets().isEmpty())
? f.getWidgets().get(0).getX()
? f.getWidgets().getFirst().getX()
: 0;
}
@@ -26,29 +26,27 @@ import lombok.extern.slf4j.Slf4j;
public class ImageProcessingUtils {
static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) {
BufferedImage convertedImage;
switch (colorType) {
case "greyscale":
convertedImage =
return switch (colorType) {
case "greyscale" -> {
BufferedImage convertedImage =
new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
BufferedImage.TYPE_BYTE_GRAY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
case "blackwhite":
convertedImage =
yield convertedImage;
}
case "blackwhite" -> {
BufferedImage convertedImage =
new BufferedImage(
sourceImage.getWidth(),
sourceImage.getHeight(),
BufferedImage.TYPE_BYTE_BINARY);
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
break;
default: // full color
convertedImage = sourceImage;
break;
}
return convertedImage;
yield convertedImage;
}
default -> sourceImage;
};
}
public static byte[] getImageData(BufferedImage image) {
@@ -330,7 +330,7 @@ public class PDFToFile {
if (outputFiles.size() == 1) {
// Return single output file
File outputFile = outputFiles.get(0);
File outputFile = outputFiles.getFirst();
if ("txt:Text".equals(outputFormat)) {
outputFormat = "txt";
}
@@ -307,7 +307,7 @@ public class ProcessExecutor {
boolean isQpdf =
commandToRun != null
&& !commandToRun.isEmpty()
&& commandToRun.get(0).contains("qpdf");
&& commandToRun.getFirst().contains("qpdf");
if (!outputLines.isEmpty()) {
String outputMessage = String.join("\n", outputLines);
@@ -370,7 +370,7 @@ public class ProcessExecutor {
}
// Check if this is a UNO conversion by looking for unoconvert executable
String executable = command.get(0);
String executable = command.getFirst();
if (executable != null) {
// Extract basename from path for matching
String basename = executable;
@@ -504,7 +504,7 @@ public class ProcessExecutor {
}
// Validate executable (first argument)
String executable = command.get(0);
String executable = command.getFirst();
if (executable == null || executable.isBlank()) {
throw new IllegalArgumentException("Command executable must not be empty");
}
@@ -114,7 +114,7 @@ public class YamlHelper {
for (NodeTuple tuple : mappingNode.getValue()) {
ScalarNode keyNode = (tuple.getKeyNode() instanceof ScalarNode sk) ? sk : null;
if (keyNode == null || !keyNode.getValue().equals(keys.get(0))) {
if (keyNode == null || !keyNode.getValue().equals(keys.getFirst())) {
updatedTuples.add(tuple);
continue;
}
@@ -721,7 +721,7 @@ class PDFToFileTest {
.thenAnswer(
invocation -> {
List<String> args = invocation.getArgument(0);
String outputPath = args.get(args.size() - 1);
String outputPath = args.getLast();
Files.write(Path.of(outputPath), "Fake DOCX content".getBytes());
return mockExecutorResult;
});
@@ -1,6 +1,5 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -22,7 +21,11 @@ import stirling.software.SPDF.controller.web.UploadLimitService;
@Slf4j
public class MultipartConfiguration {
@Autowired private UploadLimitService uploadLimitService;
private final UploadLimitService uploadLimitService;
public MultipartConfiguration(UploadLimitService uploadLimitService) {
this.uploadLimitService = uploadLimitService;
}
/**
* Creates MultipartConfigElement that respects fileUploadLimit from settings.yml or environment
@@ -297,24 +297,25 @@ public class BookletImpositionController {
// Apply rotation if needed (rotate about origin), then translate to keep in cell
switch (rot) {
case 90:
case 90 -> {
cs.transform(Matrix.getRotateInstance(Math.PI / 2, 0, 0));
// After 90° CCW, the content spans x in [-r.getHeight(), 0] and y in [0,
// r.getWidth()]
cs.transform(Matrix.getTranslateInstance(0, -r.getWidth()));
break;
case 180:
}
case 180 -> {
cs.transform(Matrix.getRotateInstance(Math.PI, 0, 0));
cs.transform(Matrix.getTranslateInstance(-r.getWidth(), -r.getHeight()));
break;
case 270:
}
case 270 -> {
cs.transform(Matrix.getRotateInstance(3 * Math.PI / 2, 0, 0));
// After 270° CCW, the content spans x in [0, r.getHeight()] and y in
// [-r.getWidth(), 0]
cs.transform(Matrix.getTranslateInstance(-r.getHeight(), 0));
break;
default:
}
default -> {
// 0°: no-op
}
}
// Reuse LayerUtility passed from caller
@@ -9,7 +9,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
@@ -61,8 +60,6 @@ import stirling.software.jpdfium.doc.PdfBookmarkEditor.BookmarkTree;
@Slf4j
@RequiredArgsConstructor
public class MergeController {
private static final Pattern QUOTE_WRAP_PATTERN = Pattern.compile("^\"|\"$");
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@@ -164,30 +161,6 @@ public class MergeController {
};
}
private String[] parseClientFileIds(String clientFileIds) {
if (clientFileIds == null || clientFileIds.trim().isEmpty()) {
return new String[0];
}
try {
String trimmed = clientFileIds.trim();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
String inside = trimmed.substring(1, trimmed.length() - 1).trim();
if (inside.isEmpty()) {
return new String[0];
}
String[] parts = inside.split(",");
String[] result = new String[parts.length];
for (int i = 0; i < parts.length; i++) {
result[i] = QUOTE_WRAP_PATTERN.matcher(parts[i].trim()).replaceAll("");
}
return result;
}
} catch (Exception e) {
log.warn("Failed to parse client file IDs: {}", clientFileIds, e);
}
return new String[0];
}
private void addTableOfContents(PDDocument mergedDocument, MultipartFile[] files) {
PDDocumentOutline outline = new PDDocumentOutline();
mergedDocument.getDocumentCatalog().setDocumentOutline(outline);
@@ -125,17 +125,14 @@ public class UIDataController {
pipelineConfigs.add(content);
}
for (String config : pipelineConfigs) {
for (int i = 0; i < jsonFiles.size(); i++) {
String config = pipelineConfigs.get(i);
Map<String, Object> jsonContent =
objectMapper.readValue(
config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name");
if (name == null || name.isEmpty()) {
String filename =
jsonFiles
.get(pipelineConfigs.indexOf(config))
.getFileName()
.toString();
String filename = jsonFiles.get(i).getFileName().toString();
name = filename.substring(0, filename.lastIndexOf('.'));
}
Map<String, String> configWithName = new HashMap<>();
@@ -301,20 +298,14 @@ public class UIDataController {
}
private static String getFormatFromExtension(String extension) {
switch (extension) {
case "ttf":
return "truetype";
case "woff":
return "woff";
case "woff2":
return "woff2";
case "eot":
return "embedded-opentype";
case "svg":
return "svg";
default:
return "";
}
return switch (extension) {
case "ttf" -> "truetype";
case "woff" -> "woff";
case "woff2" -> "woff2";
case "eot" -> "embedded-opentype";
case "svg" -> "svg";
default -> "";
};
}
}
}
@@ -200,7 +200,7 @@ public class ConvertImgPDFController {
}
if (webpFiles.size() == 1) {
Path webpFilePath = webpFiles.get(0);
Path webpFilePath = webpFiles.getFirst();
byte[] webpBytes = Files.readAllBytes(webpFilePath);
Files.deleteIfExists(tempFile);
tempFile = null;
@@ -160,7 +160,7 @@ public class ConvertSvgToPDF {
String outputFilename =
filenames.isEmpty()
? "combined_svgs.pdf"
: GeneralUtils.generateFilename(filenames.get(0), "_combined.pdf");
: GeneralUtils.generateFilename(filenames.getFirst(), "_combined.pdf");
log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size());
@@ -216,7 +216,7 @@ public class ConvertSvgToPDF {
try {
if (convertedPdfs.size() == 1) {
ConvertedPdf pdf = convertedPdfs.get(0);
ConvertedPdf pdf = convertedPdfs.getFirst();
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdf.content);
@@ -231,7 +231,7 @@ public class ConvertSvgToPDF {
filenames.isEmpty()
? "converted_svgs.zip"
: GeneralUtils.generateFilename(
filenames.get(0), "_converted_svgs.zip");
filenames.getFirst(), "_converted_svgs.zip");
TempFile zipFile = createZipFromPdfs(convertedPdfs);
return WebResponseUtils.zipFileToWebResponse(zipFile, zipFilename);
} catch (IOException e) {
@@ -85,7 +85,7 @@ public class ExtractCSVController {
if (csvEntries.isEmpty()) {
return ResponseEntity.noContent().build();
} else if (csvEntries.size() == 1) {
return createCsvResponse(csvEntries.get(0), baseName);
return createCsvResponse(csvEntries.getFirst(), baseName);
} else {
return createZipResponse(csvEntries, baseName);
}
@@ -116,7 +116,9 @@ public class AutoRenameController {
mergedLineInfos.sort(
Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
String title =
mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
mergedLineInfos.isEmpty()
? null
: mergedLineInfos.getFirst().text;
return title != null
? title
@@ -336,7 +336,7 @@ public class AutoSplitPdfController {
}
if (!splitDocuments.isEmpty() && !isValidQrCode) {
splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page));
splitDocuments.getLast().addPage(document.getPage(page));
} else if (page == 0) {
PDDocument firstDocument = new PDDocument();
firstDocument.addPage(document.getPage(page));
@@ -269,7 +269,7 @@ public class CompressController {
if (references.isEmpty()) continue;
// Get the first instance of this image
PDImageXObject originalImage = getOriginalImage(doc, references.get(0));
PDImageXObject originalImage = getOriginalImage(doc, references.getFirst());
// Track original size
int originalSize = (int) originalImage.getCOSObject().getLength();
@@ -1170,7 +1170,7 @@ public class CompressController {
List<ImageReference> references = entry.getValue();
if (references.isEmpty()) continue;
PDImageXObject originalImage = getOriginalImage(doc, references.get(0));
PDImageXObject originalImage = getOriginalImage(doc, references.getFirst());
int originalSize = (int) originalImage.getCOSObject().getLength();
stats.totalOriginalBytes += originalSize;
@@ -214,7 +214,7 @@ public class ExtractImageScansController {
} else {
// Return the processed image as a response
byte[] imageBytes = processedImageBytes.get(0);
byte[] imageBytes = processedImageBytes.getFirst();
finalOutput = tempFileManager.createManagedTempFile(".png");
try (OutputStream out = Files.newOutputStream(finalOutput.getPath())) {
out.write(imageBytes);
@@ -407,7 +407,7 @@ public class CertSignController {
PDAcroForm acroForm = new PDAcroForm(doc);
doc.getDocumentCatalog().setAcroForm(acroForm);
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDAnnotationWidget widget = signatureField.getWidgets().getFirst();
List<PDField> acroFormFields = acroForm.getFields();
acroForm.setSignaturesExist(true);
acroForm.setAppendOnly(true);
@@ -634,7 +634,7 @@ class RedactExecuteService {
PageColumnLayout layout =
PageColumnLayout.fromLineBoxes(extractor.getLineBoxes(), pageWidth);
if (layout.columnCount() > 1) {
float[] g = layout.gutters().get(0);
float[] g = layout.gutters().getFirst();
log.info(
"[redact/execute] page {} layout: 2 cols, gutter x=[{}, {}]",
pageIdx + 1,
@@ -63,7 +63,7 @@ public class RemoveCertSignController {
// Remove signature fields safely
List<PDField> fieldsToRemove =
acroForm.getFields().stream()
.filter(field -> field instanceof PDSignatureField)
.filter(PDSignatureField.class::isInstance)
.toList();
if (!fieldsToRemove.isEmpty()) {
@@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.web;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@@ -13,7 +12,11 @@ import stirling.software.common.model.ApplicationProperties;
@Slf4j
public class UploadLimitService {
@Autowired private ApplicationProperties applicationProperties;
private final ApplicationProperties applicationProperties;
public UploadLimitService(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
public long getUploadLimit() {
String raw =
@@ -2688,7 +2688,7 @@ public class PdfJsonConversionService {
// Find which page the field is on
PDAnnotationWidget widget =
field.getWidgets().isEmpty() ? null : field.getWidgets().get(0);
field.getWidgets().isEmpty() ? null : field.getWidgets().getFirst();
if (widget != null) {
PDPage fieldPage = widget.getPage();
if (fieldPage != null) {
@@ -3164,7 +3164,7 @@ public class PdfJsonConversionService {
&& imageObjectNames != null
&& !imageObjectNames.isEmpty()
&& !targetTokens.isEmpty()) {
Object previous = targetTokens.get(targetTokens.size() - 1);
Object previous = targetTokens.getLast();
if (previous instanceof COSName cosName
&& imageObjectNames.contains(cosName.getName())) {
targetTokens.remove(targetTokens.size() - 1);
@@ -5246,7 +5246,7 @@ public class PdfJsonConversionService {
throws IOException {
if (OperatorName.DRAW_OBJECT.equals(operator.getName())
&& !operands.isEmpty()
&& operands.get(0) instanceof COSName name) {
&& operands.getFirst() instanceof COSName name) {
currentXObjectName = name;
}
super.processOperator(operator, operands);
@@ -420,7 +420,7 @@ public class PdfJsonImageService {
throws IOException {
if (OperatorName.DRAW_OBJECT.equals(operator.getName())
&& !operands.isEmpty()
&& operands.get(0) instanceof COSName name) {
&& operands.getFirst() instanceof COSName name) {
currentXObjectName = name;
}
super.processOperator(operator, operands);
@@ -137,7 +137,7 @@ public class JobController {
if (result.hasFiles() && !result.hasMultipleFiles()) {
try {
List<ResultFile> files = result.getAllResultFiles();
ResultFile singleFile = files.get(0);
ResultFile singleFile = files.getFirst();
byte[] fileContent = fileStorage.retrieveBytes(singleFile.getFileId());
return ResponseEntity.ok()
@@ -4,8 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -24,10 +22,7 @@ class MultipartConfigurationTest {
void setUp() throws Exception {
// Manually constructed config with a mocked service, so Spring env overrides do not apply.
uploadLimitService = mock(UploadLimitService.class);
configuration = new MultipartConfiguration();
Field field = MultipartConfiguration.class.getDeclaredField("uploadLimitService");
field.setAccessible(true);
field.set(configuration, uploadLimitService);
configuration = new MultipartConfiguration(uploadLimitService);
}
@Nested
@@ -84,12 +84,6 @@ class MergeControllerGapTest {
return (MultipartFile[]) m.invoke(null, files, fileOrder);
}
private String[] parseClientFileIds(String value) throws Exception {
Method m = MergeController.class.getDeclaredMethod("parseClientFileIds", String.class);
m.setAccessible(true);
return (String[]) m.invoke(mergeController, value);
}
private long getPdfDateTimeSafe(MultipartFile file) throws Exception {
Method m =
MergeController.class.getDeclaredMethod("getPdfDateTimeSafe", MultipartFile.class);
@@ -331,58 +325,6 @@ class MergeControllerGapTest {
}
}
// ---- parseClientFileIds -------------------------------------------------
@Nested
@DisplayName("parseClientFileIds")
class ParseClientFileIds {
@Test
@DisplayName("null input returns empty array")
void nullReturnsEmpty() throws Exception {
assertEquals(0, parseClientFileIds(null).length);
}
@Test
@DisplayName("blank input returns empty array")
void blankReturnsEmpty() throws Exception {
assertEquals(0, parseClientFileIds(" ").length);
}
@Test
@DisplayName("empty JSON array returns empty array")
void emptyArrayReturnsEmpty() throws Exception {
assertEquals(0, parseClientFileIds("[]").length);
assertEquals(0, parseClientFileIds("[ ]").length);
}
@Test
@DisplayName("non-array text returns empty array")
void nonArrayReturnsEmpty() throws Exception {
assertEquals(0, parseClientFileIds("not-an-array").length);
}
@Test
@DisplayName("parses quoted, comma-separated ids and strips surrounding quotes")
void parsesQuotedIds() throws Exception {
String[] result = parseClientFileIds("[\"id1\", \"id2\",\"id3\"]");
assertArrayEquals(new String[] {"id1", "id2", "id3"}, result);
}
@Test
@DisplayName("parses unquoted ids as-is after trimming")
void parsesUnquotedIds() throws Exception {
String[] result = parseClientFileIds("[a, b , c]");
assertArrayEquals(new String[] {"a", "b", "c"}, result);
}
@Test
@DisplayName("single element array yields a one-element result")
void singleElement() throws Exception {
assertArrayEquals(new String[] {"only"}, parseClientFileIds("[\"only\"]"));
}
}
// ---- reorderFilesByProvidedOrder ----------------------------------------
@Nested
@@ -0,0 +1,66 @@
package stirling.software.SPDF.controller.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.http.ResponseEntity;
import stirling.software.SPDF.service.SharedSignatureService;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import tools.jackson.databind.ObjectMapper;
class UIDataControllerTest {
@TempDir Path tempDir;
@Test
void getPipelineData_usesEachSourceFilenameWhenJsonContentIsIdentical() throws Exception {
Path configDir = tempDir.resolve("defaultWebUIConfigs");
Files.createDirectories(configDir);
String configJson = "{\"steps\":[]}";
Files.writeString(configDir.resolve("first-config.json"), configJson);
Files.writeString(configDir.resolve("second-config.json"), configJson);
ApplicationProperties applicationProperties = mock(ApplicationProperties.class);
SharedSignatureService signatureService = mock(SharedSignatureService.class);
RuntimePathConfig runtimePathConfig = mock(RuntimePathConfig.class);
when(runtimePathConfig.getPipelineDefaultWebUiConfigs()).thenReturn(configDir.toString());
UIDataController controller =
new UIDataController(
applicationProperties,
signatureService,
null,
new DefaultResourceLoader(),
runtimePathConfig,
new ObjectMapper());
ResponseEntity<UIDataController.PipelineData> response = controller.getPipelineData();
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
UIDataController.PipelineData body = response.getBody();
assertThat(body).isNotNull();
List<Map<String, String>> configsWithNames = body.getPipelineConfigsWithNames();
assertThat(configsWithNames).hasSize(2);
assertThat(configsWithNames)
.extracting(entry -> entry.get("name"))
.containsExactlyInAnyOrder("first-config", "second-config");
assertThat(configsWithNames)
.extracting(entry -> entry.get("json"))
.containsOnly(configJson);
}
}
@@ -204,7 +204,7 @@ class ConvertOfficeControllerTest {
inv -> {
// unoconvert writes directly to the output path (last arg)
List<String> command = inv.getArgument(0);
Path out = Path.of(command.get(command.size() - 1));
Path out = Path.of(command.getLast());
Files.writeString(out, "%PDF-1.4 produced");
return result;
});
@@ -239,7 +239,7 @@ class ConvertOfficeControllerTest {
inv -> {
// soffice writes <basename>.pdf into the --outdir (workDir)
List<String> command = inv.getArgument(0);
Path inputPath = Path.of(command.get(command.size() - 1));
Path inputPath = Path.of(command.getLast());
Path out = inputPath.getParent().resolve("report.pdf");
Files.writeString(out, "%PDF soffice");
return result;
@@ -311,7 +311,7 @@ class ConvertOfficeControllerTest {
.thenAnswer(
inv -> {
List<String> command = inv.getArgument(0);
Path inputPath = Path.of(command.get(command.size() - 1));
Path inputPath = Path.of(command.getLast());
Path out = inputPath.getParent().resolve("report.pdf");
Files.write(out, new byte[0]);
return result;
@@ -344,7 +344,7 @@ class ConvertOfficeControllerTest {
.thenAnswer(
inv -> {
List<String> command = inv.getArgument(0);
Path inputPath = Path.of(command.get(command.size() - 1));
Path inputPath = Path.of(command.getLast());
Path out = inputPath.getParent().resolve("page.pdf");
Files.writeString(out, "%PDF html");
return result;
@@ -398,7 +398,7 @@ class ConvertOfficeControllerTest {
.thenAnswer(
inv -> {
List<String> command = inv.getArgument(0);
Path inputPath = Path.of(command.get(command.size() - 1));
Path inputPath = Path.of(command.getLast());
Path out = inputPath.getParent().resolve("report.pdf");
Files.writeString(out, "%PDF produced");
return result;
@@ -197,7 +197,7 @@ class ConvertPDFToPDFAMoreTest {
// qpdf normalize/clean writes its (last-arg) output file
if (command.contains("--normalize-content=y")) {
// qpdf produced file is the last argument
Path out = Path.of(command.get(command.size() - 1));
Path out = Path.of(command.getLast());
Files.write(out, simplePdfBytes());
}
return okResult;
@@ -262,7 +262,7 @@ class ConvertPdfToVideoControllerTest {
assertTrue(command.contains("+faststart"));
assertFalse(command.contains("libvpx-vp9"));
// Output path is always the last argument.
assertEquals(backing.getAbsolutePath(), command.get(command.size() - 1));
assertEquals(backing.getAbsolutePath(), command.getLast());
}
@Test
@@ -278,7 +278,7 @@ class ConvertPdfToVideoControllerTest {
assertTrue(command.contains("30"));
assertFalse(command.contains("libx264"));
assertFalse(command.contains("+faststart"));
assertEquals(backing.getAbsolutePath(), command.get(command.size() - 1));
assertEquals(backing.getAbsolutePath(), command.getLast());
}
@Test
@@ -195,7 +195,7 @@ class CompressControllerMoreTest {
// The qpdf output path is the last argument of the command.
private static Path qpdfOutputPath(List<String> command) {
return Path.of(command.get(command.size() - 1));
return Path.of(command.getLast());
}
/** Stub gs to write a valid PDF to its output file and report success. */
@@ -162,7 +162,7 @@ class RemoveImagesControllerTest {
/** Counts every PDImageXObject reachable through page + nested form resources. */
private int countImagesInSavedOutput() throws IOException {
assertFalse(savedTempFiles.isEmpty(), "expected the controller to create a temp file");
File out = savedTempFiles.get(savedTempFiles.size() - 1);
File out = savedTempFiles.getLast();
try (PDDocument doc = Loader.loadPDF(out)) {
int count = 0;
for (PDPage page : doc.getPages()) {
@@ -245,7 +245,7 @@ class RemoveImagesControllerTest {
assertEquals(0, countImagesInSavedOutput());
// page count must be preserved
File out = savedTempFiles.get(savedTempFiles.size() - 1);
File out = savedTempFiles.getLast();
try (PDDocument result = Loader.loadPDF(out)) {
assertEquals(3, result.getNumberOfPages());
}
@@ -98,12 +98,8 @@ class RepairControllerMoreTest {
}
}
/**
* Writes a valid PDF to the path at the given command index, mimicking a successful tool run.
*/
private static void writeValidPdfTo(List<String> command, int outputPathIndex)
throws Exception {
Path out = Path.of(command.get(outputPathIndex));
/** Writes a valid PDF to the given output path, mimicking a successful tool run. */
private static void writeValidPdfTo(Path out) throws Exception {
byte[] pdf = buildPdfBytes(1);
Files.write(out, pdf);
}
@@ -133,7 +129,7 @@ class RepairControllerMoreTest {
.thenAnswer(
inv -> {
List<String> cmd = inv.getArgument(0);
writeValidPdfTo(cmd, 2);
writeValidPdfTo(Path.of(cmd.get(2)));
return okResult;
});
@@ -176,7 +172,7 @@ class RepairControllerMoreTest {
.thenAnswer(
inv -> {
List<String> cmd = inv.getArgument(0);
writeValidPdfTo(cmd, cmd.size() - 1);
writeValidPdfTo(Path.of(cmd.getLast()));
return okResult;
});
@@ -216,7 +212,7 @@ class RepairControllerMoreTest {
.thenAnswer(
inv -> {
List<String> cmd = inv.getArgument(0);
writeValidPdfTo(cmd, cmd.size() - 1);
writeValidPdfTo(Path.of(cmd.getLast()));
return okResult;
});
@@ -256,7 +252,7 @@ class RepairControllerMoreTest {
.thenAnswer(
inv -> {
List<String> cmd = inv.getArgument(0);
writeValidPdfTo(cmd, cmd.size() - 1);
writeValidPdfTo(Path.of(cmd.getLast()));
return okResult;
});
@@ -49,15 +49,7 @@ class UploadLimitServiceTest {
systemProps = mock(ApplicationProperties.System.class);
when(applicationProperties.getSystem()).thenReturn(systemProps);
uploadLimitService = new UploadLimitService();
// inject mock
try {
var field = UploadLimitService.class.getDeclaredField("applicationProperties");
field.setAccessible(true);
field.set(uploadLimitService, applicationProperties);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
uploadLimitService = new UploadLimitService(applicationProperties);
}
@ParameterizedTest(name = "getReadableUploadLimit case #{index}: rawValue={0}, expected={1}")
@@ -143,19 +143,12 @@ public class AuditRestController {
@RequestParam(value = "period", defaultValue = "week") String period) {
// Calculate days based on period
int days;
switch (period.toLowerCase()) {
case "day":
days = 1;
break;
case "month":
days = 30;
break;
case "week":
default:
days = 7;
break;
}
int days =
switch (period.toLowerCase()) {
case "day" -> 1;
case "month" -> 30;
default -> 7;
};
// Get events from the specified period
Instant startDate = Instant.now().minus(java.time.Duration.ofDays(days));
@@ -269,19 +262,12 @@ public class AuditRestController {
@RequestParam(value = "period", defaultValue = "week") String period) {
// Calculate days based on period
int days;
switch (period.toLowerCase()) {
case "day":
days = 1;
break;
case "month":
days = 30;
break;
case "week":
default:
days = 7;
break;
}
int days =
switch (period.toLowerCase()) {
case "day" -> 1;
case "month" -> 30;
default -> 7;
};
// Get events from the specified period and previous period
Instant now = Instant.now();
@@ -754,7 +740,7 @@ public class AuditRestController {
List<Map<String, Object>> files =
(List<Map<String, Object>>) eventData.get("files");
if (files != null && !files.isEmpty()) {
Map<String, Object> firstFile = files.get(0);
Map<String, Object> firstFile = files.getFirst();
data.put("documentname", String.valueOf(firstFile.getOrDefault("name", "")));
data.put("author", String.valueOf(firstFile.getOrDefault("pdfAuthor", "")));
data.put("filehash", String.valueOf(firstFile.getOrDefault("fileHash", "")));
@@ -131,7 +131,7 @@ public class PolicyExecutor {
// One call over all inputs. The outputs derive from a single input only when exactly
// one entered; otherwise (a genuine merge) there is no single source.
ToolResult r = callEndpoint(step, inputFiles, supportingFiles);
Integer origin = inputOrigins.size() == 1 ? inputOrigins.get(0) : null;
Integer origin = inputOrigins.size() == 1 ? inputOrigins.getFirst() : null;
for (Resource file : r.files()) {
files.add(file);
origins.add(origin);
@@ -2,7 +2,6 @@ package stirling.software.proprietary.security.configuration;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
@@ -19,7 +18,6 @@ public class CacheConfig {
private final ApplicationProperties applicationProperties;
@Autowired
public CacheConfig(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@@ -61,7 +61,7 @@ public class CustomSaml2ResponseAuthenticationConverter
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
Assertion assertion = responseToken.getResponse().getAssertions().getFirst();
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Debug log with actual values
@@ -117,6 +117,6 @@ public class CustomSaml2ResponseAuthenticationConverter
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
List<Object> values = attributes.get(name);
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
return values != null && !values.isEmpty() ? values.getFirst().toString() : null;
}
}
@@ -232,7 +232,7 @@ public class DatabaseService implements DatabaseServiceInterface {
List<FileInfo> backupList = this.getBackupList();
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
Path latestExport = Path.of(backupList.get(0).getFilePath());
Path latestExport = Path.of(backupList.getFirst().getFilePath());
executeDatabaseScript(latestExport);
}
@@ -376,7 +376,7 @@ public class DatabaseService implements DatabaseServiceInterface {
List<FileInfo> backupList = this.getBackupList();
List<Pair<FileInfo, Boolean>> deletedFiles = new ArrayList<>();
if (!backupList.isEmpty()) {
FileInfo lastBackup = backupList.get(backupList.size() - 1);
FileInfo lastBackup = backupList.getLast();
try {
Files.deleteIfExists(Path.of(lastBackup.getFilePath()));
deletedFiles.add(Pair.of(lastBackup, true));
@@ -399,7 +399,7 @@ public class DatabaseService implements DatabaseServiceInterface {
Comparator.comparing(
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
FileInfo oldestFile = filteredBackupList.get(0);
FileInfo oldestFile = filteredBackupList.getFirst();
Files.deleteIfExists(Path.of(oldestFile.getFilePath()));
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
} catch (IOException e) {
@@ -193,6 +193,6 @@ public class SessionPersistentRegistry implements SessionRegistry {
allSessions.sort((s1, s2) -> s2.getLastRequest().compareTo(s1.getLastRequest()));
// The first session in the list is the latest session for the given principal name
return Optional.of(allSessions.get(0));
return Optional.of(allSessions.getFirst());
}
}
@@ -299,7 +299,7 @@ public class PortalInfraAuditService {
Object files = data.get("files");
if (files instanceof List<?> list
&& !list.isEmpty()
&& list.get(0) instanceof Map<?, ?> f) {
&& list.getFirst() instanceof Map<?, ?> f) {
Object name = ((Map<String, Object>) f).get("name");
return name != null ? String.valueOf(name) : null;
}
@@ -119,20 +119,18 @@ public class UnifiedAccessControlService {
public ShareAccessRole getEffectiveRole(WorkflowParticipant participant) {
ParticipantStatus status = participant.getStatus();
switch (status) {
case SIGNED:
case DECLINED:
// After action completed, downgrade to read-only
return ShareAccessRole.VIEWER;
case PENDING:
case NOTIFIED:
case VIEWED:
// Active participants retain their assigned role
return participant.getAccessRole();
default:
return switch (status) {
case SIGNED, DECLINED ->
// After action completed, downgrade to read-only
ShareAccessRole.VIEWER;
case PENDING, NOTIFIED, VIEWED ->
// Active participants retain their assigned role
participant.getAccessRole();
default -> {
log.warn("Unknown participant status: {}", status);
return ShareAccessRole.VIEWER;
}
yield ShareAccessRole.VIEWER;
}
};
}
/** Checks if a user can access a specific file */
@@ -152,7 +152,7 @@ public class AccountLinkController {
if (rows.isEmpty()) {
return new LeaderTeam(null, null, HttpStatus.FORBIDDEN);
}
TeamMembership m = rows.get(0);
TeamMembership m = rows.getFirst();
if (m.getRole() != TeamRole.LEADER) {
return new LeaderTeam(null, null, HttpStatus.FORBIDDEN);
}
@@ -105,7 +105,7 @@ public class PaygInvoicesController {
if (rows.isEmpty()) {
return ResponseEntity.ok(List.of());
}
Long teamId = rows.get(0).getTeam().getId();
Long teamId = rows.getFirst().getTeam().getId();
// No PAYG extension row OR no Stripe customer id → team has never subscribed → no
// invoices. Empty list, not 404 — the UI distinguishes "no invoices yet" from a
@@ -85,7 +85,7 @@ public class PaygPaymentMethodController {
if (rows.isEmpty()) {
return ResponseEntity.ok(PaymentMethodResponse.absent());
}
Long teamId = rows.get(0).getTeam().getId();
Long teamId = rows.getFirst().getTeam().getId();
Optional<PaygTeamExtensions> ext = extRepo.findById(teamId);
if (ext.isEmpty() || ext.get().getStripeCustomerId() == null) {
@@ -427,7 +427,7 @@ public class PaygWalletController {
private Optional<TeamMembership> primaryMembership(Long userId) {
List<TeamMembership> rows = memberRepo.findPrimaryMembership(userId);
return rows.isEmpty() ? Optional.empty() : Optional.of(rows.get(0));
return rows.isEmpty() ? Optional.empty() : Optional.of(rows.getFirst());
}
private List<MemberRow> buildMemberRows(
@@ -324,7 +324,7 @@ public class JobChargeService {
List<Path> paths = inputs.stream().map(JobInput::path).toList();
DocumentMetrics metrics =
multiparts.size() == 1
? classifier.classify(multiparts.get(0), paths.get(0), policy)
? classifier.classify(multiparts.getFirst(), paths.getFirst(), policy)
: classifier.classify(multiparts, paths, policy);
// Apply the policy-level minChargeUnits floor per design § 3.4. The classifier returns
// raw docUnits with a "non-empty input → ≥1" floor; the charge formula's
@@ -75,7 +75,7 @@ public class JpaJobLineageStore implements JobLineageStore {
List<LineageMatch> matches =
hashRepository.findOpenJobsForSignatures(
userId, JobStatus.OPEN, since, storageKeys, Limit.of(1));
return matches.isEmpty() ? Optional.empty() : Optional.of(matches.get(0));
return matches.isEmpty() ? Optional.empty() : Optional.of(matches.getFirst());
}
@Override
@@ -291,7 +291,7 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter {
if (!(raw instanceof List<?> amrList) || amrList.isEmpty()) {
return WEB;
}
Object first = amrList.get(0);
Object first = amrList.getFirst();
if (!(first instanceof Map<?, ?> entry)) {
return WEB;
}