Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved Configuration and YAML Management #2966

Merged
merged 14 commits into from
Feb 25, 2025
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ dependencies {
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
implementation 'com.coveo:saml-client:5.0.0'


}
implementation 'org.snakeyaml:snakeyaml-engine:2.9'

testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private void checkLicense() {

public void updateLicenseKey(String newKey) throws IOException {
applicationProperties.getEnterpriseEdition().setKey(newKey);
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
checkLicense();
}

Expand Down
25 changes: 14 additions & 11 deletions src/main/java/stirling/software/SPDF/SPDFApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,27 +83,30 @@ public static void main(String[] args) throws IOException, InterruptedException
Map<String, String> propertyFiles = new HashMap<>();

// External config files
String settingsPath = InstallationPathConfig.getSettingsPath();
log.info("Settings file: {}", settingsPath);
if (Files.exists(Paths.get(settingsPath))) {
propertyFiles.put("spring.config.additional-location", "file:" + settingsPath);
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
log.info("Settings file: {}", settingsPath.toString());
if (Files.exists(settingsPath)) {
propertyFiles.put(
"spring.config.additional-location", "file:" + settingsPath.toString());
} else {
log.warn("External configuration file '{}' does not exist.", settingsPath);
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
}

String customSettingsPath = InstallationPathConfig.getCustomSettingsPath();
log.info("Custom settings file: {}", customSettingsPath);
if (Files.exists(Paths.get(customSettingsPath))) {
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
log.info("Custom settings file: {}", customSettingsPath.toString());
if (Files.exists(customSettingsPath)) {
String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", "");
if (!existingLocation.isEmpty()) {
existingLocation += ",";
}
propertyFiles.put(
"spring.config.additional-location",
existingLocation + "file:" + customSettingsPath);
existingLocation + "file:" + customSettingsPath.toString());
} else {
log.warn("Custom configuration file '{}' does not exist.", customSettingsPath);
log.warn(
"Custom configuration file '{}' does not exist.",
customSettingsPath.toString());
}
Properties finalProps = new Properties();

Expand Down Expand Up @@ -154,7 +157,7 @@ public void init() {
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
} catch (IOException e) {
log.error("Error opening browser: {}", e.getMessage());
}
}
Expand Down
12 changes: 5 additions & 7 deletions src/main/java/stirling/software/SPDF/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ public boolean enableAlphaFunctionality() {

@Bean(name = "rateLimit")
public boolean rateLimit() {
String appName = System.getProperty("rateLimit");
if (appName == null) appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false;
String rateLimit = System.getProperty("rateLimit");
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
}

@Bean(name = "RunningInDocker")
Expand Down Expand Up @@ -170,16 +170,14 @@ public String accessibilityStatement() {
@Bean(name = "analyticsPrompt")
@Scope("request")
public boolean analyticsPrompt() {
return applicationProperties.getSystem().getEnableAnalytics() == null
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
return applicationProperties.getSystem().getEnableAnalytics() == null;
}

@Bean(name = "analyticsEnabled")
@Scope("request")
public boolean analyticsEnabled() {
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
return applicationProperties.getSystem().getEnableAnalytics() != null
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
return applicationProperties.getSystem().isAnalyticsEnabled();
}

@Bean(name = "StirlingPDFLabel")
Expand Down
157 changes: 14 additions & 143 deletions src/main/java/stirling/software/SPDF/config/ConfigInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;

import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -37,7 +36,6 @@ public void ensureConfigExists() throws IOException, URISyntaxException {
log.info("Created settings file from template");
} else {
// 2) Merge existing file with the template
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
URL templateResource = getClass().getClassLoader().getResource("settings.yml.template");
if (templateResource == null) {
throw new IOException("Resource not found: settings.yml.template");
Expand All @@ -49,160 +47,33 @@ public void ensureConfigExists() throws IOException, URISyntaxException {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
}

// 2a) Read lines from both files
List<String> templateLines = Files.readAllLines(tempTemplatePath);
List<String> mainLines = Files.readAllLines(settingsPath);
// 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);
}

// 2b) Merge lines
List<String> mergedLines = mergeYamlLinesWithTemplate(templateLines, mainLines);
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
YamlHelper settingsFile = new YamlHelper(settingTempPath);

// 2c) Only write if there's an actual difference
if (!mergedLines.equals(mainLines)) {
Files.write(settingsPath, mergedLines);
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.exists(customSettingsPath)) {
if (Files.notExists(customSettingsPath)) {
Files.createFile(customSettingsPath);
log.info("Created custom_settings file: {}", customSettingsPath.toString());
}
}

/**
* Merge logic that: - Reads the template lines block-by-block (where a "block" = a key and all
* the lines that belong to it), - If the main file has that key, we keep the main file's block
* (preserving whitespace + inline comments). - Otherwise, we insert the template's block. - We
* also remove keys from main that no longer exist in the template.
*
* @param templateLines lines from settings.yml.template
* @param mainLines lines from the existing settings.yml
* @return merged lines
*/
private List<String> mergeYamlLinesWithTemplate(
List<String> templateLines, List<String> mainLines) {

// 1) Parse template lines into an ordered map: path -> Block
LinkedHashMap<String, Block> templateBlocks = parseYamlBlocks(templateLines);

// 2) Parse main lines into a map: path -> Block
LinkedHashMap<String, Block> mainBlocks = parseYamlBlocks(mainLines);

// 3) Build the final list by iterating template blocks in order
List<String> merged = new ArrayList<>();
for (Map.Entry<String, Block> entry : templateBlocks.entrySet()) {
String path = entry.getKey();
Block templateBlock = entry.getValue();

if (mainBlocks.containsKey(path)) {
// If main has the same block, prefer main's lines
merged.addAll(mainBlocks.get(path).lines);
} else {
// Otherwise, add the template block
merged.addAll(templateBlock.lines);
}
}

return merged;
}

/**
* Parse a list of lines into a map of "path -> Block" where "Block" is all lines that belong to
* that key (including subsequent indented lines). Very naive approach that may not work with
* advanced YAML.
*/
private LinkedHashMap<String, Block> parseYamlBlocks(List<String> lines) {
LinkedHashMap<String, Block> blocks = new LinkedHashMap<>();

Block currentBlock = null;
String currentPath = null;

for (String line : lines) {
if (isLikelyKeyLine(line)) {
// Found a new "key: ..." line
if (currentBlock != null && currentPath != null) {
blocks.put(currentPath, currentBlock);
}
currentBlock = new Block();
currentBlock.lines.add(line);
currentPath = computePathForLine(line);
} else {
// Continuation of current block (comments, blank lines, sub-lines)
if (currentBlock == null) {
// If file starts with comments/blank lines, treat as "header block" with path
// ""
currentBlock = new Block();
currentPath = "";
}
currentBlock.lines.add(line);
}
}

if (currentBlock != null && currentPath != null) {
blocks.put(currentPath, currentBlock);
}

return blocks;
}

/**
* Checks if the line is likely "key:" or "key: value", ignoring comments/blank. Skips lines
* starting with "-" or "#".
*/
private boolean isLikelyKeyLine(String line) {
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
return false;
}
int colonIdx = trimmed.indexOf(':');
return (colonIdx > 0); // someKey:
}

// For a line like "security: ", returns "security" or "security.enableLogin"
// by looking at indentation. Very naive.
private static final Deque<String> pathStack = new ArrayDeque<>();
private static int currentIndentLevel = 0;

private String computePathForLine(String line) {
// count leading spaces
int leadingSpaces = 0;
for (char c : line.toCharArray()) {
if (c == ' ') leadingSpaces++;
else break;
}
// assume 2 spaces = 1 indent
int indentLevel = leadingSpaces / 2;

String trimmed = line.trim();
int colonIdx = trimmed.indexOf(':');
String keyName = trimmed.substring(0, colonIdx).trim();

// pop stack until we match the new indent level
while (currentIndentLevel >= indentLevel && !pathStack.isEmpty()) {
pathStack.pop();
currentIndentLevel--;
}

// push the new key
pathStack.push(keyName);
currentIndentLevel = indentLevel;

// build path by reversing the stack
String[] arr = pathStack.toArray(new String[0]);
List<String> reversed = Arrays.asList(arr);
Collections.reverse(reversed);
return String.join(".", reversed);
}

/**
* Simple holder for the lines that comprise a "block" (i.e. a key and its subsequent lines).
*/
private static class Block {
List<String> lines = new ArrayList<>();
}
}
14 changes: 7 additions & 7 deletions src/main/java/stirling/software/SPDF/config/InitialSetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void initUUIDKey() throws IOException {
if (!GeneralUtils.isValidUUID(uuid)) {
// Generating a random UUID as the secret key
uuid = UUID.randomUUID().toString();
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.UUID", uuid);
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
}
}
Expand All @@ -54,7 +54,7 @@ public void initSecretKey() throws IOException {
if (!GeneralUtils.isValidUUID(secretKey)) {
// Generating a random UUID as the secret key
secretKey = UUID.randomUUID().toString();
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.key", secretKey);
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
}
}
Expand All @@ -64,8 +64,8 @@ public void initEnableCSRFSecurity() throws IOException {
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
if (!csrf) {
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
GeneralUtils.saveKeyToSettings("security.csrfDisabled", false);
GeneralUtils.saveKeyToSettings("system.enableAnalytics", true);
applicationProperties.getSecurity().setCsrfDisabled(false);
}
}
Expand All @@ -76,14 +76,14 @@ public void initLegalUrls() throws IOException {
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
if (StringUtils.isEmpty(termsUrl)) {
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
GeneralUtils.saveKeyToSettings("legal.termsAndConditions", defaultTermsUrl);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
}
// Initialize Privacy Policy
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) {
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false);
GeneralUtils.saveKeyToSettings("legal.privacyPolicy", defaultPrivacyUrl);
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
}
}
Expand All @@ -97,7 +97,7 @@ public void initSetAppVersion() throws IOException {
appVersion = props.getProperty("version");
} catch (Exception e) {
}
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.appVersion", appVersion);
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
}
}
Loading
Loading