mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
New common module (#3573)
# Description of Changes Introduced a new `common` module for shared libs and commonly used classes. See the screenshot below for the file structure and classes that have been moved. --- <img width="452" alt="Screenshot 2025-05-22 at 11 46 56" src="https://github.com/user-attachments/assets/c9badabc-48f9-4079-b83e-7cfde0fb840f" /> <img width="470" alt="Screenshot 2025-05-22 at 11 47 30" src="https://github.com/user-attachments/assets/e8315b09-2e78-4c50-b9de-4dd9b9b0ecb1" /> ## 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/DeveloperGuide.md) (if applicable) - [x] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/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/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
### Eclipse ###
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.exe
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
.classpath
|
||||
.project
|
||||
version.properties
|
||||
|
||||
#### Stirling-PDF Files ###
|
||||
pipeline/watchedFolders/
|
||||
pipeline/finishedFolders/
|
||||
customFiles/
|
||||
configs/
|
||||
watchedFolders/
|
||||
clientWebUI/
|
||||
!cucumber/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
/testing/file_snapshots
|
||||
SwaggerDoc.json
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.lock
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
.apt_generated_test/
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# Uncomment this line if you wish to ignore the project description file.
|
||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||
#.project
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Spring Boot Tooling
|
||||
.sts4-cache/
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.db
|
||||
/build
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
|
||||
# Virtual environments
|
||||
.env*
|
||||
.venv*
|
||||
env*/
|
||||
venv*/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# VS Code
|
||||
/.vscode/**/*
|
||||
!/.vscode/settings.json
|
||||
!/.vscode/extensions.json
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
*.iml
|
||||
out/
|
||||
|
||||
# Ignore Mac DS_Store files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# cucumber
|
||||
/cucumber/reports/**
|
||||
|
||||
# Certs and Security Files
|
||||
*.p12
|
||||
*.pk8
|
||||
*.pem
|
||||
*.crt
|
||||
*.cer
|
||||
*.cert
|
||||
*.der
|
||||
*.key
|
||||
*.csr
|
||||
*.kdbx
|
||||
*.jks
|
||||
*.asc
|
||||
|
||||
# SSH Keys
|
||||
*.pub
|
||||
*.priv
|
||||
id_rsa
|
||||
id_rsa.pub
|
||||
id_ecdsa
|
||||
id_ecdsa.pub
|
||||
id_ed25519
|
||||
id_ed25519.pub
|
||||
.ssh/
|
||||
*ssh
|
||||
|
||||
# cache
|
||||
.cache
|
||||
.ruff_cache
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
**/jcef-bundle/
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
*.mjs
|
||||
@@ -0,0 +1,52 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
}
|
||||
|
||||
group = 'stirling.software'
|
||||
version = '0.46.2'
|
||||
|
||||
ext {
|
||||
lombokVersion = "1.18.38"
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom 'org.springframework.boot:spring-boot-dependencies:3.4.5'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
implementation 'com.fathzer:javaluator:3.0.6'
|
||||
implementation 'com.posthog.java:posthog:1.2.0'
|
||||
implementation 'io.github.pixee:java-security-toolkit:1.2.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.17.0'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||
implementation 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
implementation "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
|
||||
implementation 'org.snakeyaml:snakeyaml-engine:2.9'
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6"
|
||||
|
||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
|
||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.apache.pdfbox.examples.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/** A custom RandomAccessRead implementation that deletes the file when closed */
|
||||
@Slf4j
|
||||
public class DeletingRandomAccessFile extends RandomAccessReadBufferedFile {
|
||||
private final Path tempFilePath;
|
||||
|
||||
public DeletingRandomAccessFile(File file) throws IOException {
|
||||
super(file);
|
||||
this.tempFilePath = file.toPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
} finally {
|
||||
try {
|
||||
boolean deleted = Files.deleteIfExists(tempFilePath);
|
||||
if (deleted) {
|
||||
log.info("Successfully deleted temp file: {}", tempFilePath);
|
||||
} else {
|
||||
log.warn("Failed to delete temp file (may not exist): {}", tempFilePath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting temp file: {}", tempFilePath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Lazy
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class AppConfig {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Getter
|
||||
@Value("${baseUrl:http://localhost}")
|
||||
private String baseUrl;
|
||||
|
||||
@Getter
|
||||
@Value("${server.servlet.context-path:/}")
|
||||
private String contextPath;
|
||||
|
||||
@Getter
|
||||
@Value("${server.port:8080}")
|
||||
private String serverPort;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
return templateEngine;
|
||||
}
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
return applicationProperties.getSecurity().getEnableLogin();
|
||||
}
|
||||
|
||||
@Bean(name = "appName")
|
||||
public String appName() {
|
||||
String homeTitle = applicationProperties.getUi().getAppName();
|
||||
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "appVersion")
|
||||
public String appVersion() {
|
||||
Resource resource = new ClassPathResource("version.properties");
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
props.load(resource.getInputStream());
|
||||
return props.getProperty("version");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
@Bean(name = "homeText")
|
||||
public String homeText() {
|
||||
return (applicationProperties.getUi().getHomeDescription() != null)
|
||||
? applicationProperties.getUi().getHomeDescription()
|
||||
: "null";
|
||||
}
|
||||
|
||||
@Bean(name = "languages")
|
||||
public List<String> languages() {
|
||||
return applicationProperties.getUi().getLanguages();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String contextPath(@Value("${server.servlet.context-path}") String contextPath) {
|
||||
return contextPath;
|
||||
}
|
||||
|
||||
@Bean(name = "navBarText")
|
||||
public String navBarText() {
|
||||
String defaultNavBar =
|
||||
applicationProperties.getUi().getAppNameNavbar() != null
|
||||
? applicationProperties.getUi().getAppNameNavbar()
|
||||
: applicationProperties.getUi().getAppName();
|
||||
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "enableAlphaFunctionality")
|
||||
public boolean enableAlphaFunctionality() {
|
||||
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
|
||||
? applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
: false;
|
||||
}
|
||||
|
||||
@Bean(name = "rateLimit")
|
||||
public boolean rateLimit() {
|
||||
String rateLimit = System.getProperty("rateLimit");
|
||||
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
||||
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
|
||||
}
|
||||
|
||||
@Bean(name = "RunningInDocker")
|
||||
public boolean runningInDocker() {
|
||||
return Files.exists(Paths.get("/.dockerenv"));
|
||||
}
|
||||
|
||||
@Bean(name = "configDirMounted")
|
||||
public boolean isRunningInDockerWithConfig() {
|
||||
Path dockerEnv = Paths.get("/.dockerenv");
|
||||
// default to true if not docker
|
||||
if (!Files.exists(dockerEnv)) {
|
||||
return true;
|
||||
}
|
||||
Path mountInfo = Paths.get("/proc/1/mountinfo");
|
||||
// this should always exist, if not some unknown usecase
|
||||
if (!Files.exists(mountInfo)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
||||
@Bean(name = "activeSecurity")
|
||||
public boolean missingActiveSecurity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "directoryFilter")
|
||||
public Predicate<Path> processOnlyFiles() {
|
||||
return path -> {
|
||||
if (Files.isDirectory(path)) {
|
||||
return !path.toString().contains("processing");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean(name = "termsAndConditions")
|
||||
public String termsAndConditions() {
|
||||
return applicationProperties.getLegal().getTermsAndConditions();
|
||||
}
|
||||
|
||||
@Bean(name = "privacyPolicy")
|
||||
public String privacyPolicy() {
|
||||
return applicationProperties.getLegal().getPrivacyPolicy();
|
||||
}
|
||||
|
||||
@Bean(name = "cookiePolicy")
|
||||
public String cookiePolicy() {
|
||||
return applicationProperties.getLegal().getCookiePolicy();
|
||||
}
|
||||
|
||||
@Bean(name = "impressum")
|
||||
public String impressum() {
|
||||
return applicationProperties.getLegal().getImpressum();
|
||||
}
|
||||
|
||||
@Bean(name = "accessibilityStatement")
|
||||
public String accessibilityStatement() {
|
||||
return applicationProperties.getLegal().getAccessibilityStatement();
|
||||
}
|
||||
|
||||
@Bean(name = "analyticsPrompt")
|
||||
@Scope("request")
|
||||
public boolean analyticsPrompt() {
|
||||
return applicationProperties.getSystem().getEnableAnalytics() == null;
|
||||
}
|
||||
|
||||
@Bean(name = "analyticsEnabled")
|
||||
@Scope("request")
|
||||
public boolean analyticsEnabled() {
|
||||
if (applicationProperties.getPremium().isEnabled()) return true;
|
||||
return applicationProperties.getSystem().isAnalyticsEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "StirlingPDFLabel")
|
||||
public String stirlingPDFLabel() {
|
||||
return "Stirling-PDF" + " v" + appVersion();
|
||||
}
|
||||
|
||||
@Bean(name = "UUID")
|
||||
public String uuid() {
|
||||
return applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationProperties.Security security() {
|
||||
return applicationProperties.getSecurity();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationProperties.Security.OAUTH2 oAuth2() {
|
||||
return applicationProperties.getSecurity().getOauth2();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationProperties.Premium premium() {
|
||||
return applicationProperties.getPremium();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationProperties.System system() {
|
||||
return applicationProperties.getSystem();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApplicationProperties.Datasource datasource() {
|
||||
return applicationProperties.getSystem().getDatasource();
|
||||
}
|
||||
|
||||
@Bean(name = "disablePixel")
|
||||
public boolean disablePixel() {
|
||||
return Boolean.getBoolean(env.getProperty("DISABLE_PIXEL"));
|
||||
}
|
||||
|
||||
@Bean(name = "machineType")
|
||||
public String determineMachineType() {
|
||||
try {
|
||||
boolean isDocker = runningInDocker();
|
||||
boolean isKubernetes = System.getenv("KUBERNETES_SERVICE_HOST") != null;
|
||||
boolean isBrowserOpen = "true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"));
|
||||
|
||||
if (isKubernetes) {
|
||||
return "Kubernetes";
|
||||
} else if (isDocker) {
|
||||
return "Docker";
|
||||
} else if (isBrowserOpen) {
|
||||
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
if (os.contains("win")) {
|
||||
return "Client-windows";
|
||||
} else if (os.contains("mac")) {
|
||||
return "Client-mac";
|
||||
} else {
|
||||
return "Client-unix";
|
||||
}
|
||||
} else {
|
||||
return "Server-jar";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.YamlHelper;
|
||||
|
||||
/**
|
||||
* A naive, line-based approach to merging "settings.yml" with "settings.yml.template" while
|
||||
* preserving exact whitespace, blank lines, and inline comments -- but we only rewrite the file if
|
||||
* the merged content actually differs.
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigInitializer {
|
||||
|
||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||
// 1) If settings file doesn't exist, create from template
|
||||
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
if (Files.notExists(destPath)) {
|
||||
Files.createDirectories(destPath.getParent());
|
||||
try (InputStream in =
|
||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
if (in == null) {
|
||||
throw new FileNotFoundException(
|
||||
"Resource file not found: settings.yml.template");
|
||||
}
|
||||
Files.copy(in, destPath);
|
||||
}
|
||||
log.info("Created settings file from template");
|
||||
} else {
|
||||
// 2) Merge existing file with the template
|
||||
URL templateResource = getClass().getClassLoader().getResource("settings.yml.template");
|
||||
if (templateResource == null) {
|
||||
throw new IOException("Resource not found: settings.yml.template");
|
||||
}
|
||||
|
||||
// Copy template to a temp location so we can read lines
|
||||
Path tempTemplatePath = Files.createTempFile("settings.yml", ".template");
|
||||
try (InputStream in = templateResource.openStream()) {
|
||||
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
// Copy setting.yaml to a temp location so we can read lines
|
||||
Path settingTempPath = Files.createTempFile("settings", ".yaml");
|
||||
try (InputStream in = Files.newInputStream(destPath)) {
|
||||
Files.copy(in, settingTempPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
|
||||
YamlHelper settingsFile = new YamlHelper(settingTempPath);
|
||||
|
||||
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
|
||||
|
||||
boolean changesMade =
|
||||
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
||||
if (changesMade) {
|
||||
settingsTemplateFile.save(destPath);
|
||||
log.info("Settings file updated based on template changes.");
|
||||
} else {
|
||||
log.info("No changes detected; settings file left as-is.");
|
||||
}
|
||||
|
||||
Files.deleteIfExists(tempTemplatePath);
|
||||
Files.deleteIfExists(settingTempPath);
|
||||
}
|
||||
|
||||
// 3) Ensure custom settings file exists
|
||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
||||
if (Files.notExists(customSettingsPath)) {
|
||||
Files.createFile(customSettingsPath);
|
||||
log.info("Created custom_settings file: {}", customSettingsPath);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
private void migrateEnterpriseEditionToPremium(YamlHelper yaml, YamlHelper template) {
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "enabled") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "enabled"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "enabled"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "key") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "key"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "key"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "SSOAutoLogin"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")
|
||||
!= null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "autoUpdateMetadata"),
|
||||
yaml.getValueByExactKeyPath(
|
||||
"enterpriseEdition", "CustomMetadata", "autoUpdateMetadata"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "author"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "creator"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")
|
||||
!= null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "producer"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.InputStreamTemplateResource;
|
||||
|
||||
@Slf4j
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
public FileFallbackTemplateResolver(ResourceLoader resourceLoader) {
|
||||
super();
|
||||
this.resourceLoader = resourceLoader;
|
||||
setSuffix(".html");
|
||||
}
|
||||
|
||||
// Note this does not work in local IDE, Prod jar only.
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(
|
||||
IEngineConfiguration configuration,
|
||||
String ownerTemplate,
|
||||
String template,
|
||||
String resourceName,
|
||||
String characterEncoding,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
Resource resource =
|
||||
resourceLoader.getResource(
|
||||
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
|
||||
try {
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Log the exception to help with debugging issues loading external templates
|
||||
log.warn("Unable to read template '{}' from file system", resourceName, e);
|
||||
}
|
||||
|
||||
InputStream inputStream =
|
||||
Thread.currentThread()
|
||||
.getContextClassLoader()
|
||||
.getResourceAsStream("templates/" + resourceName);
|
||||
if (inputStream != null) {
|
||||
return new InputStreamTemplateResource(inputStream, "UTF-8");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class InstallationPathConfig {
|
||||
private static final String BASE_PATH;
|
||||
|
||||
// Root paths
|
||||
private static final String LOG_PATH;
|
||||
private static final String CONFIG_PATH;
|
||||
private static final String CUSTOM_FILES_PATH;
|
||||
private static final String CLIENT_WEBUI_PATH;
|
||||
|
||||
// Config paths
|
||||
private static final String SETTINGS_PATH;
|
||||
private static final String CUSTOM_SETTINGS_PATH;
|
||||
|
||||
// Custom file paths
|
||||
private static final String STATIC_PATH;
|
||||
private static final String TEMPLATES_PATH;
|
||||
private static final String SIGNATURES_PATH;
|
||||
|
||||
static {
|
||||
BASE_PATH = initializeBasePath();
|
||||
|
||||
// Initialize root paths
|
||||
LOG_PATH = BASE_PATH + "logs" + File.separator;
|
||||
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
||||
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
||||
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
||||
|
||||
// Initialize config paths
|
||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||
|
||||
// Initialize custom file paths
|
||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
||||
}
|
||||
|
||||
private static String initializeBasePath() {
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("win")) {
|
||||
return Paths.get(
|
||||
System.getenv("APPDATA"), // parent path
|
||||
"Stirling-PDF")
|
||||
+ File.separator;
|
||||
} else if (os.contains("mac")) {
|
||||
return Paths.get(
|
||||
System.getProperty("user.home"),
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Stirling-PDF")
|
||||
+ File.separator;
|
||||
} else {
|
||||
return Paths.get(
|
||||
System.getProperty("user.home"), // parent path
|
||||
".config",
|
||||
"Stirling-PDF")
|
||||
+ File.separator;
|
||||
}
|
||||
}
|
||||
return "." + File.separator;
|
||||
}
|
||||
|
||||
public static String getPath() {
|
||||
return BASE_PATH;
|
||||
}
|
||||
|
||||
public static String getLogPath() {
|
||||
return LOG_PATH;
|
||||
}
|
||||
|
||||
public static String getConfigPath() {
|
||||
return CONFIG_PATH;
|
||||
}
|
||||
|
||||
public static String getCustomFilesPath() {
|
||||
return CUSTOM_FILES_PATH;
|
||||
}
|
||||
|
||||
public static String getClientWebUIPath() {
|
||||
return CLIENT_WEBUI_PATH;
|
||||
}
|
||||
|
||||
public static String getSettingsPath() {
|
||||
return SETTINGS_PATH;
|
||||
}
|
||||
|
||||
public static String getCustomSettingsPath() {
|
||||
return CUSTOM_SETTINGS_PATH;
|
||||
}
|
||||
|
||||
public static String getStaticPath() {
|
||||
return STATIC_PATH;
|
||||
}
|
||||
|
||||
public static String getTemplatesPath() {
|
||||
return TEMPLATES_PATH;
|
||||
}
|
||||
|
||||
public static String getSignaturesPath() {
|
||||
return SIGNATURES_PATH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.posthog.java.PostHog;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class PostHogConfig {
|
||||
|
||||
@Value("${posthog.api.key}")
|
||||
private String posthogApiKey;
|
||||
|
||||
@Value("${posthog.host}")
|
||||
private String posthogHost;
|
||||
|
||||
private PostHog postHogClient;
|
||||
|
||||
@Bean
|
||||
public PostHog postHogClient() {
|
||||
postHogClient =
|
||||
new PostHog.Builder(posthogApiKey)
|
||||
.host(posthogHost)
|
||||
.logger(new PostHogLoggerImpl())
|
||||
.build();
|
||||
return postHogClient;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdownPostHog() {
|
||||
if (postHogClient != null) {
|
||||
postHogClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.posthog.java.PostHogLogger;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PostHogLoggerImpl implements PostHogLogger {
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
log.debug(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
log.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String message) {
|
||||
log.warn(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
log.error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message, Throwable throwable) {
|
||||
if (message.contains("Error sending events to PostHog")) {
|
||||
log.warn(
|
||||
"Error sending metrics, Likely caused by no internet connection. Non Blocking");
|
||||
} else {
|
||||
log.error(message, throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@Getter
|
||||
public class RuntimePathConfig {
|
||||
private final ApplicationProperties properties;
|
||||
private final String basePath;
|
||||
private final String weasyPrintPath;
|
||||
private final String unoConvertPath;
|
||||
|
||||
// Pipeline paths
|
||||
private final String pipelineWatchedFoldersPath;
|
||||
private final String pipelineFinishedFoldersPath;
|
||||
private final String pipelineDefaultWebUiConfigs;
|
||||
private final String pipelinePath;
|
||||
|
||||
public RuntimePathConfig(ApplicationProperties properties) {
|
||||
this.properties = properties;
|
||||
this.basePath = InstallationPathConfig.getPath();
|
||||
|
||||
this.pipelinePath = Path.of(basePath, "pipeline").toString();
|
||||
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
|
||||
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
||||
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
||||
|
||||
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
|
||||
|
||||
this.pipelineWatchedFoldersPath =
|
||||
resolvePath(
|
||||
defaultWatchedFolders,
|
||||
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
|
||||
this.pipelineFinishedFoldersPath =
|
||||
resolvePath(
|
||||
defaultFinishedFolders,
|
||||
pipeline != null ? pipeline.getFinishedFoldersDir() : null);
|
||||
this.pipelineDefaultWebUiConfigs =
|
||||
resolvePath(
|
||||
defaultWebUIConfigs,
|
||||
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
|
||||
|
||||
boolean isDocker = isRunningInDocker();
|
||||
|
||||
// Initialize Operation paths
|
||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||
|
||||
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
||||
this.weasyPrintPath =
|
||||
resolvePath(
|
||||
defaultWeasyPrintPath,
|
||||
operations != null ? operations.getWeasyprint() : null);
|
||||
this.unoConvertPath =
|
||||
resolvePath(
|
||||
defaultUnoConvertPath,
|
||||
operations != null ? operations.getUnoconvert() : null);
|
||||
}
|
||||
|
||||
private String resolvePath(String defaultPath, String customPath) {
|
||||
return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
|
||||
}
|
||||
|
||||
private boolean isRunningInDocker() {
|
||||
return Files.exists(Path.of("/.dockerenv"));
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
import org.springframework.core.io.support.PropertySourceFactory;
|
||||
|
||||
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||
|
||||
@Override
|
||||
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(encodedResource.getResource());
|
||||
Properties properties = factory.getObject();
|
||||
|
||||
return new PropertiesPropertySource(
|
||||
encodedResource.getResource().getFilename(), properties);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package stirling.software.common.configuration.interfaces;
|
||||
|
||||
public interface ShowAdminInterface {
|
||||
default boolean getShowUpdateOnlyAdmins() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.YamlPropertySourceFactory;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import stirling.software.common.util.ValidationUtils;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@ConfigurationProperties(prefix = "")
|
||||
public class ApplicationProperties {
|
||||
|
||||
private Legal legal = new Legal();
|
||||
private Security security = new Security();
|
||||
private System system = new System();
|
||||
private Ui ui = new Ui();
|
||||
private Endpoints endpoints = new Endpoints();
|
||||
private Metrics metrics = new Metrics();
|
||||
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
||||
|
||||
private Mail mail = new Mail();
|
||||
|
||||
private Premium premium = new Premium();
|
||||
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
||||
private AutoPipeline autoPipeline = new AutoPipeline();
|
||||
private ProcessExecutor processExecutor = new ProcessExecutor();
|
||||
|
||||
@Bean
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
throws IOException {
|
||||
String configPath = InstallationPathConfig.getSettingsPath();
|
||||
log.debug("Attempting to load settings from: " + configPath);
|
||||
|
||||
File file = new File(configPath);
|
||||
if (!file.exists()) {
|
||||
log.error("Warning: Settings file does not exist at: " + configPath);
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(configPath);
|
||||
if (!resource.exists()) {
|
||||
throw new FileNotFoundException("Settings file not found at: " + configPath);
|
||||
}
|
||||
|
||||
EncodedResource encodedResource = new EncodedResource(resource);
|
||||
PropertySource<?> propertySource =
|
||||
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
|
||||
log.debug("Loaded properties: " + propertySource.getSource());
|
||||
|
||||
return propertySource;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AutoPipeline {
|
||||
private String outputFolder;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Legal {
|
||||
private String termsAndConditions;
|
||||
private String privacyPolicy;
|
||||
private String accessibilityStatement;
|
||||
private String cookiePolicy;
|
||||
private String impressum;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Security {
|
||||
private Boolean enableLogin;
|
||||
private Boolean csrfDisabled;
|
||||
private InitialLogin initialLogin = new InitialLogin();
|
||||
private OAUTH2 oauth2 = new OAUTH2();
|
||||
private SAML2 saml2 = new SAML2();
|
||||
private int loginAttemptCount;
|
||||
private long loginResetTimeMinutes;
|
||||
private String loginMethod = "all";
|
||||
private String customGlobalAPIKey;
|
||||
|
||||
public Boolean isAltLogin() {
|
||||
return saml2.getEnabled() || oauth2.getEnabled();
|
||||
}
|
||||
|
||||
public enum LoginMethods {
|
||||
ALL("all"),
|
||||
NORMAL("normal"),
|
||||
OAUTH2("oauth2"),
|
||||
SAML2("saml2");
|
||||
|
||||
private String method;
|
||||
|
||||
LoginMethods(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUserPass() {
|
||||
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|
||||
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
|
||||
}
|
||||
|
||||
public boolean isOauth2Active() {
|
||||
return (oauth2 != null
|
||||
&& oauth2.getEnabled()
|
||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||
}
|
||||
|
||||
public boolean isSaml2Active() {
|
||||
return (saml2 != null
|
||||
&& saml2.getEnabled()
|
||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class InitialLogin {
|
||||
private String username;
|
||||
@ToString.Exclude private String password;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
public static class SAML2 {
|
||||
private String provider;
|
||||
private Boolean enabled = false;
|
||||
private Boolean autoCreateUser = false;
|
||||
private Boolean blockRegistration = false;
|
||||
private String registrationId = "stirling";
|
||||
@ToString.Exclude private String idpMetadataUri;
|
||||
private String idpSingleLogoutUrl;
|
||||
private String idpSingleLoginUrl;
|
||||
private String idpIssuer;
|
||||
private String idpCert;
|
||||
@ToString.Exclude private String privateKey;
|
||||
@ToString.Exclude private String spCert;
|
||||
|
||||
public InputStream getIdpMetadataUri() throws IOException {
|
||||
if (idpMetadataUri.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
|
||||
.getInputStream();
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(idpMetadataUri);
|
||||
URL url = uri.toURL();
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
return connection.getInputStream();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Resource getSpCert() {
|
||||
if (spCert == null) return null;
|
||||
if (spCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(spCert);
|
||||
}
|
||||
}
|
||||
|
||||
public Resource getIdpCert() {
|
||||
if (idpCert == null) return null;
|
||||
if (idpCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(idpCert);
|
||||
}
|
||||
}
|
||||
|
||||
public Resource getPrivateKey() {
|
||||
if (privateKey.startsWith("classpath:")) {
|
||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(privateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OAUTH2 {
|
||||
private Boolean enabled = false;
|
||||
private String issuer;
|
||||
private String clientId;
|
||||
@ToString.Exclude private String clientSecret;
|
||||
private Boolean autoCreateUser = false;
|
||||
private Boolean blockRegistration = false;
|
||||
private String useAsUsername;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String provider;
|
||||
private Client client = new Client();
|
||||
|
||||
public void setScopes(String scopes) {
|
||||
List<String> scopesList =
|
||||
Arrays.stream(scopes.split(",")).map(String::trim).toList();
|
||||
this.scopes.addAll(scopesList);
|
||||
}
|
||||
|
||||
protected boolean isValid(String value, String name) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
protected boolean isValid(Collection<String> value, String name) {
|
||||
return value != null && !value.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return !ValidationUtils.isStringEmpty(this.getIssuer())
|
||||
&& !ValidationUtils.isStringEmpty(this.getClientId())
|
||||
&& !ValidationUtils.isStringEmpty(this.getClientSecret())
|
||||
&& !ValidationUtils.isCollectionEmpty(this.getScopes())
|
||||
&& !ValidationUtils.isStringEmpty(this.getUseAsUsername());
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Client {
|
||||
private GoogleProvider google = new GoogleProvider();
|
||||
private GitHubProvider github = new GitHubProvider();
|
||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||
|
||||
public Provider get(String registrationId) throws UnsupportedProviderException {
|
||||
return switch (registrationId.toLowerCase()) {
|
||||
case "google" -> getGoogle();
|
||||
case "github" -> getGithub();
|
||||
case "keycloak" -> getKeycloak();
|
||||
default ->
|
||||
throw new UnsupportedProviderException(
|
||||
"Logout from the provider "
|
||||
+ registrationId
|
||||
+ " is not supported. "
|
||||
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class System {
|
||||
private String defaultLocale;
|
||||
private Boolean googlevisibility;
|
||||
private boolean showUpdate;
|
||||
private Boolean showUpdateOnlyAdmin;
|
||||
private boolean customHTMLFiles;
|
||||
private String tessdataDir;
|
||||
private Boolean enableAlphaFunctionality;
|
||||
private Boolean enableAnalytics;
|
||||
private Datasource datasource;
|
||||
private Boolean disableSanitize;
|
||||
private Boolean enableUrlToPDF;
|
||||
private CustomPaths customPaths = new CustomPaths();
|
||||
private String fileUploadLimit;
|
||||
|
||||
public boolean isAnalyticsEnabled() {
|
||||
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CustomPaths {
|
||||
private Pipeline pipeline = new Pipeline();
|
||||
private Operations operations = new Operations();
|
||||
|
||||
@Data
|
||||
public static class Pipeline {
|
||||
private String watchedFoldersDir;
|
||||
private String finishedFoldersDir;
|
||||
private String webUIConfigsDir;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Operations {
|
||||
private String weasyprint;
|
||||
private String unoconvert;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Datasource {
|
||||
private boolean enableCustomDatabase;
|
||||
private String customDatabaseUrl;
|
||||
private String type;
|
||||
private String hostName;
|
||||
private Integer port;
|
||||
private String name;
|
||||
private String username;
|
||||
@ToString.Exclude private String password;
|
||||
}
|
||||
|
||||
public enum Driver {
|
||||
H2("h2"),
|
||||
POSTGRESQL("postgresql"),
|
||||
ORACLE("oracle"),
|
||||
MYSQL("mysql");
|
||||
|
||||
private final String driverName;
|
||||
|
||||
Driver(String driverName) {
|
||||
this.driverName = driverName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return """
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
.formatted(driverName);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Ui {
|
||||
private String appName;
|
||||
private String homeDescription;
|
||||
private String appNameNavbar;
|
||||
private List<String> languages;
|
||||
|
||||
public String getAppName() {
|
||||
return appName != null && appName.trim().length() > 0 ? appName : null;
|
||||
}
|
||||
|
||||
public String getHomeDescription() {
|
||||
return homeDescription != null && homeDescription.trim().length() > 0
|
||||
? homeDescription
|
||||
: null;
|
||||
}
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar != null && appNameNavbar.trim().length() > 0
|
||||
? appNameNavbar
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Endpoints {
|
||||
private List<String> toRemove;
|
||||
private List<String> groupsToRemove;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Metrics {
|
||||
private Boolean enabled;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AutomaticallyGenerated {
|
||||
@ToString.Exclude private String key;
|
||||
private String UUID;
|
||||
private String appVersion;
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
@Data
|
||||
@Deprecated(since = "0.45.0")
|
||||
public static class EnterpriseEdition {
|
||||
private boolean enabled;
|
||||
@ToString.Exclude private String key;
|
||||
private int maxUsers;
|
||||
private boolean ssoAutoLogin;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
|
||||
@Data
|
||||
public static class CustomMetadata {
|
||||
private boolean autoUpdateMetadata;
|
||||
private String author;
|
||||
private String creator;
|
||||
private String producer;
|
||||
|
||||
public String getCreator() {
|
||||
return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator;
|
||||
}
|
||||
|
||||
public String getProducer() {
|
||||
return producer == null || producer.trim().isEmpty() ? "Stirling-PDF" : producer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Mail {
|
||||
private boolean enabled;
|
||||
private String host;
|
||||
private int port;
|
||||
private String username;
|
||||
@ToString.Exclude private String password;
|
||||
private String from;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Premium {
|
||||
private boolean enabled;
|
||||
@ToString.Exclude private String key;
|
||||
private int maxUsers;
|
||||
private ProFeatures proFeatures = new ProFeatures();
|
||||
private EnterpriseFeatures enterpriseFeatures = new EnterpriseFeatures();
|
||||
|
||||
@Data
|
||||
public static class ProFeatures {
|
||||
private boolean ssoAutoLogin;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
private GoogleDrive googleDrive = new GoogleDrive();
|
||||
|
||||
@Data
|
||||
public static class CustomMetadata {
|
||||
private boolean autoUpdateMetadata;
|
||||
private String author;
|
||||
private String creator;
|
||||
private String producer;
|
||||
|
||||
public String getCreator() {
|
||||
return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator;
|
||||
}
|
||||
|
||||
public String getProducer() {
|
||||
return producer == null || producer.trim().isEmpty()
|
||||
? "Stirling-PDF"
|
||||
: producer;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class GoogleDrive {
|
||||
private boolean enabled;
|
||||
private String clientId;
|
||||
private String apiKey;
|
||||
private String appId;
|
||||
|
||||
public String getClientId() {
|
||||
return clientId == null || clientId.trim().isEmpty() ? "" : clientId;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey == null || apiKey.trim().isEmpty() ? "" : apiKey;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId == null || appId.trim().isEmpty() ? "" : appId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class EnterpriseFeatures {
|
||||
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
||||
|
||||
@Data
|
||||
public static class PersistentMetrics {
|
||||
private boolean enabled;
|
||||
private int retentionDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProcessExecutor {
|
||||
private SessionLimit sessionLimit = new SessionLimit();
|
||||
private TimeoutMinutes timeoutMinutes = new TimeoutMinutes();
|
||||
|
||||
@Data
|
||||
public static class SessionLimit {
|
||||
private int libreOfficeSessionLimit;
|
||||
private int pdfToHtmlSessionLimit;
|
||||
private int pythonOpenCvSessionLimit;
|
||||
private int weasyPrintSessionLimit;
|
||||
private int installAppSessionLimit;
|
||||
private int calibreSessionLimit;
|
||||
private int qpdfSessionLimit;
|
||||
private int tesseractSessionLimit;
|
||||
|
||||
public int getQpdfSessionLimit() {
|
||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||
}
|
||||
|
||||
public int getTesseractSessionLimit() {
|
||||
return tesseractSessionLimit > 0 ? tesseractSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getLibreOfficeSessionLimit() {
|
||||
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getPdfToHtmlSessionLimit() {
|
||||
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getPythonOpenCvSessionLimit() {
|
||||
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
|
||||
}
|
||||
|
||||
public int getWeasyPrintSessionLimit() {
|
||||
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
|
||||
}
|
||||
|
||||
public int getInstallAppSessionLimit() {
|
||||
return installAppSessionLimit > 0 ? installAppSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getCalibreSessionLimit() {
|
||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TimeoutMinutes {
|
||||
private long libreOfficeTimeoutMinutes;
|
||||
private long pdfToHtmlTimeoutMinutes;
|
||||
private long pythonOpenCvTimeoutMinutes;
|
||||
private long weasyPrintTimeoutMinutes;
|
||||
private long installAppTimeoutMinutes;
|
||||
private long calibreTimeoutMinutes;
|
||||
private long tesseractTimeoutMinutes;
|
||||
private long qpdfTimeoutMinutes;
|
||||
|
||||
public long getTesseractTimeoutMinutes() {
|
||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getQpdfTimeoutMinutes() {
|
||||
return qpdfTimeoutMinutes > 0 ? qpdfTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getLibreOfficeTimeoutMinutes() {
|
||||
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getPdfToHtmlTimeoutMinutes() {
|
||||
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
|
||||
}
|
||||
|
||||
public long getPythonOpenCvTimeoutMinutes() {
|
||||
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getWeasyPrintTimeoutMinutes() {
|
||||
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getInstallAppTimeoutMinutes() {
|
||||
return installAppTimeoutMinutes > 0 ? installAppTimeoutMinutes : 60;
|
||||
}
|
||||
|
||||
public long getCalibreTimeoutMinutes() {
|
||||
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class FileInfo {
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private String fileName;
|
||||
private String filePath;
|
||||
private LocalDateTime modificationDate;
|
||||
private long fileSize;
|
||||
private LocalDateTime creationDate;
|
||||
|
||||
// Converts the file path string to a Path object.
|
||||
public Path getFilePathAsPath() {
|
||||
return Paths.get(filePath);
|
||||
}
|
||||
|
||||
// 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));
|
||||
} else if (fileSize >= 1024 * 1024) {
|
||||
return String.format(Locale.US, "%.2f MB", fileSize / (1024.0 * 1024));
|
||||
} else if (fileSize >= 1024) {
|
||||
return String.format(Locale.US, "%.2f KB", fileSize / 1024.0);
|
||||
} else {
|
||||
return String.format("%d Bytes", fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Formats the modification date to a string.
|
||||
public String getFormattedModificationDate() {
|
||||
return modificationDate.format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
// Formats the creation date to a string.
|
||||
public String getFormattedCreationDate() {
|
||||
return creationDate.format(DATE_FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
public class InputStreamTemplateResource implements ITemplateResource {
|
||||
private InputStream inputStream;
|
||||
private String characterEncoding;
|
||||
|
||||
public InputStreamTemplateResource(InputStream inputStream, String characterEncoding) {
|
||||
this.inputStream = inputStream;
|
||||
this.characterEncoding = characterEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader reader() throws IOException {
|
||||
return new InputStreamReader(inputStream, characterEncoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITemplateResource relative(String relativeLocation) {
|
||||
// Implement logic for relative resources, if needed
|
||||
throw new UnsupportedOperationException("Relative resources not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "InputStream resource [Stream]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseName() {
|
||||
return "streamResource";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return inputStream != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class PdfMetadata {
|
||||
private String author;
|
||||
private String producer;
|
||||
private String title;
|
||||
private String creator;
|
||||
private String subject;
|
||||
private String keywords;
|
||||
private Calendar creationDate;
|
||||
private Calendar modificationDate;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package stirling.software.common.model.api;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class GeneralFile {
|
||||
|
||||
@Schema(
|
||||
description = "The input file",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
format = "binary")
|
||||
private MultipartFile fileInput;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package stirling.software.common.model.api;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class PDFFile {
|
||||
@Schema(
|
||||
description = "The input PDF file",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
contentMediaType = "application/pdf",
|
||||
format = "binary")
|
||||
private MultipartFile fileInput;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package stirling.software.common.model.api.converters;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class HTMLToPdfRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "Zoom level for displaying the website. Default is '1'.",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "1")
|
||||
private float zoom;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package stirling.software.common.model.api.misc;
|
||||
|
||||
public enum HighContrastColorCombination {
|
||||
WHITE_TEXT_ON_BLACK,
|
||||
BLACK_TEXT_ON_WHITE,
|
||||
YELLOW_TEXT_ON_BLACK,
|
||||
GREEN_TEXT_ON_BLACK,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package stirling.software.common.model.api.misc;
|
||||
|
||||
public enum ReplaceAndInvert {
|
||||
HIGH_CONTRAST_COLOR,
|
||||
CUSTOM_COLOR,
|
||||
FULL_INVERSION,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package stirling.software.common.model.api.security;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class RedactionArea {
|
||||
@Schema(description = "The left edge point of the area to be redacted.")
|
||||
private Double x;
|
||||
|
||||
@Schema(description = "The top edge point of the area to be redacted.")
|
||||
private Double y;
|
||||
|
||||
@Schema(description = "The height of the area to be redacted.")
|
||||
private Double height;
|
||||
|
||||
@Schema(description = "The width of the area to be redacted.")
|
||||
private Double width;
|
||||
|
||||
@Schema(description = "The page on which the area should be redacted.")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "The color used to redact the specified area.")
|
||||
private String color;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package stirling.software.common.model.enumeration;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Role {
|
||||
|
||||
// Unlimited access
|
||||
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.admin"),
|
||||
|
||||
// Unlimited access
|
||||
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
|
||||
|
||||
// 40 API calls Per Day, 40 web calls
|
||||
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40, "adminUserSettings.apiUser"),
|
||||
|
||||
// 20 API calls Per Day, 20 web calls
|
||||
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20, "adminUserSettings.extraApiUser"),
|
||||
|
||||
// 0 API calls per day and 20 web calls
|
||||
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20, "adminUserSettings.webOnlyUser"),
|
||||
|
||||
INTERNAL_API_USER(
|
||||
"STIRLING-PDF-BACKEND-API-USER",
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
"adminUserSettings.internalApiUser"),
|
||||
|
||||
DEMO_USER("ROLE_DEMO_USER", 100, 100, "adminUserSettings.demoUser");
|
||||
|
||||
private final String roleId;
|
||||
private final int apiCallsPerDay;
|
||||
private final int webCallsPerDay;
|
||||
private final String roleName;
|
||||
|
||||
public static String getRoleNameByRoleId(String roleId) {
|
||||
// Using the fromString method to get the Role enum based on the roleId
|
||||
Role role = fromString(roleId);
|
||||
// Return the roleName of the found Role enum
|
||||
return role.getRoleName();
|
||||
}
|
||||
|
||||
// Method to retrieve all role IDs and role names
|
||||
public static Map<String, String> getAllRoleDetails() {
|
||||
// Using LinkedHashMap to preserve order
|
||||
Map<String, String> roleDetails = new LinkedHashMap<>();
|
||||
for (Role role : Role.values()) {
|
||||
roleDetails.put(role.getRoleId(), role.getRoleName());
|
||||
}
|
||||
return roleDetails;
|
||||
}
|
||||
|
||||
public static Role fromString(String roleId) {
|
||||
for (Role role : Role.values()) {
|
||||
if (role.getRoleId().equalsIgnoreCase(roleId)) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No Role defined for id: " + roleId);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package stirling.software.common.model.enumeration;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum UsernameAttribute {
|
||||
MAIL("mail"),
|
||||
EMAIL("email"),
|
||||
LOGIN("login"),
|
||||
PROFILE("profile"),
|
||||
NAME("name"),
|
||||
UID("uid"),
|
||||
USERNAME("username"),
|
||||
NICKNAME("nickname"),
|
||||
GIVEN_NAME("given_name"),
|
||||
MIDDLE_NAME("middle_name"),
|
||||
FAMILY_NAME("family_name"),
|
||||
PREFERRED_NAME("preferred_name"),
|
||||
PREFERRED_USERNAME("preferred_username");
|
||||
|
||||
private final String name;
|
||||
|
||||
UsernameAttribute(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package stirling.software.common.model.exception;
|
||||
|
||||
public class UnsupportedClaimException extends RuntimeException {
|
||||
public UnsupportedClaimException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package stirling.software.common.model.exception;
|
||||
|
||||
public class UnsupportedProviderException extends Exception {
|
||||
public UnsupportedProviderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package stirling.software.common.model.oauth2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import lombok.NoArgsConstructor;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class GitHubProvider extends Provider {
|
||||
|
||||
private static final String NAME = "github";
|
||||
private static final String CLIENT_NAME = "GitHub";
|
||||
private static final String AUTHORIZATION_URI = "https://github.com/login/oauth/authorize";
|
||||
private static final String TOKEN_URI = "https://github.com/login/oauth/access_token";
|
||||
private static final String USER_INFO_URI = "https://api.github.com/user";
|
||||
|
||||
public GitHubProvider(
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername) {
|
||||
super(
|
||||
null,
|
||||
NAME,
|
||||
CLIENT_NAME,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
useAsUsername != null ? useAsUsername : UsernameAttribute.LOGIN,
|
||||
AUTHORIZATION_URI,
|
||||
TOKEN_URI,
|
||||
USER_INFO_URI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthorizationUri() {
|
||||
return AUTHORIZATION_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenUri() {
|
||||
return TOKEN_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserInfoUri() {
|
||||
return USER_INFO_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return CLIENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
Collection<String> scopes = super.getScopes();
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("read:user");
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GitHub [clientId="
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package stirling.software.common.model.oauth2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import lombok.NoArgsConstructor;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class GoogleProvider extends Provider {
|
||||
|
||||
private static final String NAME = "google";
|
||||
private static final String CLIENT_NAME = "Google";
|
||||
private static final String AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
private static final String TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
|
||||
private static final String USER_INFO_URI =
|
||||
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
|
||||
|
||||
public GoogleProvider(
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername) {
|
||||
super(
|
||||
null,
|
||||
NAME,
|
||||
CLIENT_NAME,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
useAsUsername,
|
||||
AUTHORIZATION_URI,
|
||||
TOKEN_URI,
|
||||
USER_INFO_URI);
|
||||
}
|
||||
|
||||
public String getAuthorizationUri() {
|
||||
return AUTHORIZATION_URI;
|
||||
}
|
||||
|
||||
public String getTokenUri() {
|
||||
return TOKEN_URI;
|
||||
}
|
||||
|
||||
public String getUserinfoUri() {
|
||||
return USER_INFO_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return CLIENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
Collection<String> scopes = super.getScopes();
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("https://www.googleapis.com/auth/userinfo.email");
|
||||
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Google [clientId="
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package stirling.software.common.model.oauth2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import lombok.NoArgsConstructor;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class KeycloakProvider extends Provider {
|
||||
|
||||
private static final String NAME = "keycloak";
|
||||
private static final String CLIENT_NAME = "Keycloak";
|
||||
|
||||
public KeycloakProvider(
|
||||
String issuer,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername) {
|
||||
super(
|
||||
issuer,
|
||||
NAME,
|
||||
CLIENT_NAME,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
useAsUsername,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return CLIENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
Collection<String> scopes = super.getScopes();
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("profile");
|
||||
scopes.add("email");
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Keycloak [issuer="
|
||||
+ getIssuer()
|
||||
+ ", clientId="
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (getClientSecret() != null && !getClientSecret().isBlank() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package stirling.software.common.model.oauth2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
import stirling.software.common.model.exception.UnsupportedClaimException;
|
||||
import static stirling.software.common.model.enumeration.UsernameAttribute.EMAIL;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Provider {
|
||||
|
||||
public static final String EXCEPTION_MESSAGE = "The attribute %s is not supported for %s.";
|
||||
|
||||
private String issuer;
|
||||
private String name;
|
||||
private String clientName;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes;
|
||||
private UsernameAttribute useAsUsername;
|
||||
private String authorizationUri;
|
||||
private String tokenUri;
|
||||
private String userInfoUri;
|
||||
|
||||
public Provider(
|
||||
String issuer,
|
||||
String name,
|
||||
String clientName,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername,
|
||||
String authorizationUri,
|
||||
String tokenUri,
|
||||
String userInfoUri) {
|
||||
this.issuer = issuer;
|
||||
this.name = name;
|
||||
this.clientName = clientName;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.scopes = scopes == null ? new ArrayList<>() : scopes;
|
||||
this.useAsUsername =
|
||||
useAsUsername != null ? validateUsernameAttribute(useAsUsername) : EMAIL;
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.tokenUri = tokenUri;
|
||||
this.userInfoUri = userInfoUri;
|
||||
}
|
||||
|
||||
public void setScopes(String scopes) {
|
||||
if (scopes != null && !scopes.isBlank()) {
|
||||
this.scopes = Arrays.stream(scopes.split(",")).map(String::trim).toList();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameAttribute validateKeycloakUsernameAttribute(
|
||||
UsernameAttribute usernameAttribute) {
|
||||
switch (usernameAttribute) {
|
||||
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> {
|
||||
return 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;
|
||||
}
|
||||
default ->
|
||||
throw new UnsupportedClaimException(
|
||||
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) {
|
||||
switch (usernameAttribute) {
|
||||
case LOGIN, EMAIL, NAME -> {
|
||||
return usernameAttribute;
|
||||
}
|
||||
default ->
|
||||
throw new UnsupportedClaimException(
|
||||
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Provider [name="
|
||||
+ getName()
|
||||
+ ", clientName="
|
||||
+ getClientName()
|
||||
+ ", clientId="
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.examples.util.DeletingRandomAccessFile;
|
||||
import org.apache.pdfbox.io.IOUtils;
|
||||
import org.apache.pdfbox.io.MemoryUsageSetting;
|
||||
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
|
||||
import org.apache.pdfbox.io.ScratchFile;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
/**
|
||||
* Adaptive PDF document factory that optimizes memory usage based on file size and available system
|
||||
* resources.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class CustomPDFDocumentFactory {
|
||||
|
||||
private final PdfMetadataService pdfMetadataService;
|
||||
|
||||
// Memory thresholds and limits
|
||||
|
||||
private static final long SMALL_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
||||
// Files smaller than this threshold are loaded entirely in memory for better performance.
|
||||
// These files use IOUtils.createMemoryOnlyStreamCache() which keeps all document data in RAM.
|
||||
// No temp files are created for document data, reducing I/O operations but consuming more
|
||||
// memory.
|
||||
|
||||
private static final long LARGE_FILE_THRESHOLD = 50 * 1024 * 1024; // 50 MB
|
||||
// Files between SMALL and LARGE thresholds use file-based caching with ScratchFile,
|
||||
// but are loaded directly from byte arrays if provided that way.
|
||||
// When loading from byte arrays, once size exceeds this threshold, bytes are first
|
||||
// written to temp files before loading to reduce memory pressure.
|
||||
|
||||
private static final long LARGE_FILE_USAGE = 10 * 1024 * 1024;
|
||||
|
||||
private static final long EXTREMELY_LARGE_THRESHOLD = 100 * 1024 * 1024; // 100 MB
|
||||
// Files exceeding this threshold use specialized loading with RandomAccessReadBufferedFile
|
||||
// which provides buffered access to the file without loading the entire content at once.
|
||||
// These files are always processed using file-based caching with minimal memory footprint,
|
||||
// trading some performance for significantly reduced memory usage.
|
||||
// For extremely large PDFs, this prevents OutOfMemoryErrors at the cost of being more I/O
|
||||
// bound.
|
||||
|
||||
private static final double MIN_FREE_MEMORY_PERCENTAGE = 30.0; // 30%
|
||||
private static final long MIN_FREE_MEMORY_BYTES = 4L * 1024 * 1024 * 1024; // 4 GB
|
||||
|
||||
// Counter for tracking temporary resources
|
||||
private static final AtomicLong tempCounter = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* Main entry point for loading a PDF document from a file. Automatically selects the most
|
||||
* appropriate loading strategy.
|
||||
*/
|
||||
public PDDocument load(File file) throws IOException {
|
||||
return load(file, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for loading a PDF document from a file with read-only option. Automatically
|
||||
* selects the most appropriate loading strategy.
|
||||
*/
|
||||
public PDDocument load(File file, boolean readOnly) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("File cannot be null");
|
||||
}
|
||||
|
||||
long fileSize = file.length();
|
||||
log.debug("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024));
|
||||
|
||||
PDDocument doc = loadAdaptively(file, fileSize);
|
||||
if (!readOnly) {
|
||||
postProcessDocument(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for loading a PDF document from a Path. Automatically selects the most
|
||||
* appropriate loading strategy.
|
||||
*/
|
||||
public PDDocument load(Path path) throws IOException {
|
||||
return load(path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for loading a PDF document from a Path with read-only option. Automatically
|
||||
* selects the most appropriate loading strategy.
|
||||
*/
|
||||
public PDDocument load(Path path, boolean readOnly) throws IOException {
|
||||
if (path == null) {
|
||||
throw new IllegalArgumentException("File cannot be null");
|
||||
}
|
||||
|
||||
long fileSize = Files.size(path);
|
||||
log.debug("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024));
|
||||
|
||||
PDDocument doc = loadAdaptively(path.toFile(), fileSize);
|
||||
if (!readOnly) {
|
||||
postProcessDocument(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Load a PDF from byte array with automatic optimization. */
|
||||
public PDDocument load(byte[] input) throws IOException {
|
||||
return load(input, false);
|
||||
}
|
||||
|
||||
/** Load a PDF from byte array with automatic optimization and read-only option. */
|
||||
public PDDocument load(byte[] input, boolean readOnly) throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("Input bytes cannot be null");
|
||||
}
|
||||
|
||||
long dataSize = input.length;
|
||||
log.debug("Loading PDF from byte array, size: {}MB", dataSize / (1024 * 1024));
|
||||
|
||||
PDDocument doc = loadAdaptively(input, dataSize);
|
||||
if (!readOnly) {
|
||||
postProcessDocument(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Load a PDF from InputStream with automatic optimization. */
|
||||
public PDDocument load(InputStream input) throws IOException {
|
||||
return load(input, false);
|
||||
}
|
||||
|
||||
/** Load a PDF from InputStream with automatic optimization and read-only option. */
|
||||
public PDDocument load(InputStream input, boolean readOnly) throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("InputStream cannot be null");
|
||||
}
|
||||
|
||||
// Since we don't know the size upfront, buffer to a temp file
|
||||
Path tempFile = createTempFile("pdf-stream-");
|
||||
|
||||
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
PDDocument doc = loadAdaptively(tempFile.toFile(), Files.size(tempFile));
|
||||
if (!readOnly) {
|
||||
postProcessDocument(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Load with password from InputStream */
|
||||
public PDDocument load(InputStream input, String password) throws IOException {
|
||||
return load(input, password, false);
|
||||
}
|
||||
|
||||
/** Load with password from InputStream and read-only option */
|
||||
public PDDocument load(InputStream input, String password, boolean readOnly)
|
||||
throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("InputStream cannot be null");
|
||||
}
|
||||
|
||||
// Since we don't know the size upfront, buffer to a temp file
|
||||
Path tempFile = createTempFile("pdf-stream-");
|
||||
|
||||
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
PDDocument doc =
|
||||
loadAdaptivelyWithPassword(tempFile.toFile(), Files.size(tempFile), password);
|
||||
if (!readOnly) {
|
||||
postProcessDocument(doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Load from a file path string */
|
||||
public PDDocument load(String path) throws IOException {
|
||||
return load(path, false);
|
||||
}
|
||||
|
||||
/** Load from a file path string with read-only option */
|
||||
public PDDocument load(String path, boolean readOnly) throws IOException {
|
||||
return load(new File(path), readOnly);
|
||||
}
|
||||
|
||||
/** Load from a PDFFile object */
|
||||
public PDDocument load(PDFFile pdfFile) throws IOException {
|
||||
return load(pdfFile, false);
|
||||
}
|
||||
|
||||
/** Load from a PDFFile object with read-only option */
|
||||
public PDDocument load(PDFFile pdfFile, boolean readOnly) throws IOException {
|
||||
return load(pdfFile.getFileInput(), readOnly);
|
||||
}
|
||||
|
||||
/** Load from a MultipartFile */
|
||||
public PDDocument load(MultipartFile pdfFile) throws IOException {
|
||||
return load(pdfFile, false);
|
||||
}
|
||||
|
||||
/** Load from a MultipartFile with read-only option */
|
||||
public PDDocument load(MultipartFile pdfFile, boolean readOnly) throws IOException {
|
||||
return load(pdfFile.getInputStream(), readOnly);
|
||||
}
|
||||
|
||||
/** Load with password from MultipartFile */
|
||||
public PDDocument load(MultipartFile fileInput, String password) throws IOException {
|
||||
return load(fileInput, password, false);
|
||||
}
|
||||
|
||||
/** Load with password from MultipartFile with read-only option */
|
||||
public PDDocument load(MultipartFile fileInput, String password, boolean readOnly)
|
||||
throws IOException {
|
||||
return load(fileInput.getInputStream(), password, readOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the appropriate caching strategy based on file size and available memory. This
|
||||
* common method is used by both password and non-password loading paths.
|
||||
*/
|
||||
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
||||
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||
long freeMemory = Runtime.getRuntime().freeMemory();
|
||||
long totalMemory = Runtime.getRuntime().totalMemory();
|
||||
long usedMemory = totalMemory - freeMemory;
|
||||
|
||||
// Calculate percentage of free memory
|
||||
double freeMemoryPercent = (double) (maxMemory - usedMemory) / maxMemory * 100;
|
||||
long actualFreeMemory = maxMemory - usedMemory;
|
||||
|
||||
// Log memory status
|
||||
log.debug(
|
||||
"Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB",
|
||||
actualFreeMemory / (1024 * 1024),
|
||||
String.format("%.2f", freeMemoryPercent),
|
||||
usedMemory / (1024 * 1024),
|
||||
maxMemory / (1024 * 1024));
|
||||
|
||||
// If free memory is critically low, always use file-based caching
|
||||
if (freeMemoryPercent < MIN_FREE_MEMORY_PERCENTAGE
|
||||
|| actualFreeMemory < MIN_FREE_MEMORY_BYTES) {
|
||||
log.debug(
|
||||
"Low memory detected ({}%), forcing file-based cache",
|
||||
String.format("%.2f", freeMemoryPercent));
|
||||
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
|
||||
} else if (contentSize < SMALL_FILE_THRESHOLD) {
|
||||
log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024);
|
||||
return IOUtils.createMemoryOnlyStreamCache();
|
||||
} else if (contentSize < LARGE_FILE_THRESHOLD) {
|
||||
// For medium files (10-50MB), use a mixed approach
|
||||
log.debug(
|
||||
"Using mixed memory/file cache for medium document ({}MB)",
|
||||
contentSize / (1024 * 1024));
|
||||
return createScratchFileCacheFunction(MemoryUsageSetting.setupMixed(LARGE_FILE_USAGE));
|
||||
} else {
|
||||
log.debug("Using file-based cache for large document");
|
||||
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the existing loadAdaptively method to use the common function */
|
||||
private PDDocument loadAdaptively(Object source, long contentSize) throws IOException {
|
||||
// Get the appropriate caching strategy
|
||||
StreamCacheCreateFunction cacheFunction = getStreamCacheFunction(contentSize);
|
||||
|
||||
// If small handle as bytes and remove original file
|
||||
if (contentSize <= SMALL_FILE_THRESHOLD && source instanceof File file) {
|
||||
source = Files.readAllBytes(file.toPath());
|
||||
file.delete();
|
||||
}
|
||||
PDDocument document;
|
||||
if (source instanceof File file) {
|
||||
document = loadFromFile(file, contentSize, cacheFunction);
|
||||
} else if (source instanceof byte[] bytes) {
|
||||
document = loadFromBytes(bytes, contentSize, cacheFunction);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
/** Load a PDF with password protection using adaptive loading strategies */
|
||||
private PDDocument loadAdaptivelyWithPassword(Object source, long contentSize, String password)
|
||||
throws IOException {
|
||||
// Get the appropriate caching strategy
|
||||
StreamCacheCreateFunction cacheFunction = getStreamCacheFunction(contentSize);
|
||||
// If small handle as bytes and remove original file
|
||||
if (contentSize <= SMALL_FILE_THRESHOLD && source instanceof File file) {
|
||||
source = Files.readAllBytes(file.toPath());
|
||||
file.delete();
|
||||
}
|
||||
PDDocument document;
|
||||
if (source instanceof File file) {
|
||||
document = loadFromFileWithPassword(file, contentSize, cacheFunction, password);
|
||||
} else if (source instanceof byte[] bytes) {
|
||||
document = loadFromBytesWithPassword(bytes, contentSize, cacheFunction, password);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
/** Load a file with password */
|
||||
private PDDocument loadFromFileWithPassword(
|
||||
File file, long size, StreamCacheCreateFunction cache, String password)
|
||||
throws IOException {
|
||||
return Loader.loadPDF(new DeletingRandomAccessFile(file), password, null, null, cache);
|
||||
}
|
||||
|
||||
/** Load bytes with password */
|
||||
private PDDocument loadFromBytesWithPassword(
|
||||
byte[] bytes, long size, StreamCacheCreateFunction cache, String password)
|
||||
throws IOException {
|
||||
if (size >= SMALL_FILE_THRESHOLD) {
|
||||
log.debug("Writing large byte array to temp file for password-protected PDF");
|
||||
Path tempFile = createTempFile("pdf-bytes-");
|
||||
|
||||
Files.write(tempFile, bytes);
|
||||
return Loader.loadPDF(tempFile.toFile(), password, null, null, cache);
|
||||
}
|
||||
return Loader.loadPDF(bytes, password, null, null, cache);
|
||||
}
|
||||
|
||||
private StreamCacheCreateFunction createScratchFileCacheFunction(MemoryUsageSetting settings) {
|
||||
return () -> {
|
||||
try {
|
||||
return new ScratchFile(settings);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("ScratchFile initialization failed", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void postProcessDocument(PDDocument doc) throws IOException {
|
||||
pdfMetadataService.setDefaultMetadata(doc);
|
||||
removePassword(doc);
|
||||
}
|
||||
|
||||
private PDDocument loadFromFile(File file, long size, StreamCacheCreateFunction cache)
|
||||
throws IOException {
|
||||
return Loader.loadPDF(new DeletingRandomAccessFile(file), "", null, null, cache);
|
||||
}
|
||||
|
||||
private PDDocument loadFromBytes(byte[] bytes, long size, StreamCacheCreateFunction cache)
|
||||
throws IOException {
|
||||
if (size >= SMALL_FILE_THRESHOLD) {
|
||||
log.debug("Writing large byte array to temp file");
|
||||
Path tempFile = createTempFile("pdf-bytes-");
|
||||
|
||||
Files.write(tempFile, bytes);
|
||||
return loadFromFile(tempFile.toFile(), size, cache);
|
||||
}
|
||||
return Loader.loadPDF(bytes, "", null, null, cache);
|
||||
}
|
||||
|
||||
public PDDocument createNewDocument(MemoryUsageSetting settings) throws IOException {
|
||||
PDDocument doc = new PDDocument(createScratchFileCacheFunction(settings));
|
||||
pdfMetadataService.setDefaultMetadata(doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
public PDDocument createNewDocument() throws IOException {
|
||||
return createNewDocument(MemoryUsageSetting.setupTempFileOnly());
|
||||
}
|
||||
|
||||
public byte[] saveToBytes(PDDocument document) throws IOException {
|
||||
if (document.getNumberOfPages() < 10) { // Simple heuristic
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
document.save(baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
} else {
|
||||
Path tempFile = createTempFile("pdf-save-");
|
||||
|
||||
document.save(tempFile.toFile());
|
||||
return Files.readAllBytes(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Improved password handling
|
||||
private void removePassword(PDDocument document) throws IOException {
|
||||
if (document.isEncrypted()) {
|
||||
try {
|
||||
document.setAllSecurityToBeRemoved(true);
|
||||
} catch (Exception e) {
|
||||
log.error("Decryption failed", e);
|
||||
throw new IOException("PDF decryption failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temp file handling with enhanced logging
|
||||
private Path createTempFile(String prefix) throws IOException {
|
||||
Path file = Files.createTempFile(prefix + tempCounter.incrementAndGet() + "-", ".tmp");
|
||||
log.debug("Created temp file: {}", file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/** Create a uniquely named temporary directory */
|
||||
private Path createTempDirectory(String prefix) throws IOException {
|
||||
return Files.createTempDirectory(prefix + tempCounter.incrementAndGet() + "-");
|
||||
}
|
||||
|
||||
/** Create new document bytes based on an existing document */
|
||||
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
|
||||
try (PDDocument document = load(oldDocument)) {
|
||||
return saveToBytes(document);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create new document bytes based on an existing document file */
|
||||
public byte[] createNewBytesBasedOnOldDocument(File oldDocument) throws IOException {
|
||||
try (PDDocument document = load(oldDocument)) {
|
||||
return saveToBytes(document);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create new document bytes based on an existing PDDocument */
|
||||
public byte[] createNewBytesBasedOnOldDocument(PDDocument oldDocument) throws IOException {
|
||||
pdfMetadataService.setMetadataToPdf(
|
||||
oldDocument, pdfMetadataService.extractMetadataFromPdf(oldDocument), true);
|
||||
return saveToBytes(oldDocument);
|
||||
}
|
||||
|
||||
/** Create a new document based on an existing document bytes */
|
||||
public PDDocument createNewDocumentBasedOnOldDocument(byte[] oldDocument) throws IOException {
|
||||
try (PDDocument document = load(oldDocument)) {
|
||||
return createNewDocumentBasedOnOldDocument(document);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new document based on an existing document file */
|
||||
public PDDocument createNewDocumentBasedOnOldDocument(File oldDocument) throws IOException {
|
||||
try (PDDocument document = load(oldDocument)) {
|
||||
return createNewDocumentBasedOnOldDocument(document);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new document based on an existing PDDocument */
|
||||
public PDDocument createNewDocumentBasedOnOldDocument(PDDocument oldDocument)
|
||||
throws IOException {
|
||||
PDDocument document = createNewDocument();
|
||||
pdfMetadataService.setMetadataToPdf(
|
||||
document, pdfMetadataService.extractMetadataFromPdf(oldDocument), true);
|
||||
return document;
|
||||
}
|
||||
|
||||
/** Load document from a file and convert it to bytes */
|
||||
public byte[] loadToBytes(File file) throws IOException {
|
||||
try (PDDocument document = load(file)) {
|
||||
return saveToBytes(document);
|
||||
}
|
||||
}
|
||||
|
||||
/** Load document from bytes and convert it back to bytes */
|
||||
public byte[] loadToBytes(byte[] bytes) throws IOException {
|
||||
try (PDDocument document = load(bytes)) {
|
||||
return saveToBytes(document);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
|
||||
@Service
|
||||
public class PdfMetadataService {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final String stirlingPDFLabel;
|
||||
private final UserServiceInterface userService;
|
||||
private final boolean runningProOrHigher;
|
||||
|
||||
public PdfMetadataService(
|
||||
ApplicationProperties applicationProperties,
|
||||
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.stirlingPDFLabel = stirlingPDFLabel;
|
||||
this.userService = userService;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
}
|
||||
|
||||
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
||||
return PdfMetadata.builder()
|
||||
.author(pdf.getDocumentInformation().getAuthor())
|
||||
.producer(pdf.getDocumentInformation().getProducer())
|
||||
.title(pdf.getDocumentInformation().getTitle())
|
||||
.creator(pdf.getDocumentInformation().getCreator())
|
||||
.subject(pdf.getDocumentInformation().getSubject())
|
||||
.keywords(pdf.getDocumentInformation().getKeywords())
|
||||
.creationDate(pdf.getDocumentInformation().getCreationDate())
|
||||
.modificationDate(pdf.getDocumentInformation().getModificationDate())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setDefaultMetadata(PDDocument pdf) {
|
||||
PdfMetadata metadata = extractMetadataFromPdf(pdf);
|
||||
setMetadataToPdf(pdf, metadata);
|
||||
}
|
||||
|
||||
public void setMetadataToPdf(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||
setMetadataToPdf(pdf, pdfMetadata, false);
|
||||
}
|
||||
|
||||
public void setMetadataToPdf(PDDocument pdf, PdfMetadata pdfMetadata, boolean newlyCreated) {
|
||||
if (newlyCreated || pdfMetadata.getCreationDate() == null) {
|
||||
setNewDocumentMetadata(pdf, pdfMetadata);
|
||||
}
|
||||
setCommonMetadata(pdf, pdfMetadata);
|
||||
}
|
||||
|
||||
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||
|
||||
String creator = stirlingPDFLabel;
|
||||
|
||||
if (applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata()
|
||||
&& runningProOrHigher) {
|
||||
|
||||
creator =
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getCreator();
|
||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||
}
|
||||
|
||||
pdf.getDocumentInformation().setCreator(creator);
|
||||
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
||||
}
|
||||
|
||||
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||
String title = pdfMetadata.getTitle();
|
||||
pdf.getDocumentInformation().setTitle(title);
|
||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
||||
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||
|
||||
String author = pdfMetadata.getAuthor();
|
||||
if (applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata()
|
||||
&& runningProOrHigher) {
|
||||
author =
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getAuthor();
|
||||
|
||||
if (userService != null) {
|
||||
author = author.replace("username", userService.getCurrentUsername());
|
||||
}
|
||||
}
|
||||
pdf.getDocumentInformation().setAuthor(author);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.lang.management.ThreadMXBean;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.posthog.java.PostHog;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Service
|
||||
public class PostHogService {
|
||||
private final PostHog postHog;
|
||||
private final String uniqueId;
|
||||
private final String appVersion;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final UserServiceInterface userService;
|
||||
private final Environment env;
|
||||
private boolean configDirMounted;
|
||||
|
||||
public PostHogService(
|
||||
PostHog postHog,
|
||||
@Qualifier("UUID") String uuid,
|
||||
@Qualifier("configDirMounted") boolean configDirMounted,
|
||||
@Qualifier("appVersion") String appVersion,
|
||||
ApplicationProperties applicationProperties,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
Environment env) {
|
||||
this.postHog = postHog;
|
||||
this.uniqueId = uuid;
|
||||
this.appVersion = appVersion;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.userService = userService;
|
||||
this.env = env;
|
||||
this.configDirMounted = configDirMounted;
|
||||
captureSystemInfo();
|
||||
}
|
||||
|
||||
private void captureSystemInfo() {
|
||||
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
postHog.capture(uniqueId, "system_info_captured", captureServerMetrics());
|
||||
} catch (Exception e) {
|
||||
// Handle exceptions
|
||||
}
|
||||
}
|
||||
|
||||
public void captureEvent(String eventName, Map<String, Object> properties) {
|
||||
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
properties.put("app_version", appVersion);
|
||||
postHog.capture(uniqueId, eventName, properties);
|
||||
}
|
||||
|
||||
public Map<String, Object> captureServerMetrics() {
|
||||
Map<String, Object> metrics = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Application version
|
||||
metrics.put("app_version", appVersion);
|
||||
String deploymentType = "JAR"; // default
|
||||
if ("true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"))) {
|
||||
deploymentType = "EXE";
|
||||
} else if (isRunningInDocker()) {
|
||||
deploymentType = "DOCKER";
|
||||
}
|
||||
metrics.put("deployment_type", deploymentType);
|
||||
metrics.put("mounted_config_dir", configDirMounted);
|
||||
|
||||
// System info
|
||||
metrics.put("os_name", System.getProperty("os.name"));
|
||||
metrics.put("os_version", System.getProperty("os.version"));
|
||||
metrics.put("java_version", System.getProperty("java.version"));
|
||||
metrics.put("user_name", System.getProperty("user.name"));
|
||||
metrics.put("user_home", System.getProperty("user.home"));
|
||||
metrics.put("user_dir", System.getProperty("user.dir"));
|
||||
|
||||
// CPU and Memory
|
||||
metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors());
|
||||
metrics.put("total_memory", Runtime.getRuntime().totalMemory());
|
||||
metrics.put("free_memory", Runtime.getRuntime().freeMemory());
|
||||
|
||||
// Network and Server Identity
|
||||
InetAddress localHost = InetAddress.getLocalHost();
|
||||
metrics.put("ip_address", localHost.getHostAddress());
|
||||
metrics.put("hostname", localHost.getHostName());
|
||||
metrics.put("mac_address", getMacAddress());
|
||||
|
||||
// JVM info
|
||||
metrics.put("jvm_vendor", System.getProperty("java.vendor"));
|
||||
metrics.put("jvm_version", System.getProperty("java.vm.version"));
|
||||
|
||||
// Locale and Timezone
|
||||
metrics.put("system_language", System.getProperty("user.language"));
|
||||
metrics.put("system_country", System.getProperty("user.country"));
|
||||
metrics.put("timezone", TimeZone.getDefault().getID());
|
||||
metrics.put("locale", Locale.getDefault().toString());
|
||||
|
||||
// Disk info
|
||||
File root = new File(".");
|
||||
metrics.put("total_disk_space", root.getTotalSpace());
|
||||
metrics.put("free_disk_space", root.getFreeSpace());
|
||||
|
||||
// Process info
|
||||
metrics.put("process_id", ProcessHandle.current().pid());
|
||||
|
||||
// JVM metrics
|
||||
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
|
||||
metrics.put("jvm_uptime_ms", runtimeMXBean.getUptime());
|
||||
metrics.put("jvm_start_time", runtimeMXBean.getStartTime());
|
||||
|
||||
// Memory metrics
|
||||
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||
metrics.put("heap_memory_usage", memoryMXBean.getHeapMemoryUsage().getUsed());
|
||||
metrics.put("non_heap_memory_usage", memoryMXBean.getNonHeapMemoryUsage().getUsed());
|
||||
|
||||
// CPU metrics
|
||||
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
metrics.put("system_load_average", osMXBean.getSystemLoadAverage());
|
||||
|
||||
// Thread metrics
|
||||
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
||||
metrics.put("thread_count", threadMXBean.getThreadCount());
|
||||
metrics.put("daemon_thread_count", threadMXBean.getDaemonThreadCount());
|
||||
metrics.put("peak_thread_count", threadMXBean.getPeakThreadCount());
|
||||
|
||||
// Garbage collection metrics
|
||||
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
metrics.put("gc_" + gcBean.getName() + "_count", gcBean.getCollectionCount());
|
||||
metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime());
|
||||
}
|
||||
|
||||
// Network interfaces
|
||||
metrics.put("network_interfaces", getNetworkInterfacesInfo());
|
||||
|
||||
// Docker detection and stats
|
||||
boolean isDocker = isRunningInDocker();
|
||||
if (isDocker) {
|
||||
metrics.put("docker_metrics", getDockerMetrics());
|
||||
}
|
||||
metrics.put("application_properties", captureApplicationProperties());
|
||||
|
||||
if (userService != null) {
|
||||
metrics.put("total_users_created", userService.getTotalUsersCount());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
metrics.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private boolean isRunningInDocker() {
|
||||
return Files.exists(Paths.get("/.dockerenv"));
|
||||
}
|
||||
|
||||
private Map<String, Object> getDockerMetrics() {
|
||||
Map<String, Object> dockerMetrics = new HashMap<>();
|
||||
|
||||
// Network-related Docker info
|
||||
dockerMetrics.put("docker_network_mode", System.getenv("DOCKER_NETWORK_MODE"));
|
||||
|
||||
// Container name (if set)
|
||||
String containerName = System.getenv("CONTAINER_NAME");
|
||||
if (containerName != null && !containerName.isEmpty()) {
|
||||
dockerMetrics.put("container_name", containerName);
|
||||
}
|
||||
|
||||
// Docker compose information
|
||||
String composeProject = System.getenv("COMPOSE_PROJECT_NAME");
|
||||
String composeService = System.getenv("COMPOSE_SERVICE_NAME");
|
||||
if (composeProject != null && composeService != null) {
|
||||
dockerMetrics.put("compose_project", composeProject);
|
||||
dockerMetrics.put("compose_service", composeService);
|
||||
}
|
||||
|
||||
// Kubernetes-specific info (if running in K8s)
|
||||
String k8sPodName = System.getenv("KUBERNETES_POD_NAME");
|
||||
if (k8sPodName != null) {
|
||||
dockerMetrics.put("k8s_pod_name", k8sPodName);
|
||||
dockerMetrics.put("k8s_namespace", System.getenv("KUBERNETES_NAMESPACE"));
|
||||
dockerMetrics.put("k8s_node_name", System.getenv("KUBERNETES_NODE_NAME"));
|
||||
}
|
||||
|
||||
// New environment variables
|
||||
dockerMetrics.put("version_tag", System.getenv("VERSION_TAG"));
|
||||
dockerMetrics.put("without_enhanced_features", System.getenv("WITHOUT_ENHANCED_FEATURES"));
|
||||
dockerMetrics.put("fat_docker", System.getenv("FAT_DOCKER"));
|
||||
|
||||
return dockerMetrics;
|
||||
}
|
||||
|
||||
private void addIfNotEmpty(Map<String, Object> map, String key, Object value) {
|
||||
if (value != null) {
|
||||
if (value instanceof String strValue) {
|
||||
if (!StringUtils.isBlank(strValue)) {
|
||||
map.put(key, strValue.trim());
|
||||
}
|
||||
} else {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> captureApplicationProperties() {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
// Capture Legal properties
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"legal_termsAndConditions",
|
||||
applicationProperties.getLegal().getTermsAndConditions());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"legal_privacyPolicy",
|
||||
applicationProperties.getLegal().getPrivacyPolicy());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"legal_accessibilityStatement",
|
||||
applicationProperties.getLegal().getAccessibilityStatement());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"legal_cookiePolicy",
|
||||
applicationProperties.getLegal().getCookiePolicy());
|
||||
addIfNotEmpty(
|
||||
properties, "legal_impressum", applicationProperties.getLegal().getImpressum());
|
||||
|
||||
// Capture Security properties
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_enableLogin",
|
||||
applicationProperties.getSecurity().getEnableLogin());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_csrfDisabled",
|
||||
applicationProperties.getSecurity().getCsrfDisabled());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_loginAttemptCount",
|
||||
applicationProperties.getSecurity().getLoginAttemptCount());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_loginResetTimeMinutes",
|
||||
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_loginMethod",
|
||||
applicationProperties.getSecurity().getLoginMethod());
|
||||
|
||||
// Capture OAuth2 properties (excluding sensitive information)
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_oauth2_enabled",
|
||||
applicationProperties.getSecurity().getOauth2().getEnabled());
|
||||
if (applicationProperties.getSecurity().getOauth2().getEnabled()) {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_oauth2_autoCreateUser",
|
||||
applicationProperties.getSecurity().getOauth2().getAutoCreateUser());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_oauth2_blockRegistration",
|
||||
applicationProperties.getSecurity().getOauth2().getBlockRegistration());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_oauth2_useAsUsername",
|
||||
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_oauth2_provider",
|
||||
applicationProperties.getSecurity().getOauth2().getProvider());
|
||||
}
|
||||
// Capture System properties
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_defaultLocale",
|
||||
applicationProperties.getSystem().getDefaultLocale());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_googlevisibility",
|
||||
applicationProperties.getSystem().getGooglevisibility());
|
||||
addIfNotEmpty(
|
||||
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_showUpdateOnlyAdmin",
|
||||
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_customHTMLFiles",
|
||||
applicationProperties.getSystem().isCustomHTMLFiles());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_tessdataDir",
|
||||
applicationProperties.getSystem().getTessdataDir());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_enableAlphaFunctionality",
|
||||
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_enableAnalytics",
|
||||
applicationProperties.getSystem().isAnalyticsEnabled());
|
||||
|
||||
// Capture UI properties
|
||||
addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"ui_homeDescription",
|
||||
applicationProperties.getUi().getHomeDescription());
|
||||
addIfNotEmpty(
|
||||
properties, "ui_appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||
|
||||
// Capture Metrics properties
|
||||
addIfNotEmpty(
|
||||
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
|
||||
|
||||
// Capture EnterpriseEdition properties
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_enabled",
|
||||
applicationProperties.getPremium().isEnabled());
|
||||
if (applicationProperties.getPremium().isEnabled()) {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_autoUpdateMetadata",
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_author",
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getAuthor());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_creator",
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getCreator());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_producer",
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getProducer());
|
||||
}
|
||||
// Capture AutoPipeline properties
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"autoPipeline_outputFolder",
|
||||
applicationProperties.getAutoPipeline().getOutputFolder());
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private String getMacAddress() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces =
|
||||
NetworkInterface.getNetworkInterfaces();
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
NetworkInterface ni = networkInterfaces.nextElement();
|
||||
byte[] hardwareAddress = ni.getHardwareAddress();
|
||||
if (hardwareAddress != null) {
|
||||
String[] hexadecimal = new String[hardwareAddress.length];
|
||||
for (int i = 0; i < hardwareAddress.length; i++) {
|
||||
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
|
||||
}
|
||||
return String.join("-", hexadecimal);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Handle exception
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private Map<String, String> getNetworkInterfacesInfo() {
|
||||
Map<String, String> interfacesInfo = new HashMap<>();
|
||||
try {
|
||||
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
|
||||
while (nets.hasMoreElements()) {
|
||||
NetworkInterface netint = nets.nextElement();
|
||||
interfacesInfo.put(netint.getName(), netint.getDisplayName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
interfacesInfo.put("error", e.getMessage());
|
||||
}
|
||||
return interfacesInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
public interface UserServiceInterface {
|
||||
String getApiKeyForUser(String username);
|
||||
|
||||
String getCurrentUsername();
|
||||
|
||||
long getTotalUsersCount();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
public class CheckProgramInstall {
|
||||
|
||||
private static final List<String> PYTHON_COMMANDS = Arrays.asList("python3", "python");
|
||||
private static boolean pythonAvailableChecked = false;
|
||||
private static String availablePythonCommand = null;
|
||||
|
||||
/**
|
||||
* Checks which Python command is available and returns it.
|
||||
*
|
||||
* @return The available Python command ("python3" or "python"), or null if neither is
|
||||
* available.
|
||||
*/
|
||||
public static String getAvailablePythonCommand() {
|
||||
if (!pythonAvailableChecked) {
|
||||
availablePythonCommand =
|
||||
PYTHON_COMMANDS.stream()
|
||||
.filter(CheckProgramInstall::checkPythonVersion)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
pythonAvailableChecked = true;
|
||||
}
|
||||
return availablePythonCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified command is available by running the command with --version.
|
||||
*
|
||||
* @param pythonCommand The Python command to check.
|
||||
* @return true if the command is available, false otherwise.
|
||||
*/
|
||||
private static boolean checkPythonVersion(String pythonCommand) {
|
||||
try {
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||
.runCommandWithOutputHandling(
|
||||
Arrays.asList(pythonCommand, "--version"));
|
||||
return true; // Command succeeded, Python is available
|
||||
} catch (IOException | InterruptedException e) {
|
||||
return false; // Command failed, Python is not available
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any Python command is available.
|
||||
*
|
||||
* @return true if any Python command is available, false otherwise.
|
||||
*/
|
||||
public static boolean isPythonAvailable() {
|
||||
return getAvailablePythonCommand() != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import org.owasp.html.HtmlPolicyBuilder;
|
||||
import org.owasp.html.PolicyFactory;
|
||||
import org.owasp.html.Sanitizers;
|
||||
|
||||
public class CustomHtmlSanitizer {
|
||||
private static final PolicyFactory POLICY =
|
||||
Sanitizers.FORMATTING
|
||||
.and(Sanitizers.BLOCKS)
|
||||
.and(Sanitizers.STYLES)
|
||||
.and(Sanitizers.LINKS)
|
||||
.and(Sanitizers.TABLES)
|
||||
.and(Sanitizers.IMAGES)
|
||||
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
||||
|
||||
public static String sanitize(String html) {
|
||||
String htmlAfter = POLICY.sanitize(html);
|
||||
return htmlAfter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
public class ErrorUtils {
|
||||
|
||||
public static Model exceptionToModel(Model model, Exception ex) {
|
||||
StringWriter sw = new StringWriter();
|
||||
ex.printStackTrace(new PrintWriter(sw));
|
||||
String stackTrace = sw.toString();
|
||||
|
||||
model.addAttribute("errorMessage", ex.getMessage());
|
||||
model.addAttribute("stackTrace", stackTrace);
|
||||
return model;
|
||||
}
|
||||
|
||||
public static ModelAndView exceptionToModelView(Model model, Exception ex) {
|
||||
StringWriter sw = new StringWriter();
|
||||
ex.printStackTrace(new PrintWriter(sw));
|
||||
String stackTrace = sw.toString();
|
||||
|
||||
ModelAndView modelAndView = new ModelAndView();
|
||||
modelAndView.addObject("errorMessage", ex.getMessage());
|
||||
modelAndView.addObject("stackTrace", stackTrace);
|
||||
return modelAndView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static java.nio.file.StandardWatchEventKinds.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class FileMonitor {
|
||||
|
||||
private final Map<Path, WatchKey> path2KeyMapping;
|
||||
private final Set<Path> newlyDiscoveredFiles;
|
||||
private final ConcurrentHashMap.KeySetView<Path, Boolean> readyForProcessingFiles;
|
||||
private final WatchService watchService;
|
||||
private final Predicate<Path> pathFilter;
|
||||
private final Path rootDir;
|
||||
private Set<Path> stagingFiles;
|
||||
|
||||
/**
|
||||
* @param rootDirectory the root directory to monitor
|
||||
* @param pathFilter the filter to apply to the paths, return true if the path should be
|
||||
* monitored, false otherwise
|
||||
*/
|
||||
public FileMonitor(
|
||||
@Qualifier("directoryFilter") Predicate<Path> pathFilter,
|
||||
RuntimePathConfig runtimePathConfig)
|
||||
throws IOException {
|
||||
this.newlyDiscoveredFiles = new HashSet<>();
|
||||
this.path2KeyMapping = new HashMap<>();
|
||||
this.stagingFiles = new HashSet<>();
|
||||
this.pathFilter = pathFilter;
|
||||
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
|
||||
this.watchService = FileSystems.getDefault().newWatchService();
|
||||
log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath());
|
||||
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath());
|
||||
}
|
||||
|
||||
private boolean shouldNotProcess(Path path) {
|
||||
return !pathFilter.test(path);
|
||||
}
|
||||
|
||||
private void recursivelyRegisterEntry(Path dir) throws IOException {
|
||||
WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
|
||||
path2KeyMapping.put(dir, key);
|
||||
log.info("Registered directory: {}", dir);
|
||||
|
||||
try (Stream<Path> directoryVisitor = Files.walk(dir, 1)) {
|
||||
final Iterator<Path> iterator = directoryVisitor.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Path path = iterator.next();
|
||||
if (path.equals(dir) || shouldNotProcess(path)) continue;
|
||||
|
||||
if (Files.isDirectory(path)) {
|
||||
recursivelyRegisterEntry(path);
|
||||
} else if (Files.isRegularFile(path)) {
|
||||
handleFileCreation(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 5000)
|
||||
public void trackFiles() {
|
||||
/*
|
||||
All files observed changes in the last iteration will be considered as staging files.
|
||||
If those files are not modified in current iteration, they will be considered as ready for processing.
|
||||
*/
|
||||
stagingFiles = new HashSet<>(newlyDiscoveredFiles);
|
||||
readyForProcessingFiles.clear();
|
||||
|
||||
if (path2KeyMapping.isEmpty()) {
|
||||
log.warn("not monitoring any directory, even the root directory itself: {}", rootDir);
|
||||
if (Files.exists(
|
||||
rootDir)) { // if the root directory exists, re-register the root directory
|
||||
try {
|
||||
recursivelyRegisterEntry(rootDir);
|
||||
} catch (IOException e) {
|
||||
log.error("unable to register monitoring", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WatchKey key;
|
||||
while ((key = watchService.poll()) != null) {
|
||||
final Path watchingDir = (Path) key.watchable();
|
||||
key.pollEvents()
|
||||
.forEach(
|
||||
(evt) -> {
|
||||
final Path path = (Path) evt.context();
|
||||
final WatchEvent.Kind<?> kind = evt.kind();
|
||||
if (shouldNotProcess(path)) return;
|
||||
|
||||
try {
|
||||
if (Files.isDirectory(path)) {
|
||||
if (kind == ENTRY_CREATE) {
|
||||
handleDirectoryCreation(path);
|
||||
}
|
||||
/*
|
||||
we don't need to handle directory deletion or modification
|
||||
- directory deletion will be handled by key.reset()
|
||||
- directory modification indicates a new file creation or deletion, which is handled by below
|
||||
*/
|
||||
}
|
||||
Path relativePathFromRoot = watchingDir.resolve(path);
|
||||
if (kind == ENTRY_CREATE) {
|
||||
handleFileCreation(relativePathFromRoot);
|
||||
} else if (kind == ENTRY_DELETE) {
|
||||
handleFileRemoval(relativePathFromRoot);
|
||||
} else if (kind == ENTRY_MODIFY) {
|
||||
handleFileModification(relativePathFromRoot);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error while processing file: {}", path, e);
|
||||
}
|
||||
});
|
||||
|
||||
boolean isKeyValid = key.reset();
|
||||
if (!isKeyValid) { // key is invalid when the directory itself is no longer exists
|
||||
path2KeyMapping.remove((Path) key.watchable());
|
||||
}
|
||||
}
|
||||
readyForProcessingFiles.addAll(stagingFiles);
|
||||
}
|
||||
|
||||
private void handleDirectoryCreation(Path dir) throws IOException {
|
||||
WatchKey key = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
|
||||
path2KeyMapping.put(dir, key);
|
||||
}
|
||||
|
||||
private void handleFileRemoval(Path path) {
|
||||
newlyDiscoveredFiles.remove(path);
|
||||
stagingFiles.remove(path);
|
||||
}
|
||||
|
||||
private void handleFileCreation(Path path) {
|
||||
newlyDiscoveredFiles.add(path);
|
||||
stagingFiles.remove(path);
|
||||
}
|
||||
|
||||
private void handleFileModification(Path path) {
|
||||
// the logic is the same
|
||||
handleFileCreation(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file is ready for processing.
|
||||
*
|
||||
* <p>A file is ready for processing if it is not being modified for 5000ms.
|
||||
*
|
||||
* @param path the path of the file
|
||||
* @return true if the file is ready for processing, false otherwise
|
||||
*/
|
||||
public boolean isFileReadyForProcessing(Path path) {
|
||||
// 1. Check FileMonitor's ready list
|
||||
boolean isReady = readyForProcessingFiles.contains(path.toAbsolutePath());
|
||||
|
||||
// 2. Check last modified timestamp
|
||||
if (!isReady) {
|
||||
try {
|
||||
long lastModified = Files.getLastModifiedTime(path).toMillis();
|
||||
long currentTime = System.currentTimeMillis();
|
||||
isReady = (currentTime - lastModified) > 5000;
|
||||
} catch (IOException e) {
|
||||
log.info("Timestamp check failed for {}", path, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Direct file lock check
|
||||
if (isReady) {
|
||||
try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw");
|
||||
FileChannel channel = raf.getChannel()) {
|
||||
// Try acquiring an exclusive lock
|
||||
FileLock lock = channel.tryLock();
|
||||
if (lock == null) {
|
||||
isReady = false;
|
||||
} else {
|
||||
lock.release();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.info("File lock detected on {}", path);
|
||||
isReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isReady;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import io.github.pixee.security.ZipSecurity;
|
||||
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
public class FileToPdf {
|
||||
|
||||
public static byte[] convertHtmlToPdf(
|
||||
String weasyprintPath,
|
||||
HTMLToPdfRequest request,
|
||||
byte[] fileBytes,
|
||||
String fileName,
|
||||
boolean disableSanitize)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
Path tempInputFile = null;
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
if (fileName.endsWith(".html")) {
|
||||
tempInputFile = Files.createTempFile("input_", ".html");
|
||||
String sanitizedHtml =
|
||||
sanitizeHtmlContent(
|
||||
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
||||
Files.write(tempInputFile, sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||
} else if (fileName.endsWith(".zip")) {
|
||||
tempInputFile = Files.createTempFile("input_", ".zip");
|
||||
Files.write(tempInputFile, fileBytes);
|
||||
sanitizeHtmlFilesInZip(tempInputFile, disableSanitize);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported file format: " + fileName);
|
||||
}
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(weasyprintPath);
|
||||
command.add("-e");
|
||||
command.add("utf-8");
|
||||
command.add("-v");
|
||||
command.add("--pdf-forms");
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
} catch (IOException e) {
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
if (pdfBytes.length < 1) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
}
|
||||
|
||||
return pdfBytes;
|
||||
}
|
||||
|
||||
private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) {
|
||||
return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent;
|
||||
}
|
||||
|
||||
private static void sanitizeHtmlFilesInZip(Path zipFilePath, boolean disableSanitize)
|
||||
throws IOException {
|
||||
Path tempUnzippedDir = Files.createTempDirectory("unzipped_");
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(
|
||||
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName()));
|
||||
if (!entry.isDirectory()) {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
if (entry.getName().toLowerCase().endsWith(".html")
|
||||
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
||||
String content = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String sanitizedContent = sanitizeHtmlContent(content, disableSanitize);
|
||||
Files.write(filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
Files.copy(zipIn, filePath);
|
||||
}
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// Repack the sanitized files
|
||||
zipDirectory(tempUnzippedDir, zipFilePath);
|
||||
|
||||
// Clean up
|
||||
deleteDirectory(tempUnzippedDir);
|
||||
}
|
||||
|
||||
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
|
||||
try (ZipOutputStream zos =
|
||||
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
|
||||
Files.walk(sourceDir)
|
||||
.filter(path -> !Files.isDirectory(path))
|
||||
.forEach(
|
||||
path -> {
|
||||
ZipEntry zipEntry =
|
||||
new ZipEntry(sourceDir.relativize(path).toString());
|
||||
try {
|
||||
zos.putNextEntry(zipEntry);
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteDirectory(Path dir) throws IOException {
|
||||
Files.walkFileTree(
|
||||
dir,
|
||||
new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
|
||||
throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
|
||||
Path tempDirectory = Files.createTempDirectory("unzipped_");
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(filePath); // Explicitly create the directory structure
|
||||
} else {
|
||||
Files.createDirectories(
|
||||
filePath.getParent()); // Create parent directories if they don't exist
|
||||
Files.copy(zipIn, filePath);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// Search for the main HTML file.
|
||||
try (Stream<Path> walk = Files.walk(tempDirectory)) {
|
||||
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html")).toList();
|
||||
|
||||
if (htmlFiles.isEmpty()) {
|
||||
throw new IOException("No HTML files found in the unzipped directory.");
|
||||
}
|
||||
|
||||
// Prioritize 'index.html' if it exists, otherwise use the first .html file
|
||||
for (Path htmlFile : htmlFiles) {
|
||||
if ("index.html".equals(htmlFile.getFileName().toString())) {
|
||||
return htmlFile;
|
||||
}
|
||||
}
|
||||
|
||||
return htmlFiles.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
static String sanitizeZipFilename(String entryName) {
|
||||
if (entryName == null || entryName.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
// Remove any drive letters (e.g., "C:\") and leading forward/backslashes
|
||||
entryName = entryName.replaceAll("^[a-zA-Z]:[\\\\/]+", "");
|
||||
entryName = entryName.replaceAll("^[\\\\/]+", "");
|
||||
|
||||
// Recursively remove path traversal sequences
|
||||
while (entryName.contains("../") || entryName.contains("..\\")) {
|
||||
entryName = entryName.replace("../", "").replace("..\\", "");
|
||||
}
|
||||
// Normalize all backslashes to forward slashes
|
||||
entryName = entryName.replaceAll("\\\\", "/");
|
||||
return entryName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fathzer.soft.javaluator.DoubleEvaluator;
|
||||
|
||||
import io.github.pixee.security.HostValidator;
|
||||
import io.github.pixee.security.Urls;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Slf4j
|
||||
public class GeneralUtils {
|
||||
|
||||
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||
File tempFile = Files.createTempFile("temp", null).toFile();
|
||||
try (InputStream inputStream = multipartFile.getInputStream();
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
public static void deleteDirectory(Path path) throws IOException {
|
||||
Files.walkFileTree(
|
||||
path,
|
||||
new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
Files.deleteIfExists(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
|
||||
throws IOException {
|
||||
Files.deleteIfExists(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String convertToFileName(String name) {
|
||||
String safeName = name.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
if (safeName.length() > 50) {
|
||||
safeName = safeName.substring(0, 50);
|
||||
}
|
||||
return safeName;
|
||||
}
|
||||
|
||||
// Get resources from a location pattern
|
||||
public static Resource[] getResourcesFromLocationPattern(
|
||||
String locationPattern, ResourceLoader resourceLoader) throws Exception {
|
||||
// Normalize the path for file resources
|
||||
if (locationPattern.startsWith("file:")) {
|
||||
String rawPath = locationPattern.substring(5).replace("\\*", "").replace("/*", "");
|
||||
Path normalizePath = Paths.get(rawPath).normalize();
|
||||
locationPattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
||||
}
|
||||
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||
.getResources(locationPattern);
|
||||
}
|
||||
|
||||
public static boolean isValidURL(String urlStr) {
|
||||
try {
|
||||
Urls.create(
|
||||
urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS);
|
||||
return true;
|
||||
} catch (MalformedURLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isURLReachable(String urlStr) {
|
||||
try {
|
||||
// Parse the URL
|
||||
URL url = URI.create(urlStr).toURL();
|
||||
|
||||
// Allow only http and https protocols
|
||||
String protocol = url.getProtocol();
|
||||
if (!"http".equals(protocol) && !"https".equals(protocol)) {
|
||||
return false; // Disallow other protocols
|
||||
}
|
||||
|
||||
// Check if the host is a local address
|
||||
String host = url.getHost();
|
||||
if (isLocalAddress(host)) {
|
||||
return false; // Exclude local addresses
|
||||
}
|
||||
|
||||
// Check if the URL is reachable
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("HEAD");
|
||||
// connection.setConnectTimeout(5000); // Set connection timeout
|
||||
// connection.setReadTimeout(5000); // Set read timeout
|
||||
int responseCode = connection.getResponseCode();
|
||||
return (200 <= responseCode && responseCode <= 399);
|
||||
} catch (Exception e) {
|
||||
return false; // Return false in case of any exception
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLocalAddress(String host) {
|
||||
try {
|
||||
// Resolve DNS to IP address
|
||||
InetAddress address = InetAddress.getByName(host);
|
||||
|
||||
// Check for local addresses
|
||||
return address.isAnyLocalAddress()
|
||||
|| // Matches 0.0.0.0 or similar
|
||||
address.isLoopbackAddress()
|
||||
|| // Matches 127.0.0.1 or ::1
|
||||
address.isSiteLocalAddress()
|
||||
|| // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to
|
||||
// 172.31.x.x
|
||||
address.getHostAddress()
|
||||
.startsWith("fe80:"); // Matches link-local IPv6 addresses
|
||||
} catch (Exception e) {
|
||||
return false; // Return false for invalid or unresolved addresses
|
||||
}
|
||||
}
|
||||
|
||||
public static File multipartToFile(MultipartFile multipart) throws IOException {
|
||||
Path tempFile = Files.createTempFile("overlay-", ".pdf");
|
||||
try (InputStream in = multipart.getInputStream();
|
||||
FileOutputStream out = new FileOutputStream(tempFile.toFile())) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
return tempFile.toFile();
|
||||
}
|
||||
|
||||
public static Long convertSizeToBytes(String sizeStr) {
|
||||
if (sizeStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sizeStr = sizeStr.trim().toUpperCase();
|
||||
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
||||
try {
|
||||
if (sizeStr.endsWith("KB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024
|
||||
* 1024);
|
||||
} else if (sizeStr.endsWith("GB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024
|
||||
* 1024
|
||||
* 1024);
|
||||
} else if (sizeStr.endsWith("B")) {
|
||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
} else {
|
||||
// Assume MB if no unit is specified
|
||||
return (long) (Double.parseDouble(sizeStr) * 1024 * 1024);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// The numeric part of the input string cannot be parsed, handle this case
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) {
|
||||
if (pages == null) {
|
||||
return List.of(1); // Default to first page if input is null
|
||||
}
|
||||
try {
|
||||
return parsePageList(pages.split(","), totalPages, oneBased);
|
||||
} catch (NumberFormatException e) {
|
||||
return List.of(1); // Default to first page if input is invalid
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String[] pages, int totalPages) {
|
||||
return parsePageList(pages, totalPages, false);
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
int offset = oneBased ? 1 : 0;
|
||||
for (String page : pages) {
|
||||
if ("all".equalsIgnoreCase(page)) {
|
||||
|
||||
for (int i = 0; i < totalPages; i++) {
|
||||
result.add(i + offset);
|
||||
}
|
||||
} else if (page.contains(",")) {
|
||||
// Split the string into parts, could be single pages or ranges
|
||||
String[] parts = page.split(",");
|
||||
for (String part : parts) {
|
||||
result.addAll(handlePart(part, totalPages, offset));
|
||||
}
|
||||
} else {
|
||||
result.addAll(handlePart(page, totalPages, offset));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<Integer> evaluateNFunc(String expression, int maxValue) {
|
||||
List<Integer> results = new ArrayList<>();
|
||||
DoubleEvaluator evaluator = new DoubleEvaluator();
|
||||
|
||||
// Validate the expression
|
||||
if (!expression.matches("[0-9n+\\-*/() ]+")) {
|
||||
throw new IllegalArgumentException("Invalid expression");
|
||||
}
|
||||
|
||||
for (int n = 1; n <= maxValue; n++) {
|
||||
// Replace 'n' with the current value of n, correctly handling numbers before
|
||||
// 'n'
|
||||
String sanitizedExpression = sanitizeNFunction(expression, n);
|
||||
Double result = evaluator.evaluate(sanitizedExpression);
|
||||
|
||||
// Check if the result is null or not within bounds
|
||||
if (result == null) break;
|
||||
|
||||
if (result.intValue() > 0 && result.intValue() <= maxValue)
|
||||
results.add(result.intValue());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static String sanitizeNFunction(String expression, int nValue) {
|
||||
String sanitizedExpression = expression.replace(" ", "");
|
||||
String multiplyByOpeningRoundBracketPattern =
|
||||
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
|
||||
sanitizedExpression =
|
||||
sanitizedExpression.replaceAll(multiplyByOpeningRoundBracketPattern, "$1*(");
|
||||
|
||||
String multiplyByClosingRoundBracketPattern =
|
||||
"\\)([0-9n)])"; // example: (n-1)n, (n-1)9, (n-1)(n-2)
|
||||
sanitizedExpression =
|
||||
sanitizedExpression.replaceAll(multiplyByClosingRoundBracketPattern, ")*$1");
|
||||
|
||||
sanitizedExpression = insertMultiplicationBeforeN(sanitizedExpression, nValue);
|
||||
return sanitizedExpression;
|
||||
}
|
||||
|
||||
private static String insertMultiplicationBeforeN(String expression, int nValue) {
|
||||
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
||||
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
||||
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
||||
// Now replace 'n' with its current value
|
||||
return withMultiplication.replace("n", String.valueOf(nValue));
|
||||
}
|
||||
|
||||
private static String formatConsecutiveNsForNFunction(String expression) {
|
||||
String text = expression;
|
||||
while (text.matches(".*n{2,}.*")) {
|
||||
text = text.replaceAll("(?<!n)n{2}", "n*n");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static List<Integer> handlePart(String part, int totalPages, int offset) {
|
||||
List<Integer> partResult = new ArrayList<>();
|
||||
|
||||
// First check for n-syntax because it should not be processed as a range
|
||||
if (part.contains("n")) {
|
||||
partResult = evaluateNFunc(part, totalPages);
|
||||
// Adjust the results according to the offset
|
||||
for (int i = 0; i < partResult.size(); i++) {
|
||||
int adjustedValue = partResult.get(i) - 1 + offset;
|
||||
partResult.set(i, adjustedValue);
|
||||
}
|
||||
} else if (part.contains("-")) {
|
||||
// Process ranges only if it's not n-syntax
|
||||
String[] rangeParts = part.split("-");
|
||||
try {
|
||||
int start = Integer.parseInt(rangeParts[0]);
|
||||
int end =
|
||||
(rangeParts.length > 1 && !rangeParts[1].isEmpty())
|
||||
? Integer.parseInt(rangeParts[1])
|
||||
: totalPages;
|
||||
for (int i = start; i <= end; i++) {
|
||||
if (i >= 1 && i <= totalPages) {
|
||||
partResult.add(i - 1 + offset);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Range is invalid, ignore this part
|
||||
}
|
||||
} else {
|
||||
// This is a single page number
|
||||
try {
|
||||
int pageNum = Integer.parseInt(part.trim());
|
||||
if (pageNum >= 1 && pageNum <= totalPages) {
|
||||
partResult.add(pageNum - 1 + offset);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Ignore invalid numbers
|
||||
}
|
||||
}
|
||||
return partResult;
|
||||
}
|
||||
|
||||
public static boolean createDir(String path) {
|
||||
Path folder = Paths.get(path);
|
||||
if (!Files.exists(folder)) {
|
||||
try {
|
||||
Files.createDirectories(folder);
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isValidUUID(String uuid) {
|
||||
if (uuid == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
UUID.fromString(uuid);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------*
|
||||
* Internal Implementation Details *
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
public static void saveKeyToSettings(String key, Object newValue) throws IOException {
|
||||
String[] keyArray = key.split("\\.");
|
||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
||||
settingsYaml.updateValue(Arrays.asList(keyArray), newValue);
|
||||
settingsYaml.saveOverride(settingsPath);
|
||||
}
|
||||
|
||||
public static String generateMachineFingerprint() {
|
||||
try {
|
||||
// Get the MAC address
|
||||
StringBuilder sb = new StringBuilder();
|
||||
InetAddress ip = InetAddress.getLocalHost();
|
||||
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
||||
|
||||
if (network == null) {
|
||||
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
||||
while (networks.hasMoreElements()) {
|
||||
NetworkInterface net = networks.nextElement();
|
||||
byte[] mac = net.getHardwareAddress();
|
||||
if (mac != null) {
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X", mac[i]));
|
||||
}
|
||||
break; // Use the first network interface with a MAC address
|
||||
}
|
||||
}
|
||||
} else {
|
||||
byte[] mac = network.getHardwareAddress();
|
||||
if (mac != null) {
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X", mac[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the MAC address for privacy and consistency
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder fingerprint = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
fingerprint.append(String.format("%02x", b));
|
||||
}
|
||||
return fingerprint.toString();
|
||||
} catch (Exception e) {
|
||||
return "GenericID";
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
||||
if (currentVersion == null || compareVersion == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split versions into components
|
||||
String[] current = currentVersion.split("\\.");
|
||||
String[] compare = compareVersion.split("\\.");
|
||||
|
||||
// Get the length of the shorter version array
|
||||
int length = Math.min(current.length, compare.length);
|
||||
|
||||
// Compare each component
|
||||
for (int i = 0; i < length; i++) {
|
||||
int currentPart = Integer.parseInt(current[i]);
|
||||
int comparePart = Integer.parseInt(compare[i]);
|
||||
|
||||
if (currentPart > comparePart) {
|
||||
return true;
|
||||
}
|
||||
if (currentPart < comparePart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If all components so far are equal, the longer version is considered higher
|
||||
return current.length > compare.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.drew.imaging.ImageMetadataReader;
|
||||
import com.drew.imaging.ImageProcessingException;
|
||||
import com.drew.metadata.Metadata;
|
||||
import com.drew.metadata.MetadataException;
|
||||
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ImageProcessingUtils {
|
||||
|
||||
static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) {
|
||||
BufferedImage convertedImage;
|
||||
switch (colorType) {
|
||||
case "greyscale":
|
||||
convertedImage =
|
||||
new BufferedImage(
|
||||
sourceImage.getWidth(),
|
||||
sourceImage.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_GRAY);
|
||||
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
|
||||
break;
|
||||
case "blackwhite":
|
||||
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;
|
||||
}
|
||||
|
||||
public static byte[] getImageData(BufferedImage image) {
|
||||
DataBuffer dataBuffer = image.getRaster().getDataBuffer();
|
||||
if (dataBuffer instanceof DataBufferByte dataBufferByte) {
|
||||
return dataBufferByte.getData();
|
||||
} else if (dataBuffer instanceof DataBufferInt dataBufferInt) {
|
||||
int[] intData = dataBufferInt.getData();
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(intData.length * 4);
|
||||
byteBuffer.asIntBuffer().put(intData);
|
||||
return byteBuffer.array();
|
||||
} else {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
byte[] data = new byte[width * height * 3];
|
||||
int index = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int rgb = image.getRGB(x, y);
|
||||
data[index++] = (byte) ((rgb >> 16) & 0xFF); // Red
|
||||
data[index++] = (byte) ((rgb >> 8) & 0xFF); // Green
|
||||
data[index++] = (byte) (rgb & 0xFF); // Blue
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static double extractImageOrientation(InputStream is) throws IOException {
|
||||
try {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
ExifSubIFDDirectory directory =
|
||||
metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||
if (directory == null) {
|
||||
return 0;
|
||||
}
|
||||
int orientationTag = directory.getInt(ExifSubIFDDirectory.TAG_ORIENTATION);
|
||||
switch (orientationTag) {
|
||||
case 1:
|
||||
return 0;
|
||||
case 6:
|
||||
return 90;
|
||||
case 3:
|
||||
return 180;
|
||||
case 8:
|
||||
return 270;
|
||||
default:
|
||||
log.warn("Unknown orientation tag: {}", orientationTag);
|
||||
return 0;
|
||||
}
|
||||
} catch (ImageProcessingException | MetadataException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static BufferedImage applyOrientation(BufferedImage image, double orientation) {
|
||||
if (orientation == 0) {
|
||||
return image;
|
||||
}
|
||||
AffineTransform transform =
|
||||
AffineTransform.getRotateInstance(
|
||||
Math.toRadians(orientation),
|
||||
image.getWidth() / 2.0,
|
||||
image.getHeight() / 2.0);
|
||||
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
||||
return op.filter(image, null);
|
||||
}
|
||||
|
||||
public static BufferedImage loadImageWithExifOrientation(MultipartFile file)
|
||||
throws IOException {
|
||||
BufferedImage image = ImageIO.read(file.getInputStream());
|
||||
double orientation = extractImageOrientation(file.getInputStream());
|
||||
return applyOrientation(image, orientation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
|
||||
import com.vladsch.flexmark.util.data.MutableDataSet;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
@Slf4j
|
||||
@NoArgsConstructor
|
||||
public class PDFToFile {
|
||||
|
||||
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
MutableDataSet options =
|
||||
new MutableDataSet()
|
||||
.set(
|
||||
FlexmarkHtmlConverter.MAX_BLANK_LINES,
|
||||
2) // Control max consecutive blank lines
|
||||
.set(
|
||||
FlexmarkHtmlConverter.MAX_TRAILING_BLANK_LINES,
|
||||
1) // Control trailing blank lines
|
||||
.set(
|
||||
FlexmarkHtmlConverter.SETEXT_HEADINGS,
|
||||
true) // Use Setext headings for h1 and h2
|
||||
.set(
|
||||
FlexmarkHtmlConverter.OUTPUT_UNKNOWN_TAGS,
|
||||
false) // Don't output HTML for unknown tags
|
||||
.set(
|
||||
FlexmarkHtmlConverter.TYPOGRAPHIC_QUOTES,
|
||||
true) // Convert quotation marks
|
||||
.set(
|
||||
FlexmarkHtmlConverter.BR_AS_PARA_BREAKS,
|
||||
true) // Convert <br> to paragraph breaks
|
||||
.set(FlexmarkHtmlConverter.CODE_INDENT, " "); // Indent for code blocks
|
||||
|
||||
FlexmarkHtmlConverter htmlToMarkdownConverter =
|
||||
FlexmarkHtmlConverter.builder(options).build();
|
||||
|
||||
String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||
String pdfBaseName = originalPdfFileName;
|
||||
if (originalPdfFileName.contains(".")) {
|
||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
String fileName = "temp.file";
|
||||
|
||||
try {
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"pdftohtml",
|
||||
"-s",
|
||||
"-noframes",
|
||||
"-c",
|
||||
tempInputFile.toString(),
|
||||
pdfBaseName));
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
|
||||
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
||||
// Process HTML files to Markdown
|
||||
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
|
||||
List<File> markdownFiles = new ArrayList<>();
|
||||
|
||||
// Convert HTML files to Markdown
|
||||
for (File outputFile : outputFiles) {
|
||||
if (outputFile.getName().endsWith(".html")) {
|
||||
String html = Files.readString(outputFile.toPath());
|
||||
String markdown = htmlToMarkdownConverter.convert(html);
|
||||
|
||||
String mdFileName = outputFile.getName().replace(".html", ".md");
|
||||
File mdFile = new File(tempOutputDir.toFile(), mdFileName);
|
||||
Files.writeString(mdFile.toPath(), markdown);
|
||||
markdownFiles.add(mdFile);
|
||||
}
|
||||
}
|
||||
|
||||
// If there's only one markdown file, return it directly
|
||||
if (markdownFiles.size() == 1) {
|
||||
fileName = pdfBaseName + ".md";
|
||||
fileBytes = Files.readAllBytes(markdownFiles.get(0).toPath());
|
||||
} else {
|
||||
// Multiple files - create a zip
|
||||
fileName = pdfBaseName + "ToMarkdown.zip";
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
|
||||
// Add markdown files
|
||||
for (File mdFile : markdownFiles) {
|
||||
ZipEntry mdEntry = new ZipEntry(mdFile.getName());
|
||||
zipOutputStream.putNextEntry(mdEntry);
|
||||
Files.copy(mdFile.toPath(), zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
// Add images and other assets
|
||||
for (File file : outputFiles) {
|
||||
if (!file.getName().endsWith(".html") && !file.getName().endsWith(".md")) {
|
||||
ZipEntry assetEntry = new ZipEntry(file.getName());
|
||||
zipOutputStream.putNextEntry(assetEntry);
|
||||
Files.copy(file.toPath(), zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Get the original PDF file name without the extension
|
||||
String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||
String pdfBaseName = originalPdfFileName;
|
||||
if (originalPdfFileName.contains(".")) {
|
||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
String fileName = "temp.file";
|
||||
|
||||
try {
|
||||
// Save the uploaded file to a temporary location
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Prepare the output directory
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
// Run the pdftohtml command with complex output
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"pdftohtml", "-c", tempInputFile.toString(), pdfBaseName));
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
|
||||
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
||||
|
||||
// Get output files
|
||||
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
|
||||
|
||||
// Return output files in a ZIP archive
|
||||
fileName = pdfBaseName + "ToHtml.zip";
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
|
||||
for (File outputFile : outputFiles) {
|
||||
ZipEntry entry = new ZipEntry(outputFile.getName());
|
||||
zipOutputStream.putNextEntry(entry);
|
||||
try (FileInputStream fis = new FileInputStream(outputFile)) {
|
||||
IOUtils.copy(fis, zipOutputStream);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception writing zip entry", e);
|
||||
}
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Exception writing zip", e);
|
||||
}
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> processPdfToOfficeFormat(
|
||||
MultipartFile inputFile, String outputFormat, String libreOfficeFilter)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Get the original PDF file name without the extension
|
||||
String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||
|
||||
if (originalPdfFileName == null || "".equals(originalPdfFileName.trim())) {
|
||||
originalPdfFileName = "output.pdf";
|
||||
}
|
||||
// Assume file is pdf if no extension
|
||||
String pdfBaseName = originalPdfFileName;
|
||||
if (originalPdfFileName.contains(".")) {
|
||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||
}
|
||||
// Validate output format
|
||||
List<String> allowedFormats =
|
||||
Arrays.asList("doc", "docx", "odt", "ppt", "pptx", "odp", "rtf", "xml", "txt:Text");
|
||||
if (!allowedFormats.contains(outputFormat)) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
String fileName = "temp.file";
|
||||
|
||||
try {
|
||||
// Save the uploaded file to a temporary location
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Prepare the output directory
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
// Run the LibreOffice command
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"soffice",
|
||||
"--headless",
|
||||
"--nologo",
|
||||
"--infilter=" + libreOfficeFilter,
|
||||
"--convert-to",
|
||||
outputFormat,
|
||||
"--outdir",
|
||||
tempOutputDir.toString(),
|
||||
tempInputFile.toString()));
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Get output files
|
||||
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
|
||||
|
||||
if (outputFiles.size() == 1) {
|
||||
// Return single output file
|
||||
File outputFile = outputFiles.get(0);
|
||||
if ("txt:Text".equals(outputFormat)) {
|
||||
outputFormat = "txt";
|
||||
}
|
||||
fileName = pdfBaseName + "." + outputFormat;
|
||||
fileBytes = FileUtils.readFileToByteArray(outputFile);
|
||||
} else {
|
||||
// Return output files in a ZIP archive
|
||||
fileName = pdfBaseName + "To" + outputFormat + ".zip";
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
|
||||
for (File outputFile : outputFiles) {
|
||||
ZipEntry entry = new ZipEntry(outputFile.getName());
|
||||
zipOutputStream.putNextEntry(entry);
|
||||
try (FileInputStream fis = new FileInputStream(outputFile)) {
|
||||
IOUtils.copy(fis, zipOutputStream);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception writing zip entry", e);
|
||||
}
|
||||
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Exception writing zip", e);
|
||||
}
|
||||
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Slf4j
|
||||
public class PdfUtils {
|
||||
|
||||
public static 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;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid standard page size: " + size);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
||||
List<RenderedImage> images = new ArrayList<>();
|
||||
|
||||
for (COSName name : resources.getXObjectNames()) {
|
||||
PDXObject object = resources.getXObject(name);
|
||||
|
||||
if (object instanceof PDImageXObject) {
|
||||
images.add(((PDImageXObject) object).getImage());
|
||||
|
||||
} else if (object instanceof PDFormXObject) {
|
||||
images.addAll(getAllImages(((PDFormXObject) object).getResources()));
|
||||
}
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException {
|
||||
String[] pageOrderArr = pagesToCheck.split(",");
|
||||
List<Integer> pageList =
|
||||
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||
|
||||
for (int pageNumber : pageList) {
|
||||
PDPage page = document.getPage(pageNumber);
|
||||
if (hasImagesOnPage(page)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase)
|
||||
throws IOException {
|
||||
String[] pageOrderArr = pageNumbersToCheck.split(",");
|
||||
List<Integer> pageList =
|
||||
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||
|
||||
for (int pageNumber : pageList) {
|
||||
PDPage page = document.getPage(pageNumber);
|
||||
if (hasTextOnPage(page, phrase)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasImagesOnPage(PDPage page) throws IOException {
|
||||
return getAllImages(page.getResources()).size() > 0;
|
||||
}
|
||||
|
||||
public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
|
||||
PDFTextStripper textStripper = new PDFTextStripper();
|
||||
PDDocument tempDoc = new PDDocument();
|
||||
tempDoc.addPage(page);
|
||||
String pageText = textStripper.getText(tempDoc);
|
||||
tempDoc.close();
|
||||
return pageText.contains(phrase);
|
||||
}
|
||||
|
||||
public static byte[] convertFromPdf(
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
byte[] inputStream,
|
||||
String imageType,
|
||||
ImageType colorType,
|
||||
boolean singleImage,
|
||||
int DPI,
|
||||
String filename)
|
||||
throws IOException, Exception {
|
||||
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
// Create a ByteArrayOutputStream to save the image(s) to
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
if (singleImage) {
|
||||
if ("tiff".equals(imageType.toLowerCase())
|
||||
|| "tif".equals(imageType.toLowerCase())) {
|
||||
// Write the images to the output stream as a TIFF with multiple frames
|
||||
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
param.setCompressionType("ZLib");
|
||||
param.setCompressionQuality(1.0f);
|
||||
|
||||
try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) {
|
||||
writer.setOutput(ios);
|
||||
writer.prepareWriteSequence(null);
|
||||
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||
}
|
||||
|
||||
writer.endWriteSequence();
|
||||
}
|
||||
|
||||
writer.dispose();
|
||||
} else {
|
||||
// Combine all images into a single big image
|
||||
|
||||
// Calculate the combined image dimensions
|
||||
int maxWidth = 0;
|
||||
int totalHeight = 0;
|
||||
|
||||
BufferedImage pdfSizeImage = null;
|
||||
int pdfSizeImageIndex = -1;
|
||||
|
||||
// Using a map to store the rendered dimensions of each page size
|
||||
// to avoid rendering the same page sizes multiple times
|
||||
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
|
||||
new HashMap<>();
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
PDPage page = document.getPage(i);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
int rotation = page.getRotation();
|
||||
PdfRenderSettingsKey settings =
|
||||
new PdfRenderSettingsKey(
|
||||
mediaBox.getWidth(), mediaBox.getHeight(), rotation);
|
||||
PdfImageDimensionValue dimension = pageSizes.get(settings);
|
||||
if (dimension == null) {
|
||||
// Render the image to get the dimensions
|
||||
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
pdfSizeImageIndex = i;
|
||||
dimension =
|
||||
new PdfImageDimensionValue(
|
||||
pdfSizeImage.getWidth(), pdfSizeImage.getHeight());
|
||||
pageSizes.put(settings, dimension);
|
||||
if (pdfSizeImage.getWidth() > maxWidth) {
|
||||
maxWidth = pdfSizeImage.getWidth();
|
||||
}
|
||||
}
|
||||
totalHeight += dimension.height();
|
||||
}
|
||||
|
||||
// Create a new BufferedImage to store the combined images
|
||||
BufferedImage combined =
|
||||
prepareImageForPdfToImage(maxWidth, totalHeight, imageType);
|
||||
Graphics g = combined.getGraphics();
|
||||
|
||||
int currentHeight = 0;
|
||||
BufferedImage pageImage;
|
||||
|
||||
// Check if the first image is the last rendered image
|
||||
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
|
||||
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
if (firstImageAlreadyRendered && i == 0) {
|
||||
pageImage = pdfSizeImage;
|
||||
} else {
|
||||
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
}
|
||||
|
||||
// Calculate the x-coordinate to center the image
|
||||
int x = (maxWidth - pageImage.getWidth()) / 2;
|
||||
|
||||
g.drawImage(pageImage, x, currentHeight, null);
|
||||
currentHeight += pageImage.getHeight();
|
||||
}
|
||||
|
||||
// Write the image to the output stream
|
||||
ImageIO.write(combined, imageType, baos);
|
||||
}
|
||||
|
||||
// Log that the image was successfully written to the byte array
|
||||
log.info("Image successfully written to byte array");
|
||||
} else {
|
||||
// Zip the images and return as byte array
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, imageType, baosImage);
|
||||
|
||||
// Add the image to the zip file
|
||||
zos.putNextEntry(
|
||||
new ZipEntry(
|
||||
String.format(
|
||||
filename + "_%d.%s",
|
||||
i + 1,
|
||||
imageType.toLowerCase())));
|
||||
zos.write(baosImage.toByteArray());
|
||||
}
|
||||
}
|
||||
// Log that the images were successfully written to the byte array
|
||||
log.info("Images successfully written to byte array as a zip");
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
// Log an error message if there is an issue converting the PDF to an image
|
||||
log.error("Error converting PDF to image", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given Pdf file to PDF-Image.
|
||||
*
|
||||
* @param document to be converted. Note: the caller is responsible for closing the document
|
||||
* @return converted document to PDF-Image
|
||||
* @throws IOException if conversion fails
|
||||
*/
|
||||
public static PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
|
||||
PDDocument imageDocument = new PDDocument();
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||
PDPage originalPage = document.getPage(page);
|
||||
|
||||
float width = originalPage.getMediaBox().getWidth();
|
||||
float height = originalPage.getMediaBox().getHeight();
|
||||
|
||||
PDPage newPage = new PDPage(new PDRectangle(width, height));
|
||||
imageDocument.addPage(newPage);
|
||||
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
|
||||
contentStream.drawImage(pdImage, 0, 0, width, height);
|
||||
contentStream.close();
|
||||
}
|
||||
return imageDocument;
|
||||
}
|
||||
|
||||
private static BufferedImage prepareImageForPdfToImage(
|
||||
int maxWidth, int height, String imageType) {
|
||||
BufferedImage combined;
|
||||
if ("png".equalsIgnoreCase(imageType)) {
|
||||
combined = new BufferedImage(maxWidth, height, BufferedImage.TYPE_INT_ARGB);
|
||||
} else {
|
||||
combined = new BufferedImage(maxWidth, height, BufferedImage.TYPE_INT_RGB);
|
||||
}
|
||||
if (!"png".equalsIgnoreCase(imageType)) {
|
||||
Graphics g = combined.getGraphics();
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(0, 0, combined.getWidth(), combined.getHeight());
|
||||
g.dispose();
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static byte[] imageToPdf(
|
||||
MultipartFile[] files,
|
||||
String fitOption,
|
||||
boolean autoRotate,
|
||||
String colorType,
|
||||
CustomPDFDocumentFactory pdfDocumentFactory)
|
||||
throws IOException {
|
||||
try (PDDocument doc = pdfDocumentFactory.createNewDocument()) {
|
||||
for (MultipartFile file : files) {
|
||||
String contentType = file.getContentType();
|
||||
String originalFilename = Filenames.toSimpleFileName(file.getOriginalFilename());
|
||||
if (originalFilename != null
|
||||
&& (originalFilename.toLowerCase().endsWith(".tiff")
|
||||
|| originalFilename.toLowerCase().endsWith(".tif"))) {
|
||||
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
|
||||
reader.setInput(ImageIO.createImageInputStream(file.getInputStream()));
|
||||
int numPages = reader.getNumImages(true);
|
||||
for (int i = 0; i < numPages; i++) {
|
||||
BufferedImage pageImage = reader.read(i);
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(pageImage, colorType);
|
||||
PDImageXObject pdImage =
|
||||
LosslessFactory.createFromImage(doc, convertedImage);
|
||||
addImageToDocument(doc, pdImage, fitOption, autoRotate);
|
||||
}
|
||||
} else {
|
||||
BufferedImage image = ImageProcessingUtils.loadImageWithExifOrientation(file);
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(image, colorType);
|
||||
// Use JPEGFactory if it's JPEG since JPEG is lossy
|
||||
PDImageXObject pdImage =
|
||||
(contentType != null && "image/jpeg".equals(contentType))
|
||||
? JPEGFactory.createFromImage(doc, convertedImage)
|
||||
: LosslessFactory.createFromImage(doc, convertedImage);
|
||||
addImageToDocument(doc, pdImage, fitOption, autoRotate);
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
doc.save(byteArrayOutputStream);
|
||||
log.info("PDF successfully saved to byte array");
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static void addImageToDocument(
|
||||
PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate)
|
||||
throws IOException {
|
||||
boolean imageIsLandscape = image.getWidth() > image.getHeight();
|
||||
PDRectangle pageSize = PDRectangle.A4;
|
||||
|
||||
if (autoRotate && imageIsLandscape) {
|
||||
pageSize = new PDRectangle(pageSize.getHeight(), pageSize.getWidth());
|
||||
}
|
||||
|
||||
if ("fitDocumentToImage".equals(fitOption)) {
|
||||
pageSize = new PDRectangle(image.getWidth(), image.getHeight());
|
||||
}
|
||||
|
||||
PDPage page = new PDPage(pageSize);
|
||||
doc.addPage(page);
|
||||
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
float pageHeight = page.getMediaBox().getHeight();
|
||||
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(doc, page, AppendMode.APPEND, true, true)) {
|
||||
if ("fillPage".equals(fitOption) || "fitDocumentToImage".equals(fitOption)) {
|
||||
contentStream.drawImage(image, 0, 0, pageWidth, pageHeight);
|
||||
} else if ("maintainAspectRatio".equals(fitOption)) {
|
||||
float imageAspectRatio = (float) image.getWidth() / (float) image.getHeight();
|
||||
float pageAspectRatio = pageWidth / pageHeight;
|
||||
|
||||
float scaleFactor = 1.0f;
|
||||
if (imageAspectRatio > pageAspectRatio) {
|
||||
scaleFactor = pageWidth / image.getWidth();
|
||||
} else {
|
||||
scaleFactor = pageHeight / image.getHeight();
|
||||
}
|
||||
|
||||
float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2;
|
||||
float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2;
|
||||
contentStream.drawImage(
|
||||
image,
|
||||
xPos,
|
||||
yPos,
|
||||
image.getWidth() * scaleFactor,
|
||||
image.getHeight() * scaleFactor);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error adding image to PDF", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] overlayImage(
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
byte[] pdfBytes,
|
||||
byte[] imageBytes,
|
||||
float x,
|
||||
float y,
|
||||
boolean everyPage)
|
||||
throws IOException {
|
||||
|
||||
PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
||||
|
||||
// Get the first page of the PDF
|
||||
int pages = document.getNumberOfPages();
|
||||
for (int i = 0; i < pages; i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
// Create an image object from the image bytes
|
||||
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
|
||||
// Draw the image onto the page at the specified x and y coordinates
|
||||
contentStream.drawImage(image, x, y);
|
||||
log.info("Image successfully overlayed onto PDF");
|
||||
if (!everyPage && i == 0) {
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Log an error message if there is an issue overlaying the image onto the PDF
|
||||
log.error("Error overlaying image onto PDF", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Create a ByteArrayOutputStream to save the PDF to
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
log.info("PDF successfully saved to byte array");
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
|
||||
throws IOException {
|
||||
PDFTextStripper textStripper = new PDFTextStripper();
|
||||
String pdfText = "";
|
||||
|
||||
if (pagesToCheck == null || "all".equals(pagesToCheck)) {
|
||||
pdfText = textStripper.getText(pdfDocument);
|
||||
} else {
|
||||
// remove whitespaces
|
||||
pagesToCheck = pagesToCheck.replaceAll("\\s+", "");
|
||||
|
||||
String[] splitPoints = pagesToCheck.split(",");
|
||||
for (String splitPoint : splitPoints) {
|
||||
if (splitPoint.contains("-")) {
|
||||
// Handle page ranges
|
||||
String[] range = splitPoint.split("-");
|
||||
int startPage = Integer.parseInt(range[0]);
|
||||
int endPage = Integer.parseInt(range[1]);
|
||||
|
||||
for (int i = startPage; i <= endPage; i++) {
|
||||
textStripper.setStartPage(i);
|
||||
textStripper.setEndPage(i);
|
||||
pdfText += textStripper.getText(pdfDocument);
|
||||
}
|
||||
} else {
|
||||
// Handle individual page
|
||||
int page = Integer.parseInt(splitPoint);
|
||||
textStripper.setStartPage(page);
|
||||
textStripper.setEndPage(page);
|
||||
pdfText += textStripper.getText(pdfDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pdfDocument.close();
|
||||
|
||||
return pdfText.contains(text);
|
||||
}
|
||||
|
||||
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator)
|
||||
throws IOException {
|
||||
int actualPageCount = pdfDocument.getNumberOfPages();
|
||||
pdfDocument.close();
|
||||
|
||||
switch (comparator.toLowerCase()) {
|
||||
case "greater":
|
||||
return actualPageCount > pageCount;
|
||||
case "equal":
|
||||
return actualPageCount == pageCount;
|
||||
case "less":
|
||||
return actualPageCount < pageCount;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
|
||||
PDPage firstPage = pdfDocument.getPage(0);
|
||||
PDRectangle mediaBox = firstPage.getMediaBox();
|
||||
|
||||
float actualPageWidth = mediaBox.getWidth();
|
||||
float actualPageHeight = mediaBox.getHeight();
|
||||
|
||||
pdfDocument.close();
|
||||
|
||||
// Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842"
|
||||
// for A4
|
||||
String[] dimensions = expectedPageSize.split("x");
|
||||
float expectedPageWidth = Float.parseFloat(dimensions[0]);
|
||||
float expectedPageHeight = Float.parseFloat(dimensions[1]);
|
||||
|
||||
// Checks if the actual page size matches the expected page size
|
||||
return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight;
|
||||
}
|
||||
|
||||
/** Key for storing the dimensions of a rendered image in a map. */
|
||||
private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {}
|
||||
|
||||
/** Value for storing the dimensions of a rendered image in a map. */
|
||||
private record PdfImageDimensionValue(int width, int height) {}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Slf4j
|
||||
public class ProcessExecutor {
|
||||
|
||||
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
|
||||
private static ApplicationProperties applicationProperties = new ApplicationProperties();
|
||||
private final Semaphore semaphore;
|
||||
private final boolean liveUpdates;
|
||||
private long timeoutDuration;
|
||||
|
||||
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
|
||||
this.semaphore = new Semaphore(semaphoreLimit);
|
||||
this.liveUpdates = liveUpdates;
|
||||
this.timeoutDuration = timeout;
|
||||
}
|
||||
|
||||
public static ProcessExecutor getInstance(Processes processType) {
|
||||
return getInstance(processType, true);
|
||||
}
|
||||
|
||||
public static ProcessExecutor getInstance(Processes processType, boolean liveUpdates) {
|
||||
return instances.computeIfAbsent(
|
||||
processType,
|
||||
key -> {
|
||||
int semaphoreLimit =
|
||||
switch (key) {
|
||||
case LIBRE_OFFICE ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getLibreOfficeSessionLimit();
|
||||
case PDFTOHTML ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getPdfToHtmlSessionLimit();
|
||||
case PYTHON_OPENCV ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getPythonOpenCvSessionLimit();
|
||||
case WEASYPRINT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getWeasyPrintSessionLimit();
|
||||
case INSTALL_APP ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getInstallAppSessionLimit();
|
||||
case TESSERACT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getTesseractSessionLimit();
|
||||
case QPDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getQpdfSessionLimit();
|
||||
case CALIBRE ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getCalibreSessionLimit();
|
||||
};
|
||||
|
||||
long timeoutMinutes =
|
||||
switch (key) {
|
||||
case LIBRE_OFFICE ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getLibreOfficeTimeoutMinutes();
|
||||
case PDFTOHTML ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getPdfToHtmlTimeoutMinutes();
|
||||
case PYTHON_OPENCV ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getPythonOpenCvTimeoutMinutes();
|
||||
case WEASYPRINT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getWeasyPrintTimeoutMinutes();
|
||||
case INSTALL_APP ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getInstallAppTimeoutMinutes();
|
||||
case TESSERACT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getTesseractTimeoutMinutes();
|
||||
case QPDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getQpdfTimeoutMinutes();
|
||||
case CALIBRE ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getCalibreTimeoutMinutes();
|
||||
};
|
||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
});
|
||||
}
|
||||
|
||||
public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
|
||||
throws IOException, InterruptedException {
|
||||
return runCommandWithOutputHandling(command, null);
|
||||
}
|
||||
|
||||
public ProcessExecutorResult runCommandWithOutputHandling(
|
||||
List<String> command, File workingDirectory) throws IOException, InterruptedException {
|
||||
String messages = "";
|
||||
int exitCode = 1;
|
||||
semaphore.acquire();
|
||||
try {
|
||||
|
||||
log.info("Running command: " + String.join(" ", command));
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
|
||||
// Use the working directory if it's set
|
||||
if (workingDirectory != null) {
|
||||
processBuilder.directory(workingDirectory);
|
||||
}
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// Read the error stream and standard output stream concurrently
|
||||
List<String> errorLines = new ArrayList<>();
|
||||
List<String> outputLines = new ArrayList<>();
|
||||
|
||||
Thread errorReaderThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try (BufferedReader errorReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
errorReader, 5_000_000))
|
||||
!= null) {
|
||||
errorLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn("Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
|
||||
Thread outputReaderThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try (BufferedReader outputReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
outputReader, 5_000_000))
|
||||
!= null) {
|
||||
outputLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn("Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
|
||||
errorReaderThread.start();
|
||||
outputReaderThread.start();
|
||||
|
||||
// Wait for the conversion process to complete
|
||||
boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES);
|
||||
|
||||
if (!finished) {
|
||||
// Terminate the process
|
||||
process.destroy();
|
||||
// Interrupt the reader threads
|
||||
errorReaderThread.interrupt();
|
||||
outputReaderThread.interrupt();
|
||||
throw new IOException("Process timeout exceeded.");
|
||||
}
|
||||
exitCode = process.exitValue();
|
||||
// Wait for the reader threads to finish
|
||||
errorReaderThread.join();
|
||||
outputReaderThread.join();
|
||||
|
||||
boolean isQpdf =
|
||||
command != null && !command.isEmpty() && command.get(0).contains("qpdf");
|
||||
|
||||
if (!outputLines.isEmpty()) {
|
||||
String outputMessage = String.join("\n", outputLines);
|
||||
messages += outputMessage;
|
||||
if (!liveUpdates) {
|
||||
log.info("Command output:\n" + outputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorLines.isEmpty()) {
|
||||
String errorMessage = String.join("\n", errorLines);
|
||||
messages += errorMessage;
|
||||
if (!liveUpdates) {
|
||||
log.warn("Command error output:\n" + errorMessage);
|
||||
}
|
||||
if (exitCode != 0) {
|
||||
if (isQpdf && exitCode == 3) {
|
||||
log.warn("qpdf succeeded with warnings: {}", messages);
|
||||
} else {
|
||||
throw new IOException(
|
||||
"Command process failed with exit code "
|
||||
+ exitCode
|
||||
+ ". Error message: "
|
||||
+ errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exitCode != 0) {
|
||||
if (isQpdf && exitCode == 3) {
|
||||
log.warn("qpdf succeeded with warnings: {}", messages);
|
||||
} else {
|
||||
throw new IOException(
|
||||
"Command process failed with exit code "
|
||||
+ exitCode
|
||||
+ "\nLogs: "
|
||||
+ messages);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
semaphore.release();
|
||||
}
|
||||
return new ProcessExecutorResult(exitCode, messages);
|
||||
}
|
||||
|
||||
public enum Processes {
|
||||
LIBRE_OFFICE,
|
||||
PDFTOHTML,
|
||||
PYTHON_OPENCV,
|
||||
WEASYPRINT,
|
||||
INSTALL_APP,
|
||||
CALIBRE,
|
||||
TESSERACT,
|
||||
QPDF
|
||||
}
|
||||
|
||||
public class ProcessExecutorResult {
|
||||
int rc;
|
||||
String messages;
|
||||
|
||||
public ProcessExecutorResult(int rc, String messages) {
|
||||
this.rc = rc;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public int getRc() {
|
||||
return rc;
|
||||
}
|
||||
|
||||
public void setRc(int rc) {
|
||||
this.rc = rc;
|
||||
}
|
||||
|
||||
public String getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(String messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PropertyConfigs {
|
||||
|
||||
public static boolean getBooleanValue(List<String> keys, boolean defaultValue) {
|
||||
for (String key : keys) {
|
||||
String value = System.getProperty(key);
|
||||
if (value == null) value = System.getenv(key);
|
||||
|
||||
if (value != null) return Boolean.valueOf(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static String getStringValue(List<String> keys, String defaultValue) {
|
||||
for (String key : keys) {
|
||||
String value = System.getProperty(key);
|
||||
if (value == null) value = System.getenv(key);
|
||||
|
||||
if (value != null) return value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static boolean getBooleanValue(String key, boolean defaultValue) {
|
||||
String value = System.getProperty(key);
|
||||
if (value == null) value = System.getenv(key);
|
||||
return (value != null) ? Boolean.valueOf(value) : defaultValue;
|
||||
}
|
||||
|
||||
public static String getStringValue(String key, String defaultValue) {
|
||||
String value = System.getProperty(key);
|
||||
if (value == null) value = System.getenv(key);
|
||||
return (value != null) ? value : defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import static stirling.software.common.util.ValidationUtils.isCollectionEmpty;
|
||||
import static stirling.software.common.util.ValidationUtils.isStringEmpty;
|
||||
|
||||
public class ProviderUtils {
|
||||
|
||||
public static boolean validateProvider(Provider provider) {
|
||||
if (provider == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isStringEmpty(provider.getClientId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isStringEmpty(provider.getClientSecret())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCollectionEmpty(provider.getScopes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
public class RequestUriUtils {
|
||||
|
||||
public static boolean isStaticResource(String requestURI) {
|
||||
return isStaticResource("", requestURI);
|
||||
}
|
||||
|
||||
public static boolean isStaticResource(String contextPath, String requestURI) {
|
||||
return requestURI.startsWith(contextPath + "/css/")
|
||||
|| requestURI.startsWith(contextPath + "/fonts/")
|
||||
|| requestURI.startsWith(contextPath + "/js/")
|
||||
|| requestURI.endsWith(contextPath + "robots.txt")
|
||||
|| requestURI.startsWith(contextPath + "/images/")
|
||||
|| requestURI.startsWith(contextPath + "/public/")
|
||||
|| requestURI.startsWith(contextPath + "/pdfjs/")
|
||||
|| requestURI.startsWith(contextPath + "/login")
|
||||
|| requestURI.startsWith(contextPath + "/error")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|| requestURI.endsWith(".webmanifest")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/info/status");
|
||||
}
|
||||
|
||||
public static boolean isTrackableResource(String requestURI) {
|
||||
return isTrackableResource("", requestURI);
|
||||
}
|
||||
|
||||
public static boolean isTrackableResource(String contextPath, String requestURI) {
|
||||
return !(requestURI.startsWith("/js")
|
||||
|| requestURI.startsWith("/v1/api-docs")
|
||||
|| requestURI.endsWith("robots.txt")
|
||||
|| requestURI.startsWith("/images")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|| requestURI.endsWith(".css")
|
||||
|| requestURI.endsWith(".map")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith("popularity.txt")
|
||||
|| requestURI.endsWith(".js")
|
||||
|| requestURI.contains("swagger")
|
||||
|| requestURI.startsWith("/api/v1/info")
|
||||
|| requestURI.startsWith("/site.webmanifest")
|
||||
|| requestURI.startsWith("/fonts")
|
||||
|| requestURI.startsWith("/pdfjs"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public class UIScaling {
|
||||
private static final double BASE_RESOLUTION_WIDTH = 1920.0;
|
||||
private static final double BASE_RESOLUTION_HEIGHT = 1080.0;
|
||||
|
||||
public static double getWidthScaleFactor() {
|
||||
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
|
||||
return screenSize.getWidth() / BASE_RESOLUTION_WIDTH;
|
||||
}
|
||||
|
||||
public static double getHeightScaleFactor() {
|
||||
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
|
||||
return screenSize.getHeight() / BASE_RESOLUTION_HEIGHT;
|
||||
}
|
||||
|
||||
public static int scaleWidth(int value) {
|
||||
return (int) Math.round(value * getWidthScaleFactor());
|
||||
}
|
||||
|
||||
public static int scaleHeight(int value) {
|
||||
return (int) Math.round(value * getHeightScaleFactor());
|
||||
}
|
||||
|
||||
public static Dimension scale(Dimension dim) {
|
||||
return new Dimension(scaleWidth(dim.width), scaleHeight(dim.height));
|
||||
}
|
||||
|
||||
public static Insets scale(Insets insets) {
|
||||
return new Insets(
|
||||
scaleHeight(insets.top),
|
||||
scaleWidth(insets.left),
|
||||
scaleHeight(insets.bottom),
|
||||
scaleWidth(insets.right));
|
||||
}
|
||||
|
||||
public static Font scaleFont(Font font) {
|
||||
// For fonts, we'll use the smaller scale factor to ensure readability
|
||||
double scaleFactor = Math.min(getWidthScaleFactor(), getHeightScaleFactor());
|
||||
return font.deriveFont((float) (font.getSize() * scaleFactor));
|
||||
}
|
||||
|
||||
// Utility method for aspect ratio aware icon scaling
|
||||
public static Image scaleIcon(Image icon, int targetWidth, int targetHeight) {
|
||||
if (icon == null) return null;
|
||||
|
||||
double widthScale = getWidthScaleFactor();
|
||||
double heightScale = getHeightScaleFactor();
|
||||
|
||||
int scaledWidth = (int) Math.round(targetWidth * widthScale);
|
||||
int scaledHeight = (int) Math.round(targetHeight * heightScale);
|
||||
|
||||
// Maintain aspect ratio for icons
|
||||
double aspectRatio = (double) icon.getWidth(null) / icon.getHeight(null);
|
||||
if (scaledWidth / scaledHeight > aspectRatio) {
|
||||
scaledWidth = (int) (scaledHeight * aspectRatio);
|
||||
} else {
|
||||
scaledHeight = (int) (scaledWidth / aspectRatio);
|
||||
}
|
||||
|
||||
return icon.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public class UrlUtils {
|
||||
|
||||
public static String getOrigin(HttpServletRequest request) {
|
||||
String scheme = request.getScheme(); // http or https
|
||||
String serverName = request.getServerName(); // localhost
|
||||
int serverPort = request.getServerPort(); // 8080
|
||||
String contextPath = request.getContextPath(); // /myapp
|
||||
|
||||
return scheme + "://" + serverName + ":" + serverPort + contextPath;
|
||||
}
|
||||
|
||||
public static boolean isPortAvailable(int port) {
|
||||
try (ServerSocket socket = new ServerSocket(port)) {
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static String findAvailablePort(int startPort) {
|
||||
int port = startPort;
|
||||
while (!isPortAvailable(port)) {
|
||||
port++;
|
||||
}
|
||||
return String.valueOf(port);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class ValidationUtils {
|
||||
|
||||
public static boolean isStringEmpty(String input) {
|
||||
return input == null || input.isBlank();
|
||||
}
|
||||
|
||||
public static boolean isCollectionEmpty(Collection<String> input) {
|
||||
return input == null || input.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
|
||||
public class WebResponseUtils {
|
||||
|
||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
||||
ByteArrayOutputStream baos, String docName) throws IOException {
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
||||
ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> multiPartFileToWebResponse(MultipartFile file)
|
||||
throws IOException {
|
||||
String fileName = Filenames.toSimpleFileName(file.getOriginalFilename());
|
||||
MediaType mediaType = MediaType.parseMediaType(file.getContentType());
|
||||
|
||||
byte[] bytes = file.getBytes();
|
||||
|
||||
return bytesToWebResponse(bytes, fileName, mediaType);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> bytesToWebResponse(
|
||||
byte[] bytes, String docName, MediaType mediaType) throws IOException {
|
||||
|
||||
// Return the PDF as a response
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(mediaType);
|
||||
headers.setContentLength(bytes.length);
|
||||
String encodedDocName =
|
||||
URLEncoder.encode(docName, StandardCharsets.UTF_8.toString())
|
||||
.replaceAll("\\+", "%20");
|
||||
headers.setContentDispositionFormData("attachment", encodedDocName);
|
||||
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName)
|
||||
throws IOException {
|
||||
return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> pdfDocToWebResponse(PDDocument document, String docName)
|
||||
throws IOException {
|
||||
|
||||
// Open Byte Array and save document to it
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
// Close the document
|
||||
document.close();
|
||||
|
||||
return boasToWebResponse(baos, docName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.snakeyaml.engine.v2.api.Dump;
|
||||
import org.snakeyaml.engine.v2.api.DumpSettings;
|
||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||
import org.snakeyaml.engine.v2.api.StreamDataWriter;
|
||||
import org.snakeyaml.engine.v2.common.FlowStyle;
|
||||
import org.snakeyaml.engine.v2.common.ScalarStyle;
|
||||
import org.snakeyaml.engine.v2.composer.Composer;
|
||||
import org.snakeyaml.engine.v2.nodes.MappingNode;
|
||||
import org.snakeyaml.engine.v2.nodes.Node;
|
||||
import org.snakeyaml.engine.v2.nodes.NodeTuple;
|
||||
import org.snakeyaml.engine.v2.nodes.ScalarNode;
|
||||
import org.snakeyaml.engine.v2.nodes.SequenceNode;
|
||||
import org.snakeyaml.engine.v2.nodes.Tag;
|
||||
import org.snakeyaml.engine.v2.parser.ParserImpl;
|
||||
import org.snakeyaml.engine.v2.scanner.StreamReader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class YamlHelper {
|
||||
|
||||
// YAML dump settings with comment support and block flow style
|
||||
private static final DumpSettings DUMP_SETTINGS =
|
||||
DumpSettings.builder()
|
||||
.setDumpComments(true)
|
||||
.setWidth(Integer.MAX_VALUE)
|
||||
.setDefaultFlowStyle(FlowStyle.BLOCK)
|
||||
.build();
|
||||
|
||||
private final String yamlContent; // Stores the entire YAML content as a string
|
||||
|
||||
private LoadSettings loadSettings =
|
||||
LoadSettings.builder()
|
||||
.setUseMarks(true)
|
||||
.setMaxAliasesForCollections(Integer.MAX_VALUE)
|
||||
.setAllowRecursiveKeys(true)
|
||||
.setParseComments(true)
|
||||
.build();
|
||||
|
||||
private Path originalFilePath;
|
||||
private Node updatedRootNode;
|
||||
|
||||
// Constructor with custom LoadSettings and YAML string
|
||||
public YamlHelper(LoadSettings loadSettings, String yamlContent) {
|
||||
this.loadSettings = loadSettings;
|
||||
this.yamlContent = yamlContent;
|
||||
}
|
||||
|
||||
// Constructor that reads YAML from a file path
|
||||
public YamlHelper(Path originalFilePath) throws IOException {
|
||||
this.yamlContent = Files.readString(originalFilePath);
|
||||
this.originalFilePath = originalFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates values in the target YAML based on values from the source YAML. It ensures that only
|
||||
* existing keys in the target YAML are updated.
|
||||
*
|
||||
* @return true if at least one key was updated, false otherwise.
|
||||
*/
|
||||
public boolean updateValuesFromYaml(YamlHelper sourceYaml, YamlHelper targetYaml) {
|
||||
boolean updated = false;
|
||||
Set<String> sourceKeys = sourceYaml.getAllKeys();
|
||||
Set<String> targetKeys = targetYaml.getAllKeys();
|
||||
|
||||
for (String key : sourceKeys) {
|
||||
String[] keyArray = key.split("\\.");
|
||||
|
||||
Object newValue = sourceYaml.getValueByExactKeyPath(keyArray);
|
||||
Object currentValue = targetYaml.getValueByExactKeyPath(keyArray);
|
||||
if (newValue != null
|
||||
&& (!newValue.equals(currentValue) || !sourceKeys.equals(targetKeys))) {
|
||||
boolean updatedKey = targetYaml.updateValue(Arrays.asList(keyArray), newValue);
|
||||
if (updatedKey) updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a value in the YAML structure.
|
||||
*
|
||||
* @param keys The hierarchical keys leading to the value.
|
||||
* @param newValue The new value to set.
|
||||
* @return true if the value was updated, false otherwise.
|
||||
*/
|
||||
public boolean updateValue(List<String> keys, Object newValue) {
|
||||
return updateValue(getRootNode(), keys, newValue);
|
||||
}
|
||||
|
||||
private boolean updateValue(Node node, List<String> keys, Object newValue) {
|
||||
if (!(node instanceof MappingNode mappingNode)) return false;
|
||||
|
||||
List<NodeTuple> updatedTuples = new ArrayList<>();
|
||||
boolean updated = false;
|
||||
|
||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||
ScalarNode keyNode = (tuple.getKeyNode() instanceof ScalarNode sk) ? sk : null;
|
||||
if (keyNode == null || !keyNode.getValue().equals(keys.get(0))) {
|
||||
updatedTuples.add(tuple);
|
||||
continue;
|
||||
}
|
||||
|
||||
Node valueNode = tuple.getValueNode();
|
||||
|
||||
if (keys.size() == 1) {
|
||||
Tag tag = valueNode.getTag();
|
||||
Node newValueNode = null;
|
||||
|
||||
if (isAnyInteger(newValue)) {
|
||||
newValueNode =
|
||||
new ScalarNode(Tag.INT, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else if (isFloat(newValue)) {
|
||||
Object floatValue = Float.valueOf(String.valueOf(newValue));
|
||||
newValueNode =
|
||||
new ScalarNode(
|
||||
Tag.FLOAT, String.valueOf(floatValue), ScalarStyle.PLAIN);
|
||||
} else if ("true".equals(newValue) || "false".equals(newValue)) {
|
||||
newValueNode =
|
||||
new ScalarNode(Tag.BOOL, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else if (newValue instanceof List<?> list) {
|
||||
List<Node> sequenceNodes = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
Object obj = String.valueOf(item);
|
||||
if (isAnyInteger(item)) {
|
||||
tag = Tag.INT;
|
||||
} else if (isFloat(item)) {
|
||||
obj = Float.valueOf(String.valueOf(item));
|
||||
tag = Tag.FLOAT;
|
||||
} else if ("true".equals(item) || "false".equals(item)) {
|
||||
tag = Tag.BOOL;
|
||||
} else if (item == null || "null".equals(item)) {
|
||||
tag = Tag.NULL;
|
||||
} else {
|
||||
tag = Tag.STR;
|
||||
}
|
||||
sequenceNodes.add(
|
||||
new ScalarNode(tag, String.valueOf(obj), ScalarStyle.PLAIN));
|
||||
}
|
||||
newValueNode = new SequenceNode(Tag.SEQ, sequenceNodes, FlowStyle.FLOW);
|
||||
} else if (tag == Tag.NULL) {
|
||||
if ("true".equals(newValue)
|
||||
|| "false".equals(newValue)
|
||||
|| newValue instanceof Boolean) {
|
||||
tag = Tag.BOOL;
|
||||
}
|
||||
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else {
|
||||
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
}
|
||||
copyComments(valueNode, newValueNode);
|
||||
|
||||
updatedTuples.add(new NodeTuple(keyNode, newValueNode));
|
||||
updated = true;
|
||||
} else if (valueNode instanceof MappingNode) {
|
||||
updated = updateValue(valueNode, keys.subList(1, keys.size()), newValue);
|
||||
updatedTuples.add(tuple);
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
mappingNode.getValue().clear();
|
||||
mappingNode.getValue().addAll(updatedTuples);
|
||||
}
|
||||
setNewNode(node);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a value based on an exact key path.
|
||||
*
|
||||
* @param keys The key hierarchy leading to the value.
|
||||
* @return The value if found, otherwise null.
|
||||
*/
|
||||
public Object getValueByExactKeyPath(String... keys) {
|
||||
return getValueByExactKeyPath(getRootNode(), new ArrayDeque<>(List.of(keys)));
|
||||
}
|
||||
|
||||
private Object getValueByExactKeyPath(Node node, Deque<String> keyQueue) {
|
||||
if (!(node instanceof MappingNode mappingNode)) return null;
|
||||
|
||||
String currentKey = keyQueue.poll();
|
||||
if (currentKey == null) return null;
|
||||
|
||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||
if (tuple.getKeyNode() instanceof ScalarNode keyNode
|
||||
&& keyNode.getValue().equals(currentKey)) {
|
||||
if (keyQueue.isEmpty()) {
|
||||
Node valueNode = tuple.getValueNode();
|
||||
|
||||
if (valueNode instanceof ScalarNode scalarValueNode) {
|
||||
return scalarValueNode.getValue();
|
||||
} else if (valueNode instanceof MappingNode subMapping) {
|
||||
return getValueByExactKeyPath(subMapping, keyQueue);
|
||||
} else if (valueNode instanceof SequenceNode sequenceNode) {
|
||||
List<Object> valuesList = new ArrayList<>();
|
||||
for (Node o : sequenceNode.getValue()) {
|
||||
if (o instanceof ScalarNode scalarValue) {
|
||||
valuesList.add(scalarValue.getValue());
|
||||
}
|
||||
}
|
||||
return valuesList;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return getValueByExactKeyPath(tuple.getValueNode(), keyQueue);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Set<String> cachedKeys;
|
||||
|
||||
/**
|
||||
* Retrieves the set of all keys present in the YAML structure. Keys are returned as
|
||||
* dot-separated paths for nested keys.
|
||||
*
|
||||
* @return A set containing all keys in dot notation.
|
||||
*/
|
||||
public Set<String> getAllKeys() {
|
||||
if (cachedKeys == null) {
|
||||
cachedKeys = getAllKeys(getRootNode());
|
||||
}
|
||||
return cachedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all keys from the YAML node recursively.
|
||||
*
|
||||
* @param node The current YAML node.
|
||||
* @param currentPath The accumulated path of keys.
|
||||
* @param allKeys The set storing all collected keys.
|
||||
*/
|
||||
private Set<String> getAllKeys(Node node) {
|
||||
Set<String> allKeys = new LinkedHashSet<>();
|
||||
collectKeys(node, "", allKeys);
|
||||
return allKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses the YAML structure to collect all keys.
|
||||
*
|
||||
* @param node The current node in the YAML structure.
|
||||
* @param currentPath The accumulated key path.
|
||||
* @param allKeys The set storing collected keys.
|
||||
*/
|
||||
private void collectKeys(Node node, String currentPath, Set<String> allKeys) {
|
||||
if (node instanceof MappingNode mappingNode) {
|
||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||
if (tuple.getKeyNode() instanceof ScalarNode keyNode) {
|
||||
String newPath =
|
||||
currentPath.isEmpty()
|
||||
? keyNode.getValue()
|
||||
: currentPath + "." + keyNode.getValue();
|
||||
allKeys.add(newPath);
|
||||
collectKeys(tuple.getValueNode(), newPath, allKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the root node of the YAML document. If a new node was previously set, it is
|
||||
* returned instead.
|
||||
*
|
||||
* @return The root node of the YAML structure.
|
||||
*/
|
||||
private Node getRootNode() {
|
||||
if (this.updatedRootNode != null) {
|
||||
return this.updatedRootNode;
|
||||
}
|
||||
Composer composer = new Composer(loadSettings, getParserImpl());
|
||||
Optional<Node> rootNodeOpt = composer.getSingleNode();
|
||||
if (rootNodeOpt.isPresent()) {
|
||||
return rootNodeOpt.get();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new root node, allowing modifications to be tracked.
|
||||
*
|
||||
* @param newRootNode The modified root node.
|
||||
*/
|
||||
public void setNewNode(Node newRootNode) {
|
||||
this.updatedRootNode = newRootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current root node (either the original or the updated one).
|
||||
*
|
||||
* @return The root node.
|
||||
*/
|
||||
public Node getUpdatedRootNode() {
|
||||
if (this.updatedRootNode == null) {
|
||||
this.updatedRootNode = getRootNode();
|
||||
}
|
||||
return this.updatedRootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the YAML parser.
|
||||
*
|
||||
* @return The configured parser.
|
||||
*/
|
||||
private ParserImpl getParserImpl() {
|
||||
return new ParserImpl(loadSettings, getStreamReader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stream reader for the YAML content.
|
||||
*
|
||||
* @return The configured stream reader.
|
||||
*/
|
||||
private StreamReader getStreamReader() {
|
||||
return new StreamReader(loadSettings, yamlContent);
|
||||
}
|
||||
|
||||
public MappingNode save(Path saveFilePath) throws IOException {
|
||||
if (!saveFilePath.equals(originalFilePath)) {
|
||||
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
||||
}
|
||||
return (MappingNode) getUpdatedRootNode();
|
||||
}
|
||||
|
||||
public void saveOverride(Path saveFilePath) throws IOException {
|
||||
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a YAML node back to a YAML-formatted string.
|
||||
*
|
||||
* @param rootNode The root node to be converted.
|
||||
* @return A YAML-formatted string.
|
||||
*/
|
||||
public String convertNodeToYaml(Node rootNode) {
|
||||
StringWriter writer = new StringWriter();
|
||||
StreamDataWriter streamDataWriter =
|
||||
new StreamDataWriter() {
|
||||
@Override
|
||||
public void write(String str) {
|
||||
writer.write(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String str, int off, int len) {
|
||||
writer.write(str, off, len);
|
||||
}
|
||||
};
|
||||
|
||||
new Dump(DUMP_SETTINGS).dumpNode(rootNode, streamDataWriter);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private static boolean isParsable(String value, Function<String, ?> parser) {
|
||||
try {
|
||||
parser.apply(value);
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is an integer.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents an integer, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isInteger(Object object) {
|
||||
if (object instanceof Integer
|
||||
|| object instanceof Short
|
||||
|| object instanceof Byte
|
||||
|| object instanceof Long) {
|
||||
return true;
|
||||
}
|
||||
if (object instanceof String str) {
|
||||
return isParsable(str, Integer::parseInt);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a floating-point number.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a float, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isFloat(Object object) {
|
||||
return (object instanceof Float || object instanceof Double)
|
||||
|| (object instanceof String str && isParsable(str, Float::parseFloat));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a short integer.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a short integer, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isShort(Object object) {
|
||||
return (object instanceof Long)
|
||||
|| (object instanceof String str && isParsable(str, Short::parseShort));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a byte.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a byte, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isByte(Object object) {
|
||||
return (object instanceof Long)
|
||||
|| (object instanceof String str && isParsable(str, Byte::parseByte));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a long integer.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a long integer, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isLong(Object object) {
|
||||
return (object instanceof Long)
|
||||
|| (object instanceof String str && isParsable(str, Long::parseLong));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an object is any type of integer (short, byte, long, or int).
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents an integer type, false otherwise.
|
||||
*/
|
||||
public static boolean isAnyInteger(Object object) {
|
||||
return isInteger(object) || isShort(object) || isByte(object) || isLong(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies comments from an old node to a new one.
|
||||
*
|
||||
* @param oldNode The original node with comments.
|
||||
* @param newValueNode The new node to which comments should be copied.
|
||||
*/
|
||||
private void copyComments(Node oldNode, Node newValueNode) {
|
||||
if (oldNode == null || newValueNode == null) return;
|
||||
if (oldNode.getBlockComments() != null) {
|
||||
newValueNode.setBlockComments(oldNode.getBlockComments());
|
||||
}
|
||||
if (oldNode.getInLineComments() != null) {
|
||||
newValueNode.setInLineComments(oldNode.getInLineComments());
|
||||
}
|
||||
if (oldNode.getEndComments() != null) {
|
||||
newValueNode.setEndComments(oldNode.getEndComments());
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFontFactory;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
@Slf4j
|
||||
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
||||
|
||||
private String textColor;
|
||||
private String backgroundColor;
|
||||
private HighContrastColorCombination highContrastColorCombination;
|
||||
|
||||
public CustomColorReplaceStrategy(
|
||||
MultipartFile file,
|
||||
ReplaceAndInvert replaceAndInvert,
|
||||
String textColor,
|
||||
String backgroundColor,
|
||||
HighContrastColorCombination highContrastColorCombination) {
|
||||
super(file, replaceAndInvert);
|
||||
this.textColor = textColor;
|
||||
this.backgroundColor = backgroundColor;
|
||||
this.highContrastColorCombination = highContrastColorCombination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStreamResource replace() throws IOException {
|
||||
|
||||
// If ReplaceAndInvert is HighContrastColor option, then get the colors of text and
|
||||
// background from static
|
||||
if (replaceAndInvert == ReplaceAndInvert.HIGH_CONTRAST_COLOR) {
|
||||
String[] colors =
|
||||
HighContrastColorReplaceDecider.getColors(
|
||||
replaceAndInvert, highContrastColorCombination);
|
||||
this.textColor = colors[0];
|
||||
this.backgroundColor = colors[1];
|
||||
}
|
||||
|
||||
// Create a temporary file, with the original filename from the multipart file
|
||||
File file = Files.createTempFile("temp", getFileInput().getOriginalFilename()).toFile();
|
||||
|
||||
// Transfer the content of the multipart file to the file
|
||||
getFileInput().transferTo(file);
|
||||
|
||||
try (PDDocument document = Loader.loadPDF(file)) {
|
||||
|
||||
PDPageTree pages = document.getPages();
|
||||
|
||||
for (PDPage page : pages) {
|
||||
|
||||
PdfTextStripperCustom pdfTextStripperCustom = new PdfTextStripperCustom();
|
||||
// Get text positions
|
||||
List<List<TextPosition>> charactersByArticle =
|
||||
pdfTextStripperCustom.processPageCustom(page);
|
||||
|
||||
// Begin a new content stream
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
|
||||
// Set the new text color
|
||||
contentStream.setNonStrokingColor(Color.decode(this.textColor));
|
||||
|
||||
// Draw the text with the new color
|
||||
for (List<TextPosition> textPositions : charactersByArticle) {
|
||||
for (TextPosition text : textPositions) {
|
||||
// Move to the text position
|
||||
contentStream.beginText();
|
||||
contentStream.newLineAtOffset(
|
||||
text.getX(), page.getMediaBox().getHeight() - text.getY());
|
||||
PDFont font = null;
|
||||
String unicodeText = text.getUnicode();
|
||||
try {
|
||||
font = PDFontFactory.createFont(text.getFont().getCOSObject());
|
||||
} catch (IOException io) {
|
||||
log.info("Primary font not found, using fallback font.");
|
||||
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
}
|
||||
// if a character is not supported by font, then look for supported font
|
||||
try {
|
||||
byte[] bytes = font.encode(unicodeText);
|
||||
} catch (IOException io) {
|
||||
log.info("text could not be encoded ");
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} catch (IllegalArgumentException ie) {
|
||||
log.info("text not supported by font ");
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} finally {
|
||||
// if any other font is not supported, then replace default character *
|
||||
if (font == null) {
|
||||
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
unicodeText = "*";
|
||||
}
|
||||
}
|
||||
contentStream.setFont(font, text.getFontSize());
|
||||
contentStream.showText(unicodeText);
|
||||
contentStream.endText();
|
||||
}
|
||||
}
|
||||
// Close the content stream
|
||||
contentStream.close();
|
||||
// Use a content stream to overlay the background color
|
||||
try (PDPageContentStream contentStreamBg =
|
||||
new PDPageContentStream(
|
||||
document,
|
||||
page,
|
||||
PDPageContentStream.AppendMode.PREPEND,
|
||||
true,
|
||||
true)) {
|
||||
// Set background color (e.g., light yellow)
|
||||
contentStreamBg.setNonStrokingColor(Color.decode(this.backgroundColor));
|
||||
contentStreamBg.addRect(
|
||||
0, 0, page.getMediaBox().getWidth(), page.getMediaBox().getHeight());
|
||||
contentStreamBg.fill();
|
||||
}
|
||||
}
|
||||
// Save the modified PDF to a ByteArrayOutputStream
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
document.save(byteArrayOutputStream);
|
||||
document.close();
|
||||
|
||||
// Prepare the modified PDF for download
|
||||
ByteArrayInputStream inputStream =
|
||||
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
|
||||
InputStreamResource resource = new InputStreamResource(inputStream);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
private PDFont checkSupportedFontForCharacter(String unicodeText) {
|
||||
|
||||
Set<String> fonts = Standard14Fonts.getNames();
|
||||
for (String font : fonts) {
|
||||
Standard14Fonts.FontName fontName = Standard14Fonts.getMappedFontName(font);
|
||||
PDFont currentFont = new PDType1Font(fontName);
|
||||
try {
|
||||
byte[] bytes = currentFont.encode(unicodeText);
|
||||
return currentFont;
|
||||
} catch (IOException io) {
|
||||
log.info("text could not be encoded ");
|
||||
} catch (IllegalArgumentException ie) {
|
||||
log.info("text not supported by font ");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
public class HighContrastColorReplaceDecider {
|
||||
|
||||
// To decide the text and background colors for High contrast color option for replace-invert
|
||||
// color feature
|
||||
public static String[] getColors(
|
||||
ReplaceAndInvert replaceAndInvert,
|
||||
HighContrastColorCombination highContrastColorCombination) {
|
||||
|
||||
if (highContrastColorCombination == HighContrastColorCombination.BLACK_TEXT_ON_WHITE) {
|
||||
return new String[] {"0", "16777215"};
|
||||
} else if (highContrastColorCombination
|
||||
== HighContrastColorCombination.GREEN_TEXT_ON_BLACK) {
|
||||
return new String[] {"65280", "0"};
|
||||
} else if (highContrastColorCombination
|
||||
== HighContrastColorCombination.WHITE_TEXT_ON_BLACK) {
|
||||
return new String[] {"16777215", "0"};
|
||||
} else if (highContrastColorCombination
|
||||
== HighContrastColorCombination.YELLOW_TEXT_ON_BLACK) {
|
||||
|
||||
return new String[] {"16776960", "0"};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
||||
|
||||
public InvertFullColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
|
||||
super(file, replaceAndInvert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStreamResource replace() throws IOException {
|
||||
|
||||
File file = null;
|
||||
try {
|
||||
// Create a temporary file, with the original filename from the multipart file
|
||||
file = Files.createTempFile("temp", getFileInput().getOriginalFilename()).toFile();
|
||||
|
||||
// Transfer the content of the multipart file to the file
|
||||
getFileInput().transferTo(file);
|
||||
|
||||
// Load the uploaded PDF
|
||||
PDDocument document = Loader.loadPDF(file);
|
||||
|
||||
// Render each page and invert colors
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
for (int page = 0; page < document.getNumberOfPages(); page++) {
|
||||
BufferedImage image =
|
||||
pdfRenderer.renderImageWithDPI(page, 300); // Render page at 300 DPI
|
||||
|
||||
// Invert the colors
|
||||
invertImageColors(image);
|
||||
|
||||
// Create a new PDPage from the inverted image
|
||||
PDPage pdPage = document.getPage(page);
|
||||
File tempImageFile = null;
|
||||
try {
|
||||
tempImageFile = convertToBufferedImageTpFile(image);
|
||||
PDImageXObject pdImage =
|
||||
PDImageXObject.createFromFileByContent(tempImageFile, document);
|
||||
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document,
|
||||
pdPage,
|
||||
PDPageContentStream.AppendMode.OVERWRITE,
|
||||
true);
|
||||
contentStream.drawImage(
|
||||
pdImage,
|
||||
0,
|
||||
0,
|
||||
pdPage.getMediaBox().getWidth(),
|
||||
pdPage.getMediaBox().getHeight());
|
||||
contentStream.close();
|
||||
} finally {
|
||||
if (tempImageFile != null && tempImageFile.exists()) {
|
||||
Files.delete(tempImageFile.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the modified PDF to a ByteArrayOutputStream
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
document.save(byteArrayOutputStream);
|
||||
document.close();
|
||||
|
||||
// Prepare the modified PDF for download
|
||||
ByteArrayInputStream inputStream =
|
||||
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
|
||||
InputStreamResource resource = new InputStreamResource(inputStream);
|
||||
return resource;
|
||||
} finally {
|
||||
if (file != null && file.exists()) {
|
||||
Files.delete(file.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to invert image colors
|
||||
private void invertImageColors(BufferedImage image) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
int rgba = image.getRGB(x, y);
|
||||
Color color = new Color(rgba, true);
|
||||
Color invertedColor =
|
||||
new Color(
|
||||
255 - color.getRed(),
|
||||
255 - color.getGreen(),
|
||||
255 - color.getBlue());
|
||||
image.setRGB(x, y, invertedColor.getRGB());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to convert BufferedImage to InputStream
|
||||
private File convertToBufferedImageTpFile(BufferedImage image) throws IOException {
|
||||
File file = File.createTempFile("image", ".png");
|
||||
ImageIO.write(image, "png", file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.text.PDFTextStripperByArea;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
public class PdfTextStripperCustom extends PDFTextStripperByArea {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @throws IOException If there is an error loading properties.
|
||||
*/
|
||||
public PdfTextStripperCustom() throws IOException {}
|
||||
|
||||
// To process the page text using stripper and returns the TextPosition and its values
|
||||
public List<List<TextPosition>> processPageCustom(PDPage page) throws IOException {
|
||||
|
||||
addRegion(
|
||||
"wholePage",
|
||||
new Rectangle2D.Float(
|
||||
page.getMediaBox().getLowerLeftX(),
|
||||
page.getMediaBox().getLowerLeftY(),
|
||||
page.getMediaBox().getWidth(),
|
||||
page.getMediaBox().getHeight()));
|
||||
extractRegions(page);
|
||||
|
||||
List<List<TextPosition>> textPositions = getCharactersByArticle();
|
||||
|
||||
return textPositions;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public abstract class ReplaceAndInvertColorStrategy extends PDFFile {
|
||||
|
||||
protected ReplaceAndInvert replaceAndInvert;
|
||||
|
||||
public ReplaceAndInvertColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
|
||||
setFileInput(file);
|
||||
setReplaceAndInvert(replaceAndInvert);
|
||||
}
|
||||
|
||||
public abstract InputStreamResource replace() throws IOException;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package stirling.software.common.util.propertyeditor;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.api.security.RedactionArea;
|
||||
|
||||
@Slf4j
|
||||
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
setValue(new ArrayList<>());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
TypeReference<ArrayList<RedactionArea>> typeRef =
|
||||
new TypeReference<>() {
|
||||
};
|
||||
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
|
||||
setValue(list);
|
||||
} catch (Exception e) {
|
||||
log.error("Exception while converting {}", e);
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to convert java.lang.String to java.util.List");
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package stirling.software.common.util.propertyeditor;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
try {
|
||||
TypeReference<HashMap<String, String>> typeRef =
|
||||
new TypeReference<>() {};
|
||||
Map<String, String> map = objectMapper.readValue(text, typeRef);
|
||||
setValue(map);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to convert java.lang.String to java.util.Map");
|
||||
}
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.*;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.parallel.Execution;
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@Execution(value = ExecutionMode.SAME_THREAD)
|
||||
class CustomPDFDocumentFactoryTest {
|
||||
|
||||
private SpyPDFDocumentFactory factory;
|
||||
private byte[] basePdfBytes;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
PdfMetadataService mockService = mock(PdfMetadataService.class);
|
||||
factory = new SpyPDFDocumentFactory(mockService);
|
||||
|
||||
try (InputStream is = getClass().getResourceAsStream("/example.pdf")) {
|
||||
assertNotNull(is, "example.pdf must be present in src/test/resources");
|
||||
basePdfBytes = is.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
||||
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
||||
try (PDDocument doc = factory.load(file)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
try (PDDocument doc = factory.load(inflated)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||
try (PDDocument doc = factory.load(multipart)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileInput(multipart);
|
||||
try (PDDocument doc = factory.load(pdfFile)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
|
||||
try (PDDocument doc = Loader.loadPDF(input)) {
|
||||
byte[] largeData = new byte[sizeInMB * 1024 * 1024];
|
||||
Arrays.fill(largeData, (byte) 'A');
|
||||
|
||||
PDStream stream = new PDStream(doc, new ByteArrayInputStream(largeData));
|
||||
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
|
||||
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
|
||||
|
||||
doc.getDocumentCatalog()
|
||||
.getCOSObject()
|
||||
.setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
doc.save(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadFromPath() throws IOException {
|
||||
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
||||
Path path = file.toPath();
|
||||
try (PDDocument doc = factory.load(path)) {
|
||||
assertNotNull(doc);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadFromStringPath() throws IOException {
|
||||
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
||||
try (PDDocument doc = factory.load(file.getAbsolutePath())) {
|
||||
assertNotNull(doc);
|
||||
}
|
||||
}
|
||||
|
||||
// neeed to add password pdf
|
||||
// @Test
|
||||
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
|
||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
||||
// try (PDDocument doc = factory.load(is, "test123")) {
|
||||
// assertNotNull(doc);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void testLoadPasswordProtectedPdfFromMultipart() throws IOException {
|
||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
||||
// byte[] bytes = is.readAllBytes();
|
||||
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf",
|
||||
// "application/pdf", bytes);
|
||||
// try (PDDocument doc = factory.load(file, "test123")) {
|
||||
// assertNotNull(doc);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@Test
|
||||
void testLoadReadOnlySkipsPostProcessing() throws IOException {
|
||||
PdfMetadataService mockService = mock(PdfMetadataService.class);
|
||||
CustomPDFDocumentFactory readOnlyFactory = new CustomPDFDocumentFactory(mockService);
|
||||
|
||||
byte[] bytes = inflatePdf(basePdfBytes, 5);
|
||||
try (PDDocument doc = readOnlyFactory.load(bytes, true)) {
|
||||
assertNotNull(doc);
|
||||
verify(mockService, never()).setDefaultMetadata(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateNewDocument() throws IOException {
|
||||
try (PDDocument doc = factory.createNewDocument()) {
|
||||
assertNotNull(doc);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateNewDocumentBasedOnOldDocument() throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, 5);
|
||||
try (PDDocument oldDoc = Loader.loadPDF(inflated);
|
||||
PDDocument newDoc = factory.createNewDocumentBasedOnOldDocument(oldDoc)) {
|
||||
assertNotNull(newDoc);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadToBytesRoundTrip() throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, 5);
|
||||
File file = writeTempFile(inflated);
|
||||
|
||||
byte[] resultBytes = factory.loadToBytes(file);
|
||||
try (PDDocument doc = Loader.loadPDF(resultBytes)) {
|
||||
assertNotNull(doc);
|
||||
assertTrue(doc.getNumberOfPages() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveToBytesAndReload() throws IOException {
|
||||
try (PDDocument doc = Loader.loadPDF(basePdfBytes)) {
|
||||
byte[] saved = factory.saveToBytes(doc);
|
||||
try (PDDocument reloaded = Loader.loadPDF(saved)) {
|
||||
assertNotNull(reloaded);
|
||||
assertEquals(doc.getNumberOfPages(), reloaded.getNumberOfPages());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateNewBytesBasedOnOldDocument() throws IOException {
|
||||
byte[] newBytes = factory.createNewBytesBasedOnOldDocument(basePdfBytes);
|
||||
assertNotNull(newBytes);
|
||||
assertTrue(newBytes.length > 0);
|
||||
}
|
||||
|
||||
private File writeTempFile(byte[] content) throws IOException {
|
||||
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
|
||||
Files.write(file.toPath(), content);
|
||||
return file;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void cleanup() {
|
||||
System.gc();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
|
||||
|
||||
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||
enum StrategyType {
|
||||
MEMORY_ONLY,
|
||||
MIXED,
|
||||
TEMP_FILE
|
||||
}
|
||||
|
||||
public StrategyType lastStrategyUsed;
|
||||
|
||||
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
||||
StrategyType type;
|
||||
if (contentSize < 10 * 1024 * 1024) {
|
||||
type = StrategyType.MEMORY_ONLY;
|
||||
} else if (contentSize < 50 * 1024 * 1024) {
|
||||
type = StrategyType.MIXED;
|
||||
} else {
|
||||
type = StrategyType.TEMP_FILE;
|
||||
}
|
||||
this.lastStrategyUsed = type;
|
||||
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CheckProgramInstallTest {
|
||||
|
||||
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
||||
private ProcessExecutor mockExecutor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
// Reset static variables before each test
|
||||
resetStaticFields();
|
||||
|
||||
// Set up mock for ProcessExecutor
|
||||
mockExecutor = Mockito.mock(ProcessExecutor.class);
|
||||
mockProcessExecutor = mockStatic(ProcessExecutor.class);
|
||||
mockProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV))
|
||||
.thenReturn(mockExecutor);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// Close the static mock to prevent memory leaks
|
||||
if (mockProcessExecutor != null) {
|
||||
mockProcessExecutor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset static fields in the CheckProgramInstall class using reflection */
|
||||
private void resetStaticFields() throws Exception {
|
||||
Field pythonAvailableCheckedField =
|
||||
CheckProgramInstall.class.getDeclaredField("pythonAvailableChecked");
|
||||
pythonAvailableCheckedField.setAccessible(true);
|
||||
pythonAvailableCheckedField.set(null, false);
|
||||
|
||||
Field availablePythonCommandField =
|
||||
CheckProgramInstall.class.getDeclaredField("availablePythonCommand");
|
||||
availablePythonCommandField.setAccessible(true);
|
||||
availablePythonCommandField.set(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAvailablePythonCommand_WhenPython3IsAvailable()
|
||||
throws IOException, InterruptedException {
|
||||
// Arrange
|
||||
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(result.getRc()).thenReturn(0);
|
||||
when(result.getMessages()).thenReturn("Python 3.9.0");
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
|
||||
.thenReturn(result);
|
||||
|
||||
// Act
|
||||
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
// Assert
|
||||
assertEquals("python3", pythonCommand);
|
||||
assertTrue(CheckProgramInstall.isPythonAvailable());
|
||||
|
||||
// Verify that the command was executed
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAvailablePythonCommand_WhenPython3IsNotAvailableButPythonIs()
|
||||
throws IOException, InterruptedException {
|
||||
// Arrange
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
|
||||
.thenThrow(new IOException("Command not found"));
|
||||
|
||||
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(result.getRc()).thenReturn(0);
|
||||
when(result.getMessages()).thenReturn("Python 2.7.0");
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python", "--version")))
|
||||
.thenReturn(result);
|
||||
|
||||
// Act
|
||||
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
// Assert
|
||||
assertEquals("python", pythonCommand);
|
||||
assertTrue(CheckProgramInstall.isPythonAvailable());
|
||||
|
||||
// Verify that both commands were attempted
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python", "--version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode()
|
||||
throws IOException, InterruptedException, Exception {
|
||||
// Arrange
|
||||
// Reset the static fields again to ensure clean state
|
||||
resetStaticFields();
|
||||
|
||||
// Since we want to test the scenario where Python returns a non-zero exit code
|
||||
// We need to make sure both python3 and python commands are mocked to return failures
|
||||
|
||||
ProcessExecutorResult resultPython3 = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(resultPython3.getRc()).thenReturn(1); // Non-zero exit code
|
||||
when(resultPython3.getMessages()).thenReturn("Error");
|
||||
|
||||
// Important: in the CheckProgramInstall implementation, only checks if
|
||||
// command throws exception, it doesn't check the return code
|
||||
// So we need to throw an exception instead
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
|
||||
.thenThrow(new IOException("Command failed with non-zero exit code"));
|
||||
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python", "--version")))
|
||||
.thenThrow(new IOException("Command failed with non-zero exit code"));
|
||||
|
||||
// Act
|
||||
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
// Assert - Both commands throw exceptions, so no python is available
|
||||
assertNull(pythonCommand);
|
||||
assertFalse(CheckProgramInstall.isPythonAvailable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAvailablePythonCommand_WhenNoPythonIsAvailable()
|
||||
throws IOException, InterruptedException {
|
||||
// Arrange
|
||||
when(mockExecutor.runCommandWithOutputHandling(any(List.class)))
|
||||
.thenThrow(new IOException("Command not found"));
|
||||
|
||||
// Act
|
||||
String pythonCommand = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
// Assert
|
||||
assertNull(pythonCommand);
|
||||
assertFalse(CheckProgramInstall.isPythonAvailable());
|
||||
|
||||
// Verify attempts to run both python3 and python
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python", "--version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAvailablePythonCommand_CachesResult() throws IOException, InterruptedException {
|
||||
// Arrange
|
||||
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(result.getRc()).thenReturn(0);
|
||||
when(result.getMessages()).thenReturn("Python 3.9.0");
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
|
||||
.thenReturn(result);
|
||||
|
||||
// Act
|
||||
String firstCall = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
// Change the mock to simulate a change in the environment
|
||||
when(mockExecutor.runCommandWithOutputHandling(any(List.class)))
|
||||
.thenThrow(new IOException("Command not found"));
|
||||
|
||||
String secondCall = CheckProgramInstall.getAvailablePythonCommand();
|
||||
|
||||
// Assert
|
||||
assertEquals("python3", firstCall);
|
||||
assertEquals("python3", secondCall); // Second call should return the cached result
|
||||
|
||||
// Verify python3 command was only executed once (caching worked)
|
||||
verify(mockExecutor, times(1))
|
||||
.runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPythonAvailable_DirectCall() throws Exception {
|
||||
// Arrange
|
||||
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(result.getRc()).thenReturn(0);
|
||||
when(result.getMessages()).thenReturn("Python 3.9.0");
|
||||
when(mockExecutor.runCommandWithOutputHandling(Arrays.asList("python3", "--version")))
|
||||
.thenReturn(result);
|
||||
|
||||
// Reset again to ensure clean state
|
||||
resetStaticFields();
|
||||
|
||||
// Act - Call isPythonAvailable() directly
|
||||
boolean pythonAvailable = CheckProgramInstall.isPythonAvailable();
|
||||
|
||||
// Assert
|
||||
assertTrue(pythonAvailable);
|
||||
|
||||
// Verify getAvailablePythonCommand was called internally
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
class CustomHtmlSanitizerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideHtmlTestCases")
|
||||
void testSanitizeHtml(String inputHtml, String[] expectedContainedTags) {
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(inputHtml);
|
||||
|
||||
// Assert
|
||||
for (String tag : expectedContainedTags) {
|
||||
assertTrue(sanitizedHtml.contains(tag), tag + " should be preserved");
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> provideHtmlTestCases() {
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
||||
new String[] {"<p>", "<strong>", "<em>"}),
|
||||
Arguments.of(
|
||||
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
||||
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
||||
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
||||
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
||||
new String[] {
|
||||
"<b>bold</b>",
|
||||
"<i>italic</i>",
|
||||
"<em>emphasis</em>",
|
||||
"<strong>strong</strong>"
|
||||
}),
|
||||
Arguments.of(
|
||||
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
|
||||
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
||||
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
|
||||
+ "<ol><li>Ordered item</li></ol>",
|
||||
new String[] {
|
||||
"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeAllowsStyles() {
|
||||
// Arrange - Testing Sanitizers.STYLES
|
||||
String htmlWithStyles =
|
||||
"<p style=\"color: blue; font-size: 16px; margin-top: 10px;\">Styled text</p>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithStyles);
|
||||
|
||||
// Assert
|
||||
// The OWASP HTML Sanitizer might filter some specific styles, so we only check that
|
||||
// the sanitized HTML is not empty and contains a paragraph tag with style
|
||||
assertTrue(sanitizedHtml.contains("<p"), "Paragraph tag should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("style="), "Style attribute should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("Styled text"), "Content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeAllowsLinks() {
|
||||
// Arrange - Testing Sanitizers.LINKS
|
||||
String htmlWithLink =
|
||||
"<a href=\"https://example.com\" title=\"Example Site\">Example Link</a>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithLink);
|
||||
|
||||
// Assert
|
||||
// The most important aspect is that the link content is preserved
|
||||
assertTrue(sanitizedHtml.contains("Example Link"), "Link text should be preserved");
|
||||
|
||||
// Check that the href is present in some form
|
||||
assertTrue(sanitizedHtml.contains("href="), "Link href attribute should be present");
|
||||
|
||||
// Check that the URL is present in some form
|
||||
assertTrue(sanitizedHtml.contains("example.com"), "Link URL should be preserved");
|
||||
|
||||
// OWASP sanitizer may handle title attributes differently depending on version
|
||||
// So we won't make strict assertions about the title attribute
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeDisallowsJavaScriptLinks() {
|
||||
// Arrange
|
||||
String htmlWithJsLink = "<a href=\"javascript:alert('XSS')\">Malicious Link</a>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsLink);
|
||||
|
||||
// Assert
|
||||
assertFalse(sanitizedHtml.contains("javascript:"), "JavaScript URLs should be removed");
|
||||
// The link tag might still be there, but the href should be sanitized
|
||||
assertTrue(sanitizedHtml.contains("Malicious Link"), "Link text should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeAllowsTables() {
|
||||
// Arrange - Testing Sanitizers.TABLES
|
||||
String htmlWithTable =
|
||||
"<table border=\"1\">"
|
||||
+ "<thead><tr><th>Header 1</th><th>Header 2</th></tr></thead>"
|
||||
+ "<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody>"
|
||||
+ "<tfoot><tr><td colspan=\"2\">Footer</td></tr></tfoot>"
|
||||
+ "</table>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithTable);
|
||||
|
||||
// Assert
|
||||
assertTrue(sanitizedHtml.contains("<table"), "Table should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("<tr>"), "Table rows should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("<th>"), "Table headers should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("<td>"), "Table cells should be preserved");
|
||||
// Note: border attribute might be removed as it's deprecated in HTML5
|
||||
|
||||
// Check for content values instead of exact tag formats because
|
||||
// the sanitizer may normalize tags and attributes
|
||||
assertTrue(sanitizedHtml.contains("Header 1"), "Table header content should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("Cell 1"), "Table cell content should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("Footer"), "Table footer content should be preserved");
|
||||
|
||||
// OWASP sanitizer may not preserve these structural elements or attributes in the same
|
||||
// format
|
||||
// So we check for the content rather than the exact structure
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeAllowsImages() {
|
||||
// Arrange - Testing Sanitizers.IMAGES
|
||||
String htmlWithImage =
|
||||
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithImage);
|
||||
|
||||
// Assert
|
||||
assertTrue(sanitizedHtml.contains("<img"), "Image tag should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("src=\"image.jpg\""), "Image source should be preserved");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("alt=\"An image\""), "Image alt text should be preserved");
|
||||
// Width and height might be preserved, but not guaranteed by all sanitizers
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeDisallowsDataUrlImages() {
|
||||
// Arrange
|
||||
String htmlWithDataUrlImage =
|
||||
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\" alt=\"SVG with XSS\">";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithDataUrlImage);
|
||||
|
||||
// Assert
|
||||
assertFalse(
|
||||
sanitizedHtml.contains("data:image/svg"),
|
||||
"Data URLs with potentially malicious content should be removed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeRemovesJavaScriptInAttributes() {
|
||||
// Arrange
|
||||
String htmlWithJsEvent =
|
||||
"<a href=\"#\" onclick=\"alert('XSS')\" onmouseover=\"alert('XSS')\">Click me</a>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsEvent);
|
||||
|
||||
// Assert
|
||||
assertFalse(
|
||||
sanitizedHtml.contains("onclick"), "JavaScript event handlers should be removed");
|
||||
assertFalse(
|
||||
sanitizedHtml.contains("onmouseover"),
|
||||
"JavaScript event handlers should be removed");
|
||||
assertTrue(sanitizedHtml.contains("Click me"), "Link text should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeRemovesScriptTags() {
|
||||
// Arrange
|
||||
String htmlWithScript = "<p>Safe content</p><script>alert('XSS');</script>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithScript);
|
||||
|
||||
// Assert
|
||||
assertFalse(sanitizedHtml.contains("<script>"), "Script tags should be removed");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeRemovesNoScriptTags() {
|
||||
// Arrange - Testing the custom policy to disallow noscript
|
||||
String htmlWithNoscript = "<p>Safe content</p><noscript>JavaScript is disabled</noscript>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithNoscript);
|
||||
|
||||
// Assert
|
||||
assertFalse(sanitizedHtml.contains("<noscript>"), "Noscript tags should be removed");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeRemovesIframes() {
|
||||
// Arrange
|
||||
String htmlWithIframe = "<p>Safe content</p><iframe src=\"https://example.com\"></iframe>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithIframe);
|
||||
|
||||
// Assert
|
||||
assertFalse(sanitizedHtml.contains("<iframe"), "Iframe tags should be removed");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeRemovesObjectAndEmbed() {
|
||||
// Arrange
|
||||
String htmlWithObjects =
|
||||
"<p>Safe content</p>"
|
||||
+ "<object data=\"data.swf\" type=\"application/x-shockwave-flash\"></object>"
|
||||
+ "<embed src=\"embed.swf\" type=\"application/x-shockwave-flash\">";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithObjects);
|
||||
|
||||
// Assert
|
||||
assertFalse(sanitizedHtml.contains("<object"), "Object tags should be removed");
|
||||
assertFalse(sanitizedHtml.contains("<embed"), "Embed tags should be removed");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeRemovesMetaAndBaseAndLink() {
|
||||
// Arrange
|
||||
String htmlWithMetaTags =
|
||||
"<p>Safe content</p>"
|
||||
+ "<meta http-equiv=\"refresh\" content=\"0; url=http://evil.com\">"
|
||||
+ "<base href=\"http://evil.com/\">"
|
||||
+ "<link rel=\"stylesheet\" href=\"evil.css\">";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithMetaTags);
|
||||
|
||||
// Assert
|
||||
assertFalse(sanitizedHtml.contains("<meta"), "Meta tags should be removed");
|
||||
assertFalse(sanitizedHtml.contains("<base"), "Base tags should be removed");
|
||||
assertFalse(sanitizedHtml.contains("<link"), "Link tags should be removed");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<p>Safe content</p>"), "Safe content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeHandlesComplexHtml() {
|
||||
// Arrange
|
||||
String complexHtml =
|
||||
"<div class=\"container\">"
|
||||
+ " <h1 style=\"color: blue;\">Welcome</h1>"
|
||||
+ " <p>This is a <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p>"
|
||||
+ " <table>"
|
||||
+ " <tr><th>Name</th><th>Value</th></tr>"
|
||||
+ " <tr><td>Item 1</td><td>100</td></tr>"
|
||||
+ " </table>"
|
||||
+ " <img src=\"image.jpg\" alt=\"Test image\">"
|
||||
+ " <script>alert('XSS');</script>"
|
||||
+ " <iframe src=\"https://evil.com\"></iframe>"
|
||||
+ "</div>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(complexHtml);
|
||||
|
||||
// Assert
|
||||
assertTrue(sanitizedHtml.contains("<div"), "Div should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("<h1"), "H1 should be preserved");
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<strong>") && sanitizedHtml.contains("test"),
|
||||
"Strong tag should be preserved");
|
||||
|
||||
// Check for content rather than exact formatting
|
||||
assertTrue(
|
||||
sanitizedHtml.contains("<a")
|
||||
&& sanitizedHtml.contains("href=")
|
||||
&& sanitizedHtml.contains("example.com")
|
||||
&& sanitizedHtml.contains("link"),
|
||||
"Link should be preserved");
|
||||
|
||||
assertTrue(sanitizedHtml.contains("<table"), "Table should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("<img"), "Image should be preserved");
|
||||
assertFalse(sanitizedHtml.contains("<script>"), "Script tag should be removed");
|
||||
assertFalse(sanitizedHtml.contains("<iframe"), "Iframe tag should be removed");
|
||||
|
||||
// Content checks
|
||||
assertTrue(sanitizedHtml.contains("Welcome"), "Heading content should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("Name"), "Table header content should be preserved");
|
||||
assertTrue(sanitizedHtml.contains("Item 1"), "Table data content should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeHandlesEmpty() {
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize("");
|
||||
|
||||
// Assert
|
||||
assertEquals("", sanitizedHtml, "Empty input should result in empty string");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeHandlesNull() {
|
||||
// Act
|
||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(null);
|
||||
|
||||
// Assert
|
||||
assertEquals("", sanitizedHtml, "Null input should result in empty string");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
public class ErrorUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testExceptionToModel() {
|
||||
// Create a mock Model
|
||||
Model model = new org.springframework.ui.ExtendedModelMap();
|
||||
|
||||
// Create a test exception
|
||||
Exception ex = new Exception("Test Exception");
|
||||
|
||||
// Call the method under test
|
||||
Model resultModel = ErrorUtils.exceptionToModel(model, ex);
|
||||
|
||||
// Verify the result
|
||||
assertNotNull(resultModel);
|
||||
assertEquals("Test Exception", resultModel.getAttribute("errorMessage"));
|
||||
assertNotNull(resultModel.getAttribute("stackTrace"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionToModelView() {
|
||||
// Create a mock Model
|
||||
Model model = new org.springframework.ui.ExtendedModelMap();
|
||||
|
||||
// Create a test exception
|
||||
Exception ex = new Exception("Test Exception");
|
||||
|
||||
// Call the method under test
|
||||
ModelAndView modelAndView = ErrorUtils.exceptionToModelView(model, ex);
|
||||
|
||||
// Verify the result
|
||||
assertNotNull(modelAndView);
|
||||
assertEquals("Test Exception", modelAndView.getModel().get("errorMessage"));
|
||||
assertNotNull(modelAndView.getModel().get("stackTrace"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import stirling.software.common.model.FileInfo;
|
||||
|
||||
public class FileInfoTest {
|
||||
|
||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||
@CsvSource({
|
||||
"0, '0 Bytes'",
|
||||
"1023, '1023 Bytes'",
|
||||
"1024, '1.00 KB'",
|
||||
"1048575, '1024.00 KB'", // Do we really want this as result?
|
||||
"1048576, '1.00 MB'",
|
||||
"1073741823, '1024.00 MB'", // Do we really want this as result?
|
||||
"1073741824, '1.00 GB'"
|
||||
})
|
||||
void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) {
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
LocalDateTime.now(),
|
||||
fileSize,
|
||||
LocalDateTime.now().minusDays(1));
|
||||
|
||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FileMonitorTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
|
||||
@Mock private RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@Mock private Predicate<Path> pathFilter;
|
||||
|
||||
private FileMonitor fileMonitor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
|
||||
|
||||
// This mock is used in all tests except testPathFilter
|
||||
// We use lenient to avoid UnnecessaryStubbingException in that test
|
||||
Mockito.lenient().when(pathFilter.test(any())).thenReturn(true);
|
||||
|
||||
fileMonitor = new FileMonitor(pathFilter, runtimePathConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("test-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("recent-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to just now
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now()));
|
||||
|
||||
// File should not be ready for processing as it was just modified
|
||||
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_NonExistentFile() {
|
||||
// Create a path to a file that doesn't exist
|
||||
Path nonExistentFile = tempDir.resolve("non-existent-file.txt");
|
||||
|
||||
// Non-existent file should not be ready for processing
|
||||
assertFalse(fileMonitor.isFileReadyForProcessing(nonExistentFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("locked-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago to make sure it passes the time check
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// Verify the file is considered ready when it meets the time criteria
|
||||
assertTrue(
|
||||
fileMonitor.isFileReadyForProcessing(testFile),
|
||||
"File should be ready for processing when sufficiently old");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathFilter() throws IOException {
|
||||
// Use a simple lambda instead of a mock for better control
|
||||
Predicate<Path> pdfFilter = path -> path.toString().endsWith(".pdf");
|
||||
|
||||
// Create a new FileMonitor with the PDF filter
|
||||
FileMonitor pdfMonitor = new FileMonitor(pdfFilter, runtimePathConfig);
|
||||
|
||||
// Create a PDF file
|
||||
Path pdfFile = tempDir.resolve("test.pdf");
|
||||
Files.write(pdfFile, "pdf content".getBytes());
|
||||
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// Create a TXT file
|
||||
Path txtFile = tempDir.resolve("test.txt");
|
||||
Files.write(txtFile, "text content".getBytes());
|
||||
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// PDF file should be ready for processing
|
||||
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
||||
|
||||
// Note: In the current implementation, FileMonitor.isFileReadyForProcessing()
|
||||
// doesn't check file filters directly - it only checks criteria like file existence
|
||||
// and modification time. The filtering is likely handled elsewhere in the workflow.
|
||||
|
||||
// To avoid test failures, we'll verify that the filter itself works correctly
|
||||
assertFalse(pdfFilter.test(txtFile), "PDF filter should reject txt files");
|
||||
assertTrue(pdfFilter.test(pdfFile), "PDF filter should accept pdf files");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("in-use-file.txt");
|
||||
Files.write(testFile, "initial content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// First check that the file is ready when meeting time criteria
|
||||
assertTrue(
|
||||
fileMonitor.isFileReadyForProcessing(testFile),
|
||||
"File should be ready for processing when sufficiently old");
|
||||
|
||||
// After modifying the file to simulate closing, it should still be ready
|
||||
Files.write(testFile, "updated content".getBytes());
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
assertTrue(
|
||||
fileMonitor.isFileReadyForProcessing(testFile),
|
||||
"File should be ready for processing after updating");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||
// Use the absolute path to make sure it's handled correctly
|
||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile.toAbsolutePath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_DirectoryInsteadOfFile() throws IOException {
|
||||
// Create a test directory
|
||||
Path testDir = tempDir.resolve("test-directory");
|
||||
Files.createDirectory(testDir);
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testDir, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
|
||||
// A directory should not be considered ready for processing
|
||||
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
||||
assertFalse(isReady, "A directory should not be considered ready for processing");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
|
||||
public class FileToPdfTest {
|
||||
|
||||
/**
|
||||
* Test the HTML to PDF conversion. This test expects an IOException when an empty HTML input is
|
||||
* provided.
|
||||
*/
|
||||
@Test
|
||||
public void testConvertHtmlToPdf() {
|
||||
HTMLToPdfRequest request = new HTMLToPdfRequest();
|
||||
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
|
||||
String fileName = "test.html"; // Sample file name indicating an HTML file
|
||||
boolean disableSanitize = false; // Flag to control sanitization
|
||||
|
||||
// Expect an IOException to be thrown due to empty input
|
||||
Throwable thrown =
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
"/path/", request, fileBytes, fileName, disableSanitize));
|
||||
assertNotNull(thrown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test sanitizeZipFilename with null or empty input. It should return an empty string in these
|
||||
* cases.
|
||||
*/
|
||||
@Test
|
||||
public void testSanitizeZipFilename_NullOrEmpty() {
|
||||
assertEquals("", FileToPdf.sanitizeZipFilename(null));
|
||||
assertEquals("", FileToPdf.sanitizeZipFilename(" "));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test sanitizeZipFilename to ensure it removes path traversal sequences. This includes
|
||||
* removing both forward and backward slash sequences.
|
||||
*/
|
||||
@Test
|
||||
public void testSanitizeZipFilename_RemovesTraversalSequences() {
|
||||
String input = "../some/../path/..\\to\\file.txt";
|
||||
String expected = "some/path/to/file.txt";
|
||||
|
||||
// Expect that the method replaces backslashes with forward slashes
|
||||
// and removes path traversal sequences
|
||||
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
|
||||
}
|
||||
|
||||
/** Test sanitizeZipFilename to ensure that it removes leading drive letters and slashes. */
|
||||
@Test
|
||||
public void testSanitizeZipFilename_RemovesLeadingDriveAndSlashes() {
|
||||
String input = "C:\\folder\\file.txt";
|
||||
String expected = "folder/file.txt";
|
||||
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
|
||||
|
||||
input = "/folder/file.txt";
|
||||
expected = "folder/file.txt";
|
||||
assertEquals(expected, FileToPdf.sanitizeZipFilename(input));
|
||||
}
|
||||
|
||||
/** Test sanitizeZipFilename to verify that safe filenames remain unchanged. */
|
||||
@Test
|
||||
public void testSanitizeZipFilename_NoChangeForSafeNames() {
|
||||
String input = "folder/subfolder/file.txt";
|
||||
assertEquals(input, FileToPdf.sanitizeZipFilename(input));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class GeneralUtilsAdditionalTest {
|
||||
|
||||
@Test
|
||||
void testConvertSizeToBytes() {
|
||||
assertEquals(1024L, GeneralUtils.convertSizeToBytes("1KB"));
|
||||
assertEquals(1024L * 1024, GeneralUtils.convertSizeToBytes("1MB"));
|
||||
assertEquals(1024L * 1024 * 1024, GeneralUtils.convertSizeToBytes("1GB"));
|
||||
assertEquals(100L * 1024 * 1024, GeneralUtils.convertSizeToBytes("100"));
|
||||
assertNull(GeneralUtils.convertSizeToBytes("invalid"));
|
||||
assertNull(GeneralUtils.convertSizeToBytes(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFormatBytes() {
|
||||
assertEquals("512 B", GeneralUtils.formatBytes(512));
|
||||
assertEquals("1.00 KB", GeneralUtils.formatBytes(1024));
|
||||
assertEquals("1.00 MB", GeneralUtils.formatBytes(1024L * 1024));
|
||||
assertEquals("1.00 GB", GeneralUtils.formatBytes(1024L * 1024 * 1024));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testURLHelpersAndUUID() {
|
||||
assertTrue(GeneralUtils.isValidURL("https://example.com"));
|
||||
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
|
||||
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
|
||||
|
||||
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
|
||||
assertFalse(GeneralUtils.isValidUUID("not-a-uuid"));
|
||||
|
||||
assertFalse(GeneralUtils.isVersionHigher(null, "1.0"));
|
||||
assertTrue(GeneralUtils.isVersionHigher("2.0", "1.9"));
|
||||
assertFalse(GeneralUtils.isVersionHigher("1.0", "1.0.1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class GeneralUtilsTest {
|
||||
|
||||
@Test
|
||||
void testParsePageListWithAll() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"all"}, 5, false);
|
||||
assertEquals(List.of(0, 1, 2, 3, 4), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePageListWithAllOneBased() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"all"}, 5, true);
|
||||
assertEquals(List.of(1, 2, 3, 4, 5), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"n"}, 5, true);
|
||||
assertEquals(List.of(1, 2, 3, 4, 5), result, "'n' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvanced() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n"}, 9, true);
|
||||
// skip 0 as not valid
|
||||
assertEquals(List.of(4, 8), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvancedZero() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n"}, 9, false);
|
||||
// skip 0 as not valid
|
||||
assertEquals(List.of(3, 7), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvanced2() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n-1"}, 9, true);
|
||||
// skip -1 as not valid
|
||||
assertEquals(List.of(3, 7), result, "4n-1 should do (0-1), (4-1), (8-1)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvanced3() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n+1"}, 9, true);
|
||||
assertEquals(List.of(5, 9), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_spaces() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"n + 1"}, 9, true);
|
||||
assertEquals(List.of(2, 3, 4, 5, 6, 7, 8, 9), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_consecutive_Ns_nnn() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"nnn"}, 9, true);
|
||||
assertEquals(List.of(1, 8), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_consecutive_Ns_nn() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"nn"}, 9, true);
|
||||
assertEquals(List.of(1, 4, 9), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_opening_closing_round_brackets() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)(n-2)"}, 9, true);
|
||||
assertEquals(List.of(2, 6), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_opening_round_brackets() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"2(n-1)"}, 9, true);
|
||||
assertEquals(List.of(2, 4, 6, 8), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_opening_round_brackets_n() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"n(n-1)"}, 9, true);
|
||||
assertEquals(List.of(2, 6), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_closing_round_brackets() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)2"}, 9, true);
|
||||
assertEquals(List.of(2, 4, 6, 8), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_closing_round_brackets_n() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)n"}, 9, true);
|
||||
assertEquals(List.of(2, 6), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFunc_function_surrounded_with_brackets() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"(n-1)"}, 9, true);
|
||||
assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvanced4() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"3+2n"}, 9, true);
|
||||
assertEquals(List.of(5, 7, 9), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvancedZerobased() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n"}, 9, false);
|
||||
assertEquals(List.of(3, 7), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nFuncAdvanced2Zerobased() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"4n-1"}, 9, false);
|
||||
assertEquals(List.of(2, 6), result, "'All' keyword should return all pages.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePageListWithRangeOneBasedOutput() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1-3"}, 5, true);
|
||||
assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePageListWithRangeZeroBaseOutput() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1-3"}, 5, false);
|
||||
assertEquals(List.of(0, 1, 2), result, "Range should be parsed correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePageListWithRangeOneBasedOutputFull() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, true);
|
||||
assertEquals(List.of(1, 3, 7, 8), result, "Range should be parsed correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePageListWithRangeOneBasedOutputFullOutOfRange() {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 5, true);
|
||||
assertEquals(List.of(1, 3), result, "Range should be parsed correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePageListWithRangeZeroBaseOutputFull() {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ImageProcessingUtilsTest {
|
||||
|
||||
@Test
|
||||
void testConvertColorTypeToGreyscale() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
|
||||
|
||||
assertNotNull(convertedImage);
|
||||
assertEquals(BufferedImage.TYPE_BYTE_GRAY, convertedImage.getType());
|
||||
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
|
||||
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
|
||||
|
||||
// Check if a pixel is correctly converted to greyscale
|
||||
Color grey = new Color(convertedImage.getRGB(0, 0));
|
||||
assertEquals(grey.getRed(), grey.getGreen());
|
||||
assertEquals(grey.getGreen(), grey.getBlue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertColorTypeToBlackWhite() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
|
||||
|
||||
assertNotNull(convertedImage);
|
||||
assertEquals(BufferedImage.TYPE_BYTE_BINARY, convertedImage.getType());
|
||||
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
|
||||
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
|
||||
|
||||
// Check if a pixel is converted correctly (binary image will be either black or white)
|
||||
int rgb = convertedImage.getRGB(0, 0);
|
||||
assertTrue(rgb == Color.BLACK.getRGB() || rgb == Color.WHITE.getRGB());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertColorTypeToFullColor() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
|
||||
|
||||
assertNotNull(convertedImage);
|
||||
assertEquals(sourceImage, convertedImage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertColorTypeInvalid() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
|
||||
|
||||
assertNotNull(convertedImage);
|
||||
assertEquals(sourceImage, convertedImage);
|
||||
}
|
||||
|
||||
private void fillImageWithColor(BufferedImage image, Color color) {
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
image.setRGB(x, y, color.getRGB());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.ZipSecurity;
|
||||
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
/**
|
||||
* Tests for PDFToFile utility class. This includes both invalid content type cases and positive
|
||||
* test cases that mock external process execution.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PDFToFileTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
|
||||
private PDFToFile pdfToFile;
|
||||
|
||||
@Mock private ProcessExecutor mockProcessExecutor;
|
||||
@Mock private ProcessExecutorResult mockExecutorResult;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
pdfToFile = new PDFToFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToMarkdown_InvalidContentType() throws IOException, InterruptedException {
|
||||
// Prepare
|
||||
MultipartFile nonPdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
||||
|
||||
// Execute
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToHtml_InvalidContentType() throws IOException, InterruptedException {
|
||||
// Prepare
|
||||
MultipartFile nonPdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
||||
|
||||
// Execute
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(nonPdfFile);
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_InvalidContentType()
|
||||
throws IOException, InterruptedException {
|
||||
// Prepare
|
||||
MultipartFile nonPdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
||||
|
||||
// Execute
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFile.processPdfToOfficeFormat(nonPdfFile, "docx", "draw_pdf_import");
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_InvalidOutputFormat()
|
||||
throws IOException, InterruptedException {
|
||||
// Prepare
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
||||
|
||||
// Execute with invalid format
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFile.processPdfToOfficeFormat(pdfFile, "invalid_format", "draw_pdf_import");
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToMarkdown_SingleOutputFile() throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
||||
|
||||
// Create a mock HTML output file
|
||||
Path htmlOutputFile = tempDir.resolve("test.html");
|
||||
Files.write(
|
||||
htmlOutputFile,
|
||||
"<html><body><h1>Test</h1><p>This is a test.</p></body></html>".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class), any(File.class)))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, simulate creation of output files
|
||||
File outputDir = invocation.getArgument(1);
|
||||
|
||||
// Copy the mock HTML file to the output directory
|
||||
Files.copy(
|
||||
htmlOutputFile, Path.of(outputDir.getPath(), "test.html"));
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(pdfFile);
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
assertTrue(
|
||||
response.getHeaders().getContentDisposition().toString().contains("test.md"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToMarkdown_MultipleOutputFiles() throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"multipage.pdf",
|
||||
"application/pdf",
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class), any(File.class)))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, simulate creation of output files
|
||||
File outputDir = invocation.getArgument(1);
|
||||
|
||||
// Create multiple HTML files and an image
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "multipage.html"),
|
||||
"<html><body><h1>Cover</h1></body></html>".getBytes());
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "multipage-1.html"),
|
||||
"<html><body><h1>Page 1</h1></body></html>".getBytes());
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "multipage-2.html"),
|
||||
"<html><body><h1>Page 2</h1></body></html>".getBytes());
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "image1.png"),
|
||||
"Fake image data".getBytes());
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(pdfFile);
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
|
||||
// Verify content disposition indicates a zip file
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("ToMarkdown.zip"));
|
||||
|
||||
// Verify the content by unzipping it
|
||||
try (ZipInputStream zipStream =
|
||||
ZipSecurity.createHardenedInputStream(
|
||||
new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||
ZipEntry entry;
|
||||
boolean foundMdFiles = false;
|
||||
boolean foundImage = false;
|
||||
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
if (entry.getName().endsWith(".md")) {
|
||||
foundMdFiles = true;
|
||||
} else if (entry.getName().endsWith(".png")) {
|
||||
foundImage = true;
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
}
|
||||
|
||||
assertTrue(foundMdFiles, "ZIP should contain Markdown files");
|
||||
assertTrue(foundImage, "ZIP should contain image files");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToHtml() throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class), any(File.class)))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, simulate creation of output files
|
||||
File outputDir = invocation.getArgument(1);
|
||||
|
||||
// Create HTML files and assets
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "test.html"),
|
||||
"<html><frameset></frameset></html>".getBytes());
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "test_ind.html"),
|
||||
"<html><body>Index</body></html>".getBytes());
|
||||
Files.write(
|
||||
Path.of(outputDir.getPath(), "test_img.png"),
|
||||
"Fake image data".getBytes());
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(pdfFile);
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
|
||||
// Verify content disposition indicates a zip file
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("testToHtml.zip"));
|
||||
|
||||
// Verify the content by unzipping it
|
||||
try (ZipInputStream zipStream =
|
||||
ZipSecurity.createHardenedInputStream(
|
||||
new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||
ZipEntry entry;
|
||||
boolean foundMainHtml = false;
|
||||
boolean foundIndexHtml = false;
|
||||
boolean foundImage = false;
|
||||
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
if ("test.html".equals(entry.getName())) {
|
||||
foundMainHtml = true;
|
||||
} else if ("test_ind.html".equals(entry.getName())) {
|
||||
foundIndexHtml = true;
|
||||
} else if ("test_img.png".equals(entry.getName())) {
|
||||
foundImage = true;
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
}
|
||||
|
||||
assertTrue(foundMainHtml, "ZIP should contain main HTML file");
|
||||
assertTrue(foundIndexHtml, "ZIP should contain index HTML file");
|
||||
assertTrue(foundImage, "ZIP should contain image files");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_SingleOutputFile() throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(
|
||||
args ->
|
||||
args.contains("--convert-to")
|
||||
&& args.contains("docx"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, find the output directory argument
|
||||
List<String> args = invocation.getArgument(0);
|
||||
String outDir = null;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
|
||||
outDir = args.get(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file
|
||||
Files.write(
|
||||
Path.of(outDir, "document.docx"),
|
||||
"Fake DOCX content".getBytes());
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method with docx format
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import");
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
|
||||
// Verify content disposition has correct filename
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("document.docx"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_MultipleOutputFiles()
|
||||
throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(args -> args.contains("--convert-to") && args.contains("odp"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, find the output directory argument
|
||||
List<String> args = invocation.getArgument(0);
|
||||
String outDir = null;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
|
||||
outDir = args.get(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create multiple output files (simulating a presentation with
|
||||
// multiple files)
|
||||
Files.write(
|
||||
Path.of(outDir, "document.odp"),
|
||||
"Fake ODP content".getBytes());
|
||||
Files.write(
|
||||
Path.of(outDir, "document_media1.png"),
|
||||
"Image 1 content".getBytes());
|
||||
Files.write(
|
||||
Path.of(outDir, "document_media2.png"),
|
||||
"Image 2 content".getBytes());
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method with ODP format
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFile.processPdfToOfficeFormat(pdfFile, "odp", "draw_pdf_import");
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
|
||||
// Verify content disposition for zip file
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("documentToodp.zip"));
|
||||
|
||||
// Verify the content by unzipping it
|
||||
try (ZipInputStream zipStream =
|
||||
ZipSecurity.createHardenedInputStream(
|
||||
new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||
ZipEntry entry;
|
||||
boolean foundMainFile = false;
|
||||
boolean foundMediaFiles = false;
|
||||
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
if ("document.odp".equals(entry.getName())) {
|
||||
foundMainFile = true;
|
||||
} else if (entry.getName().startsWith("document_media")) {
|
||||
foundMediaFiles = true;
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
}
|
||||
|
||||
assertTrue(foundMainFile, "ZIP should contain main ODP file");
|
||||
assertTrue(foundMediaFiles, "ZIP should contain media files");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_TextFormat() throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(
|
||||
args ->
|
||||
args.contains("--convert-to")
|
||||
&& args.contains("txt:Text"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, find the output directory argument
|
||||
List<String> args = invocation.getArgument(0);
|
||||
String outDir = null;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
|
||||
outDir = args.get(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create text output file
|
||||
Files.write(
|
||||
Path.of(outDir, "document.txt"),
|
||||
"Extracted text content".getBytes());
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method with text format
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFile.processPdfToOfficeFormat(pdfFile, "txt:Text", "draw_pdf_import");
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
|
||||
// Verify content disposition has txt extension
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("document.txt"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_NoFilename() throws IOException, InterruptedException {
|
||||
// Setup mock objects and temp files
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
// Create a mock PDF file with no filename
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "", "application/pdf", "Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(any(List.class)))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, find the output directory argument
|
||||
List<String> args = invocation.getArgument(0);
|
||||
String outDir = null;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
|
||||
outDir = args.get(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file - uses default name
|
||||
Files.write(
|
||||
Path.of(outDir, "output.docx"),
|
||||
"Fake DOCX content".getBytes());
|
||||
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
// Execute the method
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import");
|
||||
|
||||
// Verify
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
|
||||
// Verify content disposition contains output.docx
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("output.docx"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
|
||||
public class PdfUtilsTest {
|
||||
|
||||
@Test
|
||||
void testTextToPageSize() {
|
||||
assertEquals(PDRectangle.A4, PdfUtils.textToPageSize("A4"));
|
||||
assertEquals(PDRectangle.LETTER, PdfUtils.textToPageSize("LETTER"));
|
||||
assertThrows(IllegalArgumentException.class, () -> PdfUtils.textToPageSize("INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasImagesOnPage() throws IOException {
|
||||
// Mock a PDPage and its resources
|
||||
PDPage page = Mockito.mock(PDPage.class);
|
||||
PDResources resources = Mockito.mock(PDResources.class);
|
||||
Mockito.when(page.getResources()).thenReturn(resources);
|
||||
|
||||
// Case 1: No images in resources
|
||||
Mockito.when(resources.getXObjectNames()).thenReturn(Collections.emptySet());
|
||||
assertFalse(PdfUtils.hasImagesOnPage(page));
|
||||
|
||||
// Case 2: Resources with an image
|
||||
Set<COSName> xObjectNames = new HashSet<>();
|
||||
COSName cosName = Mockito.mock(COSName.class);
|
||||
xObjectNames.add(cosName);
|
||||
|
||||
PDImageXObject imageXObject = Mockito.mock(PDImageXObject.class);
|
||||
Mockito.when(resources.getXObjectNames()).thenReturn(xObjectNames);
|
||||
Mockito.when(resources.getXObject(cosName)).thenReturn(imageXObject);
|
||||
|
||||
assertTrue(PdfUtils.hasImagesOnPage(page));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPageCountComparators() throws Exception {
|
||||
PDDocument doc1 = new PDDocument();
|
||||
doc1.addPage(new PDPage());
|
||||
doc1.addPage(new PDPage());
|
||||
doc1.addPage(new PDPage());
|
||||
PdfUtils utils = new PdfUtils();
|
||||
assertTrue(utils.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"));
|
||||
|
||||
PDDocument doc3 = new PDDocument();
|
||||
doc3.addPage(new PDPage());
|
||||
doc3.addPage(new PDPage());
|
||||
assertTrue(utils.pageCount(doc3, 5, "less"));
|
||||
|
||||
PDDocument doc4 = new PDDocument();
|
||||
doc4.addPage(new PDPage());
|
||||
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPageSize() throws Exception {
|
||||
PDDocument doc = new PDDocument();
|
||||
PDPage page = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(page);
|
||||
PDRectangle rect = page.getMediaBox();
|
||||
String expected = rect.getWidth() + "x" + rect.getHeight();
|
||||
PdfUtils utils = new PdfUtils();
|
||||
assertTrue(utils.pageSize(doc, expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOverlayImage() throws Exception {
|
||||
PDDocument doc = new PDDocument();
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
|
||||
doc.save(pdfOut);
|
||||
doc.close();
|
||||
|
||||
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = image.createGraphics();
|
||||
g.setColor(Color.RED);
|
||||
g.fillRect(0, 0, 10, 10);
|
||||
g.dispose();
|
||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
||||
javax.imageio.ImageIO.write(image, "png", imgOut);
|
||||
|
||||
PdfMetadataService meta =
|
||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||
|
||||
byte[] result =
|
||||
PdfUtils.overlayImage(
|
||||
factory, pdfOut.toByteArray(), imgOut.toByteArray(), 0, 0, false);
|
||||
try (PDDocument resultDoc = factory.load(result)) {
|
||||
assertEquals(1, resultDoc.getNumberOfPages());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ProcessExecutorTest {
|
||||
|
||||
private ProcessExecutor processExecutor;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// Initialize the ProcessExecutor instance
|
||||
processExecutor = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunCommandWithOutputHandling() throws IOException, InterruptedException {
|
||||
// Mock the command to execute
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("java");
|
||||
command.add("-version");
|
||||
|
||||
// Execute the command
|
||||
ProcessExecutor.ProcessExecutorResult result =
|
||||
processExecutor.runCommandWithOutputHandling(command);
|
||||
|
||||
// Check the exit code and output messages
|
||||
assertEquals(0, result.getRc());
|
||||
assertNotNull(result.getMessages()); // Check if messages are not null
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunCommandWithOutputHandling_Error() {
|
||||
// Mock the command to execute
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("nonexistent-command");
|
||||
|
||||
// Execute the command and expect an IOException
|
||||
IOException thrown =
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> {
|
||||
processExecutor.runCommandWithOutputHandling(command);
|
||||
});
|
||||
|
||||
// Check the exception message to ensure it indicates the command was not found
|
||||
String errorMessage = thrown.getMessage();
|
||||
assertTrue(
|
||||
errorMessage.contains("error=2")
|
||||
|| errorMessage.contains("No such file or directory"),
|
||||
"Unexpected error message: " + errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class PropertyConfigsTest {
|
||||
|
||||
@Test
|
||||
public void testGetBooleanValue_WithKeys() {
|
||||
// Define keys and default value
|
||||
List<String> keys = Arrays.asList("test.key1", "test.key2", "test.key3");
|
||||
boolean defaultValue = false;
|
||||
|
||||
// Set property for one of the keys
|
||||
System.setProperty("test.key2", "true");
|
||||
|
||||
// Call the method under test
|
||||
boolean result = PropertyConfigs.getBooleanValue(keys, defaultValue);
|
||||
|
||||
// Verify the result
|
||||
assertEquals(true, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStringValue_WithKeys() {
|
||||
// Define keys and default value
|
||||
List<String> keys = Arrays.asList("test.key1", "test.key2", "test.key3");
|
||||
String defaultValue = "default";
|
||||
|
||||
// Set property for one of the keys
|
||||
System.setProperty("test.key2", "value");
|
||||
|
||||
// Call the method under test
|
||||
String result = PropertyConfigs.getStringValue(keys, defaultValue);
|
||||
|
||||
// Verify the result
|
||||
assertEquals("value", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBooleanValue_WithKey() {
|
||||
// Define key and default value
|
||||
String key = "test.key";
|
||||
boolean defaultValue = true;
|
||||
|
||||
// Call the method under test
|
||||
boolean result = PropertyConfigs.getBooleanValue(key, defaultValue);
|
||||
|
||||
// Verify the result
|
||||
assertEquals(true, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStringValue_WithKey() {
|
||||
// Define key and default value
|
||||
String key = "test.key";
|
||||
String defaultValue = "default";
|
||||
|
||||
// Call the method under test
|
||||
String result = PropertyConfigs.getStringValue(key, defaultValue);
|
||||
|
||||
// Verify the result
|
||||
assertEquals("default", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProviderUtilsTest {
|
||||
|
||||
@Test
|
||||
void testSuccessfulValidation() {
|
||||
var provider = mock(GitHubProvider.class);
|
||||
|
||||
when(provider.getClientId()).thenReturn("clientId");
|
||||
when(provider.getClientSecret()).thenReturn("clientSecret");
|
||||
when(provider.getScopes()).thenReturn(List.of("read:user"));
|
||||
|
||||
Assertions.assertTrue(ProviderUtils.validateProvider(provider));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("providerParams")
|
||||
void testUnsuccessfulValidation(Provider provider) {
|
||||
Assertions.assertFalse(ProviderUtils.validateProvider(provider));
|
||||
}
|
||||
|
||||
public static Stream<Arguments> providerParams() {
|
||||
Provider generic = null;
|
||||
var google =
|
||||
new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
|
||||
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
|
||||
|
||||
return Stream.of(Arguments.of(generic), Arguments.of(google), Arguments.of(github));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
public class RequestUriUtilsTest {
|
||||
|
||||
@Test
|
||||
void testIsStaticResource() {
|
||||
// Test static resources without context path
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/css/styles.css"), "CSS files should be static");
|
||||
assertTrue(RequestUriUtils.isStaticResource("/js/script.js"), "JS files should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/images/logo.png"),
|
||||
"Image files should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/public/index.html"),
|
||||
"Public files should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"),
|
||||
"PDF.js files should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/api/v1/info/status"),
|
||||
"API status should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/some-path/icon.svg"),
|
||||
"SVG files should be static");
|
||||
assertTrue(RequestUriUtils.isStaticResource("/login"), "Login page should be static");
|
||||
assertTrue(RequestUriUtils.isStaticResource("/error"), "Error page should be static");
|
||||
|
||||
// Test non-static resources
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource("/api/v1/users"),
|
||||
"API users should not be static");
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource("/api/v1/orders"),
|
||||
"API orders should not be static");
|
||||
assertFalse(RequestUriUtils.isStaticResource("/"), "Root path should not be static");
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource("/register"),
|
||||
"Register page should not be static");
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource("/api/v1/products"),
|
||||
"API products should not be static");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsStaticResourceWithContextPath() {
|
||||
String contextPath = "/myapp";
|
||||
|
||||
// Test static resources with context path
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/css/styles.css"),
|
||||
"CSS with context path should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/js/script.js"),
|
||||
"JS with context path should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/images/logo.png"),
|
||||
"Images with context path should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/login"),
|
||||
"Login with context path should be static");
|
||||
|
||||
// Test non-static resources with context path
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource(contextPath, contextPath + "/api/v1/users"),
|
||||
"API users with context path should not be static");
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource(contextPath, "/"),
|
||||
"Root path with context path should not be static");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"robots.txt",
|
||||
"/favicon.ico",
|
||||
"/icon.svg",
|
||||
"/image.png",
|
||||
"/site.webmanifest",
|
||||
"/app/logo.svg",
|
||||
"/downloads/document.png",
|
||||
"/assets/brand.ico",
|
||||
"/any/path/with/image.svg",
|
||||
"/deep/nested/folder/icon.png"
|
||||
})
|
||||
void testIsStaticResourceWithFileExtensions(String path) {
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource(path),
|
||||
"Files with specific extensions should be static regardless of path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsTrackableResource() {
|
||||
// Test non-trackable resources (returns false)
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/js/script.js"),
|
||||
"JS files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/v1/api-docs"),
|
||||
"API docs should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("robots.txt"),
|
||||
"robots.txt should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/images/logo.png"),
|
||||
"Images should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/styles.css"),
|
||||
"CSS files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/script.js.map"),
|
||||
"Map files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/icon.svg"),
|
||||
"SVG files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/popularity.txt"),
|
||||
"Popularity file should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/script.js"),
|
||||
"JS files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/swagger/index.html"),
|
||||
"Swagger files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/api/v1/info/status"),
|
||||
"API info should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/site.webmanifest"),
|
||||
"Webmanifest should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/fonts/font.woff"),
|
||||
"Fonts should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/pdfjs/viewer.js"),
|
||||
"PDF.js files should not be trackable");
|
||||
|
||||
// Test trackable resources (returns true)
|
||||
assertTrue(RequestUriUtils.isTrackableResource("/login"), "Login page should be trackable");
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource("/register"),
|
||||
"Register page should be trackable");
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource("/api/v1/users"),
|
||||
"API users should be trackable");
|
||||
assertTrue(RequestUriUtils.isTrackableResource("/"), "Root path should be trackable");
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource("/some-other-path"),
|
||||
"Other paths should be trackable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsTrackableResourceWithContextPath() {
|
||||
String contextPath = "/myapp";
|
||||
|
||||
// Test with context path
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/js/script.js"),
|
||||
"JS files should not be trackable with context path");
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/login"),
|
||||
"Login page should be trackable with context path");
|
||||
|
||||
// Additional tests with context path
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/fonts/custom.woff"),
|
||||
"Font files should not be trackable with context path");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/images/header.png"),
|
||||
"Images should not be trackable with context path");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/swagger/ui.html"),
|
||||
"Swagger UI should not be trackable with context path");
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/account/profile"),
|
||||
"Account page should be trackable with context path");
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource(contextPath, "/pdf/view"),
|
||||
"PDF view page should be trackable with context path");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"/js/util.js",
|
||||
"/v1/api-docs/swagger.json",
|
||||
"/robots.txt",
|
||||
"/images/header/logo.png",
|
||||
"/styles/theme.css",
|
||||
"/build/app.js.map",
|
||||
"/assets/icon.svg",
|
||||
"/data/popularity.txt",
|
||||
"/bundle.js",
|
||||
"/api/swagger-ui.html",
|
||||
"/api/v1/info/health",
|
||||
"/site.webmanifest",
|
||||
"/fonts/roboto.woff",
|
||||
"/pdfjs/viewer.js"
|
||||
})
|
||||
void testNonTrackableResources(String path) {
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource(path),
|
||||
"Resources matching patterns should not be trackable: " + path);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"/",
|
||||
"/home",
|
||||
"/login",
|
||||
"/register",
|
||||
"/pdf/merge",
|
||||
"/pdf/split",
|
||||
"/api/v1/users/1",
|
||||
"/api/v1/documents/process",
|
||||
"/settings",
|
||||
"/account/profile",
|
||||
"/dashboard",
|
||||
"/help",
|
||||
"/about"
|
||||
})
|
||||
void testTrackableResources(String path) {
|
||||
assertTrue(
|
||||
RequestUriUtils.isTrackableResource(path),
|
||||
"App routes should be trackable: " + path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEdgeCases() {
|
||||
// Test with empty strings
|
||||
assertFalse(RequestUriUtils.isStaticResource("", ""), "Empty path should not be static");
|
||||
assertTrue(RequestUriUtils.isTrackableResource("", ""), "Empty path should be trackable");
|
||||
|
||||
// Test with null-like behavior (would actually throw NPE in real code)
|
||||
// These are not actual null tests but shows handling of odd cases
|
||||
assertFalse(RequestUriUtils.isStaticResource("null"), "String 'null' should not be static");
|
||||
|
||||
// Test String "null" as a path
|
||||
boolean isTrackable = RequestUriUtils.isTrackableResource("null");
|
||||
assertTrue(isTrackable, "String 'null' should be trackable");
|
||||
|
||||
// Mixed case extensions test - note that Java's endsWith() is case-sensitive
|
||||
// We'll check actual behavior and document it rather than asserting
|
||||
|
||||
// Always test the lowercase versions which should definitely work
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/logo.png"), "PNG (lowercase) should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/icon.svg"), "SVG (lowercase) should be static");
|
||||
|
||||
// Path with query parameters
|
||||
assertFalse(
|
||||
RequestUriUtils.isStaticResource("/api/users?page=1"),
|
||||
"Path with query params should respect base path");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/images/logo.png?v=123"),
|
||||
"Static resource with query params should still be static");
|
||||
|
||||
// Paths with fragments
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/css/styles.css#section1"),
|
||||
"CSS with fragment should be static");
|
||||
|
||||
// Multiple dots in filename
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/js/jquery.min.js"),
|
||||
"JS with multiple dots should be static");
|
||||
|
||||
// Special characters in path
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/images/user's-photo.png"),
|
||||
"Path with special chars should be handled correctly");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexPaths() {
|
||||
// Test complex static resource paths
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/css/theme/dark/styles.css"),
|
||||
"Nested CSS should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/fonts/open-sans/bold/font.woff"),
|
||||
"Nested font should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/js/vendor/jquery/3.5.1/jquery.min.js"),
|
||||
"Versioned JS should be static");
|
||||
|
||||
// Test complex paths with context
|
||||
String contextPath = "/app";
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource(
|
||||
contextPath, contextPath + "/css/theme/dark/styles.css"),
|
||||
"Nested CSS with context should be static");
|
||||
|
||||
// Test boundary cases for isTrackableResource
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/js-framework/components"),
|
||||
"Path starting with js- should not be treated as JS resource");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/fonts-selection"),
|
||||
"Path starting with fonts- should not be treated as font resource");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Toolkit;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class UIScalingTest {
|
||||
|
||||
private MockedStatic<Toolkit> mockedToolkit;
|
||||
private Toolkit mockedDefaultToolkit;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Set up mocking of Toolkit
|
||||
mockedToolkit = mockStatic(Toolkit.class);
|
||||
mockedDefaultToolkit = Mockito.mock(Toolkit.class);
|
||||
|
||||
// Return mocked toolkit when Toolkit.getDefaultToolkit() is called
|
||||
mockedToolkit.when(Toolkit::getDefaultToolkit).thenReturn(mockedDefaultToolkit);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (mockedToolkit != null) {
|
||||
mockedToolkit.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetWidthScaleFactor() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
double scaleFactor = UIScaling.getWidthScaleFactor();
|
||||
|
||||
// Assert
|
||||
assertEquals(2.0, scaleFactor, 0.001, "Scale factor should be 2.0 for 4K width");
|
||||
verify(mockedDefaultToolkit, times(1)).getScreenSize();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetHeightScaleFactor() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
double scaleFactor = UIScaling.getHeightScaleFactor();
|
||||
|
||||
// Assert
|
||||
assertEquals(2.0, scaleFactor, 0.001, "Scale factor should be 2.0 for 4K height");
|
||||
verify(mockedDefaultToolkit, times(1)).getScreenSize();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetWidthScaleFactor_HD() {
|
||||
// Arrange - HD resolution
|
||||
Dimension screenSize = new Dimension(1920, 1080);
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
double scaleFactor = UIScaling.getWidthScaleFactor();
|
||||
|
||||
// Assert
|
||||
assertEquals(1.0, scaleFactor, 0.001, "Scale factor should be 1.0 for HD width");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetHeightScaleFactor_HD() {
|
||||
// Arrange - HD resolution
|
||||
Dimension screenSize = new Dimension(1920, 1080);
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
double scaleFactor = UIScaling.getHeightScaleFactor();
|
||||
|
||||
// Assert
|
||||
assertEquals(1.0, scaleFactor, 0.001, "Scale factor should be 1.0 for HD height");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetWidthScaleFactor_SmallScreen() {
|
||||
// Arrange - Small screen
|
||||
Dimension screenSize = new Dimension(1366, 768);
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
double scaleFactor = UIScaling.getWidthScaleFactor();
|
||||
|
||||
// Assert
|
||||
assertEquals(0.711, scaleFactor, 0.001, "Scale factor should be ~0.711 for 1366x768 width");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetHeightScaleFactor_SmallScreen() {
|
||||
// Arrange - Small screen
|
||||
Dimension screenSize = new Dimension(1366, 768);
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
double scaleFactor = UIScaling.getHeightScaleFactor();
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
0.711, scaleFactor, 0.001, "Scale factor should be ~0.711 for 1366x768 height");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleWidth() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
int scaledWidth = UIScaling.scaleWidth(100);
|
||||
|
||||
// Assert
|
||||
assertEquals(200, scaledWidth, "Width should be scaled by factor of 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleHeight() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
int scaledHeight = UIScaling.scaleHeight(100);
|
||||
|
||||
// Assert
|
||||
assertEquals(200, scaledHeight, "Height should be scaled by factor of 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleWidth_SmallScreen() {
|
||||
// Arrange - Small screen
|
||||
Dimension screenSize = new Dimension(960, 540); // Half of HD
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
int scaledWidth = UIScaling.scaleWidth(100);
|
||||
|
||||
// Assert
|
||||
assertEquals(50, scaledWidth, "Width should be scaled by factor of 0.5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleHeight_SmallScreen() {
|
||||
// Arrange - Small screen
|
||||
Dimension screenSize = new Dimension(960, 540); // Half of HD
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Act
|
||||
int scaledHeight = UIScaling.scaleHeight(100);
|
||||
|
||||
// Assert
|
||||
assertEquals(50, scaledHeight, "Height should be scaled by factor of 0.5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleDimension() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
Dimension originalDim = new Dimension(200, 150);
|
||||
|
||||
// Act
|
||||
Dimension scaledDim = UIScaling.scale(originalDim);
|
||||
|
||||
// Assert
|
||||
assertEquals(400, scaledDim.width, "Width should be scaled by factor of 2");
|
||||
assertEquals(300, scaledDim.height, "Height should be scaled by factor of 2");
|
||||
// Verify the original dimension is not modified
|
||||
assertEquals(200, originalDim.width, "Original width should remain unchanged");
|
||||
assertEquals(150, originalDim.height, "Original height should remain unchanged");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleInsets() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
Insets originalInsets = new Insets(10, 20, 30, 40);
|
||||
|
||||
// Act
|
||||
Insets scaledInsets = UIScaling.scale(originalInsets);
|
||||
|
||||
// Assert
|
||||
assertEquals(20, scaledInsets.top, "Top inset should be scaled by factor of 2");
|
||||
assertEquals(40, scaledInsets.left, "Left inset should be scaled by factor of 2");
|
||||
assertEquals(60, scaledInsets.bottom, "Bottom inset should be scaled by factor of 2");
|
||||
assertEquals(80, scaledInsets.right, "Right inset should be scaled by factor of 2");
|
||||
// Verify the original insets are not modified
|
||||
assertEquals(10, originalInsets.top, "Original top inset should remain unchanged");
|
||||
assertEquals(20, originalInsets.left, "Original left inset should remain unchanged");
|
||||
assertEquals(30, originalInsets.bottom, "Original bottom inset should remain unchanged");
|
||||
assertEquals(40, originalInsets.right, "Original right inset should remain unchanged");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleFont() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
Font originalFont = new Font("Arial", Font.PLAIN, 12);
|
||||
|
||||
// Act
|
||||
Font scaledFont = UIScaling.scaleFont(originalFont);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
24.0f, scaledFont.getSize2D(), 0.001f, "Font size should be scaled by factor of 2");
|
||||
// Font family might be substituted by the system, so we don't test it
|
||||
assertEquals(Font.PLAIN, scaledFont.getStyle(), "Font style should remain unchanged");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleFont_DifferentWidthHeightScales() {
|
||||
// Arrange - Different width and height scaling factors
|
||||
Dimension screenSize =
|
||||
new Dimension(2560, 1440); // 1.33x width, 1.33x height of base resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
Font originalFont = new Font("Arial", Font.PLAIN, 12);
|
||||
|
||||
// Act
|
||||
Font scaledFont = UIScaling.scaleFont(originalFont);
|
||||
|
||||
// Assert
|
||||
// Should use the smaller of the two scale factors, which is the same in this case
|
||||
assertEquals(
|
||||
16.0f,
|
||||
scaledFont.getSize2D(),
|
||||
0.001f,
|
||||
"Font size should be scaled by factor of 1.33");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleFont_UnevenScales() {
|
||||
// Arrange - different width and height scale factors
|
||||
Dimension screenSize = new Dimension(3840, 1080); // 2x width, 1x height
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
Font originalFont = new Font("Arial", Font.PLAIN, 12);
|
||||
|
||||
// Act
|
||||
Font scaledFont = UIScaling.scaleFont(originalFont);
|
||||
|
||||
// Assert - should use the smaller of the two scale factors (height in this case)
|
||||
assertEquals(
|
||||
12.0f,
|
||||
scaledFont.getSize2D(),
|
||||
0.001f,
|
||||
"Font size should be scaled by the smaller factor (1.0)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleIcon_NullIcon() {
|
||||
// Act
|
||||
Image result = UIScaling.scaleIcon(null, 100, 100);
|
||||
|
||||
// Assert
|
||||
assertNull(result, "Should return null for null input");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleIcon_SquareImage() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Create a mock square image
|
||||
Image mockImage = Mockito.mock(Image.class);
|
||||
when(mockImage.getWidth(null)).thenReturn(100);
|
||||
when(mockImage.getHeight(null)).thenReturn(100);
|
||||
when(mockImage.getScaledInstance(anyInt(), anyInt(), anyInt())).thenReturn(mockImage);
|
||||
|
||||
// Act
|
||||
Image result = UIScaling.scaleIcon(mockImage, 100, 100);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result, "Should return a non-null result");
|
||||
verify(mockImage).getScaledInstance(eq(200), eq(200), eq(Image.SCALE_SMOOTH));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleIcon_WideImage() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Create a mock image with a 2:1 aspect ratio (wide)
|
||||
Image mockImage = Mockito.mock(Image.class);
|
||||
when(mockImage.getWidth(null)).thenReturn(200);
|
||||
when(mockImage.getHeight(null)).thenReturn(100);
|
||||
when(mockImage.getScaledInstance(anyInt(), anyInt(), anyInt())).thenReturn(mockImage);
|
||||
|
||||
// Act
|
||||
Image result = UIScaling.scaleIcon(mockImage, 100, 100);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result, "Should return a non-null result");
|
||||
// For a wide image (2:1), the width should be twice the height to maintain aspect ratio
|
||||
verify(mockImage).getScaledInstance(anyInt(), anyInt(), eq(Image.SCALE_SMOOTH));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScaleIcon_TallImage() {
|
||||
// Arrange
|
||||
Dimension screenSize = new Dimension(3840, 2160); // 4K resolution
|
||||
when(mockedDefaultToolkit.getScreenSize()).thenReturn(screenSize);
|
||||
|
||||
// Create a mock image with a 1:2 aspect ratio (tall)
|
||||
Image mockImage = Mockito.mock(Image.class);
|
||||
when(mockImage.getWidth(null)).thenReturn(100);
|
||||
when(mockImage.getHeight(null)).thenReturn(200);
|
||||
when(mockImage.getScaledInstance(anyInt(), anyInt(), anyInt())).thenReturn(mockImage);
|
||||
|
||||
// Act
|
||||
Image result = UIScaling.scaleIcon(mockImage, 100, 100);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result, "Should return a non-null result");
|
||||
// For a tall image (1:2), the height should be twice the width to maintain aspect ratio
|
||||
verify(mockImage).getScaledInstance(anyInt(), anyInt(), eq(Image.SCALE_SMOOTH));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UrlUtilsTest {
|
||||
|
||||
@Mock private HttpServletRequest request;
|
||||
|
||||
@Test
|
||||
void testGetOrigin() {
|
||||
// Arrange
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
when(request.getServerName()).thenReturn("localhost");
|
||||
when(request.getServerPort()).thenReturn(8080);
|
||||
when(request.getContextPath()).thenReturn("/myapp");
|
||||
|
||||
// Act
|
||||
String origin = UrlUtils.getOrigin(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"http://localhost:8080/myapp", origin, "Origin URL should be correctly formatted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOriginWithHttps() {
|
||||
// Arrange
|
||||
when(request.getScheme()).thenReturn("https");
|
||||
when(request.getServerName()).thenReturn("example.com");
|
||||
when(request.getServerPort()).thenReturn(443);
|
||||
when(request.getContextPath()).thenReturn("");
|
||||
|
||||
// Act
|
||||
String origin = UrlUtils.getOrigin(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"https://example.com:443",
|
||||
origin,
|
||||
"HTTPS origin URL should be correctly formatted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOriginWithEmptyContextPath() {
|
||||
// Arrange
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
when(request.getServerName()).thenReturn("localhost");
|
||||
when(request.getServerPort()).thenReturn(8080);
|
||||
when(request.getContextPath()).thenReturn("");
|
||||
|
||||
// Act
|
||||
String origin = UrlUtils.getOrigin(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"http://localhost:8080",
|
||||
origin,
|
||||
"Origin URL with empty context path should be correct");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOriginWithSpecialCharacters() {
|
||||
// Arrange - Test with server name containing special characters
|
||||
when(request.getScheme()).thenReturn("https");
|
||||
when(request.getServerName()).thenReturn("internal-server.example-domain.com");
|
||||
when(request.getServerPort()).thenReturn(8443);
|
||||
when(request.getContextPath()).thenReturn("/app-v1.2");
|
||||
|
||||
// Act
|
||||
String origin = UrlUtils.getOrigin(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"https://internal-server.example-domain.com:8443/app-v1.2",
|
||||
origin,
|
||||
"Origin URL with special characters should be correctly formatted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOriginWithIPv4Address() {
|
||||
// Arrange
|
||||
when(request.getScheme()).thenReturn("http");
|
||||
when(request.getServerName()).thenReturn("192.168.1.100");
|
||||
when(request.getServerPort()).thenReturn(8080);
|
||||
when(request.getContextPath()).thenReturn("/app");
|
||||
|
||||
// Act
|
||||
String origin = UrlUtils.getOrigin(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"http://192.168.1.100:8080/app",
|
||||
origin,
|
||||
"Origin URL with IPv4 address should be correctly formatted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOriginWithNonStandardPort() {
|
||||
// Arrange
|
||||
when(request.getScheme()).thenReturn("https");
|
||||
when(request.getServerName()).thenReturn("example.org");
|
||||
when(request.getServerPort()).thenReturn(8443);
|
||||
when(request.getContextPath()).thenReturn("/api");
|
||||
|
||||
// Act
|
||||
String origin = UrlUtils.getOrigin(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"https://example.org:8443/api",
|
||||
origin,
|
||||
"Origin URL with non-standard port should be correctly formatted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPortAvailable() {
|
||||
// We'll use a real server socket for this test
|
||||
ServerSocket socket = null;
|
||||
int port = 12345; // Choose a port unlikely to be in use
|
||||
|
||||
try {
|
||||
// First check the port is available
|
||||
boolean initialAvailability = UrlUtils.isPortAvailable(port);
|
||||
|
||||
// Then occupy the port
|
||||
socket = new ServerSocket(port);
|
||||
|
||||
// Now check the port is no longer available
|
||||
boolean afterSocketCreation = UrlUtils.isPortAvailable(port);
|
||||
|
||||
// Assert
|
||||
assertTrue(initialAvailability, "Port should be available initially");
|
||||
assertFalse(
|
||||
afterSocketCreation, "Port should not be available after socket is created");
|
||||
|
||||
} catch (IOException e) {
|
||||
// This might happen if the port is already in use by another process
|
||||
// We'll just verify the behavior of isPortAvailable matches what we expect
|
||||
assertFalse(
|
||||
UrlUtils.isPortAvailable(port),
|
||||
"Port should not be available if exception is thrown");
|
||||
} finally {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore cleanup exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAvailablePort() {
|
||||
// We'll create a socket on a port and ensure findAvailablePort returns a different port
|
||||
ServerSocket socket = null;
|
||||
int startPort = 12346; // Choose a port unlikely to be in use
|
||||
|
||||
try {
|
||||
// Occupy the start port
|
||||
socket = new ServerSocket(startPort);
|
||||
|
||||
// Find an available port
|
||||
String availablePort = UrlUtils.findAvailablePort(startPort);
|
||||
|
||||
// Assert the returned port is not the occupied one
|
||||
assertNotEquals(
|
||||
String.valueOf(startPort),
|
||||
availablePort,
|
||||
"findAvailablePort should not return an occupied port");
|
||||
|
||||
// Verify the returned port is actually available
|
||||
int portNumber = Integer.parseInt(availablePort);
|
||||
|
||||
// Close our test socket before checking the found port
|
||||
socket.close();
|
||||
socket = null;
|
||||
|
||||
// The port should now be available
|
||||
assertTrue(
|
||||
UrlUtils.isPortAvailable(portNumber),
|
||||
"The port returned by findAvailablePort should be available");
|
||||
|
||||
} catch (IOException e) {
|
||||
// If we can't create the socket, skip this assertion
|
||||
} finally {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore cleanup exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAvailablePortWithAvailableStartPort() {
|
||||
// Find an available port without occupying any
|
||||
int startPort = 23456; // Choose a different unlikely-to-be-used port
|
||||
|
||||
// Make sure the port is available first
|
||||
if (UrlUtils.isPortAvailable(startPort)) {
|
||||
// Find an available port
|
||||
String availablePort = UrlUtils.findAvailablePort(startPort);
|
||||
|
||||
// Assert the returned port is the start port since it's available
|
||||
assertEquals(
|
||||
String.valueOf(startPort),
|
||||
availablePort,
|
||||
"findAvailablePort should return the start port if it's available");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindAvailablePortWithSequentialUsedPorts() {
|
||||
// This test checks that findAvailablePort correctly skips multiple occupied ports
|
||||
ServerSocket socket1 = null;
|
||||
ServerSocket socket2 = null;
|
||||
int startPort = 34567; // Another unlikely-to-be-used port
|
||||
|
||||
try {
|
||||
// First verify the port is available
|
||||
if (!UrlUtils.isPortAvailable(startPort)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Occupy two sequential ports
|
||||
socket1 = new ServerSocket(startPort);
|
||||
socket2 = new ServerSocket(startPort + 1);
|
||||
|
||||
// Find an available port starting from our occupied range
|
||||
String availablePort = UrlUtils.findAvailablePort(startPort);
|
||||
int foundPort = Integer.parseInt(availablePort);
|
||||
|
||||
// Should have skipped the two occupied ports
|
||||
assertTrue(
|
||||
foundPort >= startPort + 2,
|
||||
"findAvailablePort should skip sequential occupied ports");
|
||||
|
||||
// Verify the found port is actually available
|
||||
try (ServerSocket testSocket = new ServerSocket(foundPort)) {
|
||||
assertTrue(testSocket.isBound(), "The found port should be bindable");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
// Skip test if we encounter IO exceptions
|
||||
} finally {
|
||||
// Clean up resources
|
||||
try {
|
||||
if (socket1 != null && !socket1.isClosed()) socket1.close();
|
||||
if (socket2 != null && !socket2.isClosed()) socket2.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore cleanup exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPortAvailableWithPrivilegedPorts() {
|
||||
// Skip tests for privileged ports as they typically require root access
|
||||
// and results are environment-dependent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
public class WebResponseUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testBoasToWebResponse() {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
baos.write("Sample PDF content".getBytes());
|
||||
String docName = "sample.pdf";
|
||||
|
||||
ResponseEntity<byte[]> responseEntity =
|
||||
WebResponseUtils.boasToWebResponse(baos, docName);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertNotNull(responseEntity.getBody());
|
||||
|
||||
HttpHeaders headers = responseEntity.getHeaders();
|
||||
assertNotNull(headers);
|
||||
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
|
||||
assertNotNull(headers.getContentDisposition());
|
||||
// assertEquals("attachment; filename=\"sample.pdf\"",
|
||||
// headers.getContentDisposition().toString());
|
||||
|
||||
} catch (IOException e) {
|
||||
fail("Exception thrown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiPartFileToWebResponse() {
|
||||
try {
|
||||
byte[] fileContent = "Sample file content".getBytes();
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile("file", "sample.txt", "text/plain", fileContent);
|
||||
|
||||
ResponseEntity<byte[]> responseEntity =
|
||||
WebResponseUtils.multiPartFileToWebResponse(file);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertNotNull(responseEntity.getBody());
|
||||
|
||||
HttpHeaders headers = responseEntity.getHeaders();
|
||||
assertNotNull(headers);
|
||||
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
|
||||
assertNotNull(headers.getContentDisposition());
|
||||
|
||||
} catch (IOException e) {
|
||||
fail("Exception thrown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBytesToWebResponse() {
|
||||
try {
|
||||
byte[] bytes = "Sample bytes".getBytes();
|
||||
String docName = "sample.txt";
|
||||
MediaType mediaType = MediaType.TEXT_PLAIN;
|
||||
|
||||
ResponseEntity<byte[]> responseEntity =
|
||||
WebResponseUtils.bytesToWebResponse(bytes, docName, mediaType);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertNotNull(responseEntity.getBody());
|
||||
|
||||
HttpHeaders headers = responseEntity.getHeaders();
|
||||
assertNotNull(headers);
|
||||
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
|
||||
assertNotNull(headers.getContentDisposition());
|
||||
|
||||
} catch (IOException e) {
|
||||
fail("Exception thrown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdfDocToWebResponse() {
|
||||
try {
|
||||
PDDocument document = new PDDocument();
|
||||
document.addPage(new org.apache.pdfbox.pdmodel.PDPage());
|
||||
String docName = "sample.pdf";
|
||||
|
||||
ResponseEntity<byte[]> responseEntity =
|
||||
WebResponseUtils.pdfDocToWebResponse(document, docName);
|
||||
|
||||
assertNotNull(responseEntity);
|
||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||
assertNotNull(responseEntity.getBody());
|
||||
|
||||
HttpHeaders headers = responseEntity.getHeaders();
|
||||
assertNotNull(headers);
|
||||
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
|
||||
assertNotNull(headers.getContentDisposition());
|
||||
|
||||
} catch (IOException e) {
|
||||
fail("Exception thrown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
class CustomColorReplaceStrategyTest {
|
||||
|
||||
private CustomColorReplaceStrategy strategy;
|
||||
private MultipartFile mockFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Create a mock file
|
||||
mockFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "test pdf content".getBytes());
|
||||
|
||||
// Initialize strategy with custom colors
|
||||
strategy =
|
||||
new CustomColorReplaceStrategy(
|
||||
mockFile,
|
||||
ReplaceAndInvert.CUSTOM_COLOR,
|
||||
"000000", // Black text color
|
||||
"FFFFFF", // White background color
|
||||
null); // Not using high contrast combination for CUSTOM_COLOR
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor() {
|
||||
// Test the constructor sets values correctly
|
||||
assertNotNull(strategy, "Strategy should be initialized");
|
||||
assertEquals(mockFile, strategy.getFileInput(), "File input should be set correctly");
|
||||
assertEquals(
|
||||
ReplaceAndInvert.CUSTOM_COLOR,
|
||||
strategy.getReplaceAndInvert(),
|
||||
"ReplaceAndInvert should be set correctly");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckSupportedFontForCharacter() throws Exception {
|
||||
// Use reflection to access private method
|
||||
Method method =
|
||||
CustomColorReplaceStrategy.class.getDeclaredMethod(
|
||||
"checkSupportedFontForCharacter", String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
// Test with ASCII character which should be supported by standard fonts
|
||||
Object result = method.invoke(strategy, "A");
|
||||
assertNotNull(result, "Standard font should support ASCII character");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHighContrastColors() {
|
||||
// Create a new strategy with HIGH_CONTRAST_COLOR setting
|
||||
CustomColorReplaceStrategy highContrastStrategy =
|
||||
new CustomColorReplaceStrategy(
|
||||
mockFile,
|
||||
ReplaceAndInvert.HIGH_CONTRAST_COLOR,
|
||||
null, // These will be overridden by the high contrast settings
|
||||
null,
|
||||
HighContrastColorCombination.BLACK_TEXT_ON_WHITE);
|
||||
|
||||
// Verify the colors after replace() is called
|
||||
try {
|
||||
// Call replace (but we don't need the actual result for this test)
|
||||
// This will throw IOException because we're using a mock file without actual PDF
|
||||
// content
|
||||
// but it will still set the colors according to the high contrast setting
|
||||
try {
|
||||
highContrastStrategy.replace();
|
||||
} catch (IOException e) {
|
||||
// Expected exception due to mock file
|
||||
}
|
||||
|
||||
// Use reflection to access private fields
|
||||
java.lang.reflect.Field textColorField =
|
||||
CustomColorReplaceStrategy.class.getDeclaredField("textColor");
|
||||
textColorField.setAccessible(true);
|
||||
java.lang.reflect.Field backgroundColorField =
|
||||
CustomColorReplaceStrategy.class.getDeclaredField("backgroundColor");
|
||||
backgroundColorField.setAccessible(true);
|
||||
|
||||
String textColor = (String) textColorField.get(highContrastStrategy);
|
||||
String backgroundColor = (String) backgroundColorField.get(highContrastStrategy);
|
||||
|
||||
// For BLACK_TEXT_ON_WHITE, text color should be "0" and background color should be
|
||||
// "16777215"
|
||||
assertEquals("0", textColor, "Text color should be black (0)");
|
||||
assertEquals(
|
||||
"16777215", backgroundColor, "Background color should be white (16777215)");
|
||||
|
||||
} catch (Exception e) {
|
||||
// If we get here, the test failed
|
||||
org.junit.jupiter.api.Assertions.fail("Exception occurred: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class HighContrastColorReplaceDeciderTest {
|
||||
|
||||
@Test
|
||||
void testGetColors_BlackTextOnWhite() {
|
||||
// Arrange
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
|
||||
HighContrastColorCombination combination = HighContrastColorCombination.BLACK_TEXT_ON_WHITE;
|
||||
|
||||
// Act
|
||||
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
|
||||
|
||||
// Assert
|
||||
assertArrayEquals(
|
||||
new String[] {"0", "16777215"},
|
||||
colors,
|
||||
"Should return black (0) for text and white (16777215) for background");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetColors_GreenTextOnBlack() {
|
||||
// Arrange
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
|
||||
HighContrastColorCombination combination = HighContrastColorCombination.GREEN_TEXT_ON_BLACK;
|
||||
|
||||
// Act
|
||||
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
|
||||
|
||||
// Assert
|
||||
assertArrayEquals(
|
||||
new String[] {"65280", "0"},
|
||||
colors,
|
||||
"Should return green (65280) for text and black (0) for background");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetColors_WhiteTextOnBlack() {
|
||||
// Arrange
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
|
||||
HighContrastColorCombination combination = HighContrastColorCombination.WHITE_TEXT_ON_BLACK;
|
||||
|
||||
// Act
|
||||
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
|
||||
|
||||
// Assert
|
||||
assertArrayEquals(
|
||||
new String[] {"16777215", "0"},
|
||||
colors,
|
||||
"Should return white (16777215) for text and black (0) for background");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetColors_YellowTextOnBlack() {
|
||||
// Arrange
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
|
||||
HighContrastColorCombination combination =
|
||||
HighContrastColorCombination.YELLOW_TEXT_ON_BLACK;
|
||||
|
||||
// Act
|
||||
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, combination);
|
||||
|
||||
// Assert
|
||||
assertArrayEquals(
|
||||
new String[] {"16776960", "0"},
|
||||
colors,
|
||||
"Should return yellow (16776960) for text and black (0) for background");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetColors_NullForInvalidCombination() {
|
||||
// Arrange - use null for combination
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.HIGH_CONTRAST_COLOR;
|
||||
|
||||
// Act
|
||||
String[] colors = HighContrastColorReplaceDecider.getColors(replaceAndInvert, null);
|
||||
|
||||
// Assert
|
||||
assertNull(colors, "Should return null for invalid combination");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetColors_ReplaceAndInvertParameterIsIgnored() {
|
||||
// Arrange - use different ReplaceAndInvert values with the same combination
|
||||
HighContrastColorCombination combination = HighContrastColorCombination.BLACK_TEXT_ON_WHITE;
|
||||
|
||||
// Act
|
||||
String[] colors1 =
|
||||
HighContrastColorReplaceDecider.getColors(
|
||||
ReplaceAndInvert.HIGH_CONTRAST_COLOR, combination);
|
||||
String[] colors2 =
|
||||
HighContrastColorReplaceDecider.getColors(
|
||||
ReplaceAndInvert.CUSTOM_COLOR, combination);
|
||||
String[] colors3 =
|
||||
HighContrastColorReplaceDecider.getColors(
|
||||
ReplaceAndInvert.FULL_INVERSION, combination);
|
||||
|
||||
// Assert - all should return the same colors, showing that the ReplaceAndInvert parameter
|
||||
// isn't used
|
||||
assertArrayEquals(colors1, colors2, "ReplaceAndInvert parameter should be ignored");
|
||||
assertArrayEquals(colors1, colors3, "ReplaceAndInvert parameter should be ignored");
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
|
||||
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
class InvertFullColorStrategyTest {
|
||||
|
||||
private InvertFullColorStrategy strategy;
|
||||
private MultipartFile mockPdfFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
// Create a simple PDF document for testing
|
||||
byte[] pdfBytes = createSimplePdfWithRectangle();
|
||||
mockPdfFile = new MockMultipartFile("file", "test.pdf", "application/pdf", pdfBytes);
|
||||
|
||||
// Create the strategy instance
|
||||
strategy = new InvertFullColorStrategy(mockPdfFile, ReplaceAndInvert.FULL_INVERSION);
|
||||
}
|
||||
|
||||
/** Helper method to create a simple PDF with a colored rectangle for testing */
|
||||
private byte[] createSimplePdfWithRectangle() throws IOException {
|
||||
PDDocument document = new PDDocument();
|
||||
PDPage page = new PDPage(PDRectangle.A4);
|
||||
document.addPage(page);
|
||||
|
||||
// Add a filled rectangle with a specific color
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page);
|
||||
contentStream.setNonStrokingColor(
|
||||
new PDColor(new float[] {0.8f, 0.2f, 0.2f}, PDDeviceRGB.INSTANCE));
|
||||
contentStream.addRect(100, 100, 400, 400);
|
||||
contentStream.fill();
|
||||
contentStream.close();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
document.close();
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReplace() throws IOException {
|
||||
// Test the replace method
|
||||
InputStreamResource result = strategy.replace();
|
||||
|
||||
// Verify that the result is not null
|
||||
assertNotNull(result, "The result should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvertImageColors()
|
||||
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
// Create a test image with known colors
|
||||
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
java.awt.Graphics graphics = image.getGraphics();
|
||||
graphics.setColor(new Color(200, 100, 50)); // RGB color to be inverted
|
||||
graphics.fillRect(0, 0, 10, 10);
|
||||
graphics.dispose();
|
||||
|
||||
// Get the color of a pixel before inversion
|
||||
Color originalColor = new Color(image.getRGB(5, 5), true);
|
||||
|
||||
// Access private method using reflection
|
||||
Method invertMethodRef =
|
||||
InvertFullColorStrategy.class.getDeclaredMethod(
|
||||
"invertImageColors", BufferedImage.class);
|
||||
invertMethodRef.setAccessible(true);
|
||||
|
||||
// Invoke the private method
|
||||
invertMethodRef.invoke(strategy, image);
|
||||
|
||||
// Get the color of the same pixel after inversion
|
||||
Color invertedColor = new Color(image.getRGB(5, 5), true);
|
||||
|
||||
// Assert that the inversion worked correctly
|
||||
assertEquals(
|
||||
255 - originalColor.getRed(),
|
||||
invertedColor.getRed(),
|
||||
"Red channel should be inverted");
|
||||
assertEquals(
|
||||
255 - originalColor.getGreen(),
|
||||
invertedColor.getGreen(),
|
||||
"Green channel should be inverted");
|
||||
assertEquals(
|
||||
255 - originalColor.getBlue(),
|
||||
invertedColor.getBlue(),
|
||||
"Blue channel should be inverted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToBufferedImageTpFile()
|
||||
throws NoSuchMethodException,
|
||||
InvocationTargetException,
|
||||
IllegalAccessException,
|
||||
IOException {
|
||||
// Create a test image
|
||||
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
// Access private method using reflection
|
||||
Method convertMethodRef =
|
||||
InvertFullColorStrategy.class.getDeclaredMethod(
|
||||
"convertToBufferedImageTpFile", BufferedImage.class);
|
||||
convertMethodRef.setAccessible(true);
|
||||
|
||||
// Invoke the private method
|
||||
File result = (File) convertMethodRef.invoke(strategy, image);
|
||||
|
||||
try {
|
||||
// Assert that the file exists and is not empty
|
||||
assertNotNull(result, "Result should not be null");
|
||||
assertTrue(result.exists(), "File should exist");
|
||||
assertTrue(result.length() > 0, "File should not be empty");
|
||||
|
||||
// Check that the file can be read back as an image
|
||||
BufferedImage readBack = ImageIO.read(result);
|
||||
assertNotNull(readBack, "Should be able to read back the image");
|
||||
assertEquals(10, readBack.getWidth(), "Image width should match");
|
||||
assertEquals(10, readBack.getHeight(), "Image height should match");
|
||||
} finally {
|
||||
// Clean up
|
||||
if (result != null && result.exists()) {
|
||||
Files.delete(result.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PdfTextStripperCustomTest {
|
||||
|
||||
private PdfTextStripperCustom stripper;
|
||||
private PDPage mockPage;
|
||||
private PDRectangle mockMediaBox;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
// Create the stripper instance
|
||||
stripper = new PdfTextStripperCustom();
|
||||
|
||||
// Create mock objects
|
||||
mockPage = mock(PDPage.class);
|
||||
mockMediaBox = mock(PDRectangle.class);
|
||||
|
||||
// Configure mock behavior
|
||||
when(mockPage.getMediaBox()).thenReturn(mockMediaBox);
|
||||
when(mockMediaBox.getLowerLeftX()).thenReturn(0f);
|
||||
when(mockMediaBox.getLowerLeftY()).thenReturn(0f);
|
||||
when(mockMediaBox.getWidth()).thenReturn(612f);
|
||||
when(mockMediaBox.getHeight()).thenReturn(792f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor() throws IOException {
|
||||
// Verify that constructor doesn't throw an exception
|
||||
PdfTextStripperCustom newStripper = new PdfTextStripperCustom();
|
||||
assertNotNull(newStripper, "Constructor should create a non-null instance");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBasicFunctionality() throws IOException {
|
||||
// Simply test that the method runs without exceptions
|
||||
try {
|
||||
stripper.addRegion("testRegion", new java.awt.geom.Rectangle2D.Float(0, 0, 100, 100));
|
||||
stripper.extractRegions(mockPage);
|
||||
assertTrue(true, "Should execute without errors");
|
||||
} catch (Exception e) {
|
||||
assertTrue(false, "Method should not throw exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
|
||||
class ReplaceAndInvertColorStrategyTest {
|
||||
|
||||
// A concrete implementation of the abstract class for testing
|
||||
private static class ConcreteReplaceAndInvertColorStrategy
|
||||
extends ReplaceAndInvertColorStrategy {
|
||||
|
||||
public ConcreteReplaceAndInvertColorStrategy(
|
||||
MultipartFile file, ReplaceAndInvert replaceAndInvert) {
|
||||
super(file, replaceAndInvert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStreamResource replace() throws IOException {
|
||||
// Simple implementation for testing purposes
|
||||
return new InputStreamResource(getFileInput().getInputStream());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor() {
|
||||
// Arrange
|
||||
MultipartFile mockFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "test content".getBytes());
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
|
||||
|
||||
// Act
|
||||
ReplaceAndInvertColorStrategy strategy =
|
||||
new ConcreteReplaceAndInvertColorStrategy(mockFile, replaceAndInvert);
|
||||
|
||||
// Assert
|
||||
assertNotNull(strategy, "Strategy should be initialized");
|
||||
assertEquals(mockFile, strategy.getFileInput(), "File input should be set correctly");
|
||||
assertEquals(
|
||||
replaceAndInvert,
|
||||
strategy.getReplaceAndInvert(),
|
||||
"ReplaceAndInvert option should be set correctly");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReplace() throws IOException {
|
||||
// Arrange
|
||||
byte[] content = "test pdf content".getBytes();
|
||||
MultipartFile mockFile =
|
||||
new MockMultipartFile("file", "test.pdf", "application/pdf", content);
|
||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
|
||||
|
||||
ReplaceAndInvertColorStrategy strategy =
|
||||
new ConcreteReplaceAndInvertColorStrategy(mockFile, replaceAndInvert);
|
||||
|
||||
// Act
|
||||
InputStreamResource result = strategy.replace();
|
||||
|
||||
// Assert
|
||||
assertNotNull(result, "Result should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGettersAndSetters() {
|
||||
// Arrange
|
||||
MultipartFile mockFile1 =
|
||||
new MockMultipartFile(
|
||||
"file1", "test1.pdf", "application/pdf", "content1".getBytes());
|
||||
MultipartFile mockFile2 =
|
||||
new MockMultipartFile(
|
||||
"file2", "test2.pdf", "application/pdf", "content2".getBytes());
|
||||
|
||||
// Act
|
||||
ReplaceAndInvertColorStrategy strategy =
|
||||
new ConcreteReplaceAndInvertColorStrategy(mockFile1, ReplaceAndInvert.CUSTOM_COLOR);
|
||||
|
||||
// Test initial values
|
||||
assertEquals(mockFile1, strategy.getFileInput());
|
||||
assertEquals(ReplaceAndInvert.CUSTOM_COLOR, strategy.getReplaceAndInvert());
|
||||
|
||||
// Test setters
|
||||
strategy.setFileInput(mockFile2);
|
||||
strategy.setReplaceAndInvert(ReplaceAndInvert.FULL_INVERSION);
|
||||
|
||||
// Assert new values
|
||||
assertEquals(mockFile2, strategy.getFileInput());
|
||||
assertEquals(ReplaceAndInvert.FULL_INVERSION, strategy.getReplaceAndInvert());
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package stirling.software.common.util.propertyeditor;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import stirling.software.common.model.api.security.RedactionArea;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class StringToArrayListPropertyEditorTest {
|
||||
|
||||
private StringToArrayListPropertyEditor editor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
editor = new StringToArrayListPropertyEditor();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_ValidJson() {
|
||||
// Arrange
|
||||
String json =
|
||||
"[{\"x\":10.5,\"y\":20.5,\"width\":100.0,\"height\":50.0,\"page\":1,\"color\":\"#FF0000\"}]";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof List, "Value should be a List");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||
assertEquals(1, list.size(), "List should have 1 entry");
|
||||
|
||||
RedactionArea area = list.get(0);
|
||||
assertEquals(10.5, area.getX(), "X should be 10.5");
|
||||
assertEquals(20.5, area.getY(), "Y should be 20.5");
|
||||
assertEquals(100.0, area.getWidth(), "Width should be 100.0");
|
||||
assertEquals(50.0, area.getHeight(), "Height should be 50.0");
|
||||
assertEquals(1, area.getPage(), "Page should be 1");
|
||||
assertEquals("#FF0000", area.getColor(), "Color should be #FF0000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_MultipleItems() {
|
||||
// Arrange
|
||||
String json =
|
||||
"["
|
||||
+ "{\"x\":10.0,\"y\":20.0,\"width\":100.0,\"height\":50.0,\"page\":1,\"color\":\"#FF0000\"},"
|
||||
+ "{\"x\":30.0,\"y\":40.0,\"width\":200.0,\"height\":150.0,\"page\":2,\"color\":\"#00FF00\"}"
|
||||
+ "]";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof List, "Value should be a List");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||
assertEquals(2, list.size(), "List should have 2 entries");
|
||||
|
||||
RedactionArea area1 = list.get(0);
|
||||
assertEquals(10.0, area1.getX(), "X should be 10.0");
|
||||
assertEquals(20.0, area1.getY(), "Y should be 20.0");
|
||||
assertEquals(1, area1.getPage(), "Page should be 1");
|
||||
|
||||
RedactionArea area2 = list.get(1);
|
||||
assertEquals(30.0, area2.getX(), "X should be 30.0");
|
||||
assertEquals(40.0, area2.getY(), "Y should be 40.0");
|
||||
assertEquals(2, area2.getPage(), "Page should be 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_EmptyString() {
|
||||
// Arrange
|
||||
String json = "";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof List, "Value should be a List");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||
assertTrue(list.isEmpty(), "List should be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_NullString() {
|
||||
// Act
|
||||
editor.setAsText(null);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof List, "Value should be a List");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||
assertTrue(list.isEmpty(), "List should be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_SingleItemAsArray() {
|
||||
// Arrange - note this is a single object, not an array
|
||||
String json =
|
||||
"{\"x\":10.0,\"y\":20.0,\"width\":100.0,\"height\":50.0,\"page\":1,\"color\":\"#FF0000\"}";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof List, "Value should be a List");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||
assertEquals(1, list.size(), "List should have 1 entry");
|
||||
|
||||
RedactionArea area = list.get(0);
|
||||
assertEquals(10.0, area.getX(), "X should be 10.0");
|
||||
assertEquals(20.0, area.getY(), "Y should be 20.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_InvalidJson() {
|
||||
// Arrange
|
||||
String json = "invalid json";
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_InvalidStructure() {
|
||||
// Arrange - this JSON doesn't match RedactionArea structure
|
||||
String json = "[{\"invalid\":\"structure\"}]";
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package stirling.software.common.util.propertyeditor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StringToMapPropertyEditorTest {
|
||||
|
||||
private StringToMapPropertyEditor editor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
editor = new StringToMapPropertyEditor();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_ValidJson() {
|
||||
// Arrange
|
||||
String json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof Map, "Value should be a Map");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> map = (Map<String, String>) value;
|
||||
assertEquals(2, map.size(), "Map should have 2 entries");
|
||||
assertEquals("value1", map.get("key1"), "First entry should be key1=value1");
|
||||
assertEquals("value2", map.get("key2"), "Second entry should be key2=value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_EmptyJson() {
|
||||
// Arrange
|
||||
String json = "{}";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof Map, "Value should be a Map");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> map = (Map<String, String>) value;
|
||||
assertTrue(map.isEmpty(), "Map should be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_WhitespaceJson() {
|
||||
// Arrange
|
||||
String json = " { \"key1\" : \"value1\" } ";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof Map, "Value should be a Map");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> map = (Map<String, String>) value;
|
||||
assertEquals(1, map.size(), "Map should have 1 entry");
|
||||
assertEquals("value1", map.get("key1"), "Entry should be key1=value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_NestedJson() {
|
||||
// Arrange
|
||||
String json = "{\"key1\":\"value1\",\"key2\":\"{\\\"nestedKey\\\":\\\"nestedValue\\\"}\"}";
|
||||
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertTrue(value instanceof Map, "Value should be a Map");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> map = (Map<String, String>) value;
|
||||
assertEquals(2, map.size(), "Map should have 2 entries");
|
||||
assertEquals("value1", map.get("key1"), "First entry should be key1=value1");
|
||||
assertEquals(
|
||||
"{\"nestedKey\":\"nestedValue\"}",
|
||||
map.get("key2"),
|
||||
"Second entry should be the nested JSON as a string");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_InvalidJson() {
|
||||
// Arrange
|
||||
String json = "{invalid json}";
|
||||
|
||||
// Act & Assert
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
|
||||
|
||||
assertEquals(
|
||||
"Failed to convert java.lang.String to java.util.Map",
|
||||
exception.getMessage(),
|
||||
"Exception message should match expected error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_Null() {
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(null));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user