mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-12 19:58:21 +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,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user