From f9d36b985ae39796b69193fa1eec5f5298bb31f7 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Mon, 28 Jul 2025 11:58:10 +0100 Subject: [PATCH] change file to be json, added delta endpoint --- .../api/AdminSettingsController.java | 137 ++++++++++++++++-- 1 file changed, 125 insertions(+), 12 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java index cec30f535..452323526 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.util.HtmlUtils; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -66,19 +68,21 @@ public class AdminSettingsController { @GetMapping("/file") @Operation( - summary = "Get settings file content", + summary = "Get settings file as JSON", description = - "Retrieve the raw settings.yml file content showing the latest saved values (after restart). Admin access required.") + "Retrieve the settings.yml file parsed as JSON, showing the latest saved values (after restart). Comments and formatting are ignored. Admin access required.") @ApiResponses( value = { @ApiResponse( responseCode = "200", - description = "Settings file retrieved successfully"), + description = "Settings file retrieved and parsed successfully"), @ApiResponse(responseCode = "404", description = "Settings file not found"), @ApiResponse( responseCode = "403", description = "Access denied - Admin role required"), - @ApiResponse(responseCode = "500", description = "Failed to read settings file") + @ApiResponse( + responseCode = "500", + description = "Failed to read or parse settings file") }) public ResponseEntity getSettingsFile() { try { @@ -87,18 +91,19 @@ public class AdminSettingsController { return ResponseEntity.notFound().build(); } - String fileContent = Files.readString(settingsPath); - log.debug("Admin requested settings file content"); + // Parse YAML file to JSON + ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + ObjectMapper jsonMapper = new ObjectMapper(); - // Return as JSON with the file content - Map response = - Map.of("filePath", settingsPath.toString(), "content", fileContent); - return ResponseEntity.ok(response); + Object yamlData = yamlMapper.readValue(settingsPath.toFile(), Object.class); + + log.debug("Admin requested settings file as JSON"); + return ResponseEntity.ok(yamlData); } catch (IOException e) { - log.error("Failed to read settings file: {}", e.getMessage(), e); + log.error("Failed to read or parse settings file: {}", e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to read settings file: " + e.getMessage()); + .body("Failed to read or parse settings file: " + e.getMessage()); } catch (Exception e) { log.error("Unexpected error reading settings file: {}", e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) @@ -106,6 +111,68 @@ public class AdminSettingsController { } } + @GetMapping("/delta") + @Operation( + summary = "Get settings delta (pending changes)", + description = + "Compare current runtime settings with saved file settings to show pending changes that will take effect after restart. Admin access required.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Settings delta retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Settings file not found"), + @ApiResponse( + responseCode = "403", + description = "Access denied - Admin role required"), + @ApiResponse( + responseCode = "500", + description = "Failed to calculate settings delta") + }) + public ResponseEntity getSettingsDelta() { + try { + Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); + if (!Files.exists(settingsPath)) { + return ResponseEntity.notFound().build(); + } + + // Get current runtime settings as JSON + Map runtimeSettings = + objectMapper.convertValue(applicationProperties, Map.class); + + // Parse YAML file to get saved settings + ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + Object fileData = yamlMapper.readValue(settingsPath.toFile(), Object.class); + Map savedSettings = objectMapper.convertValue(fileData, Map.class); + + // Calculate differences + Map delta = new HashMap<>(); + Map pendingChanges = new HashMap<>(); + Map currentValues = new HashMap<>(); + + findDifferences("", runtimeSettings, savedSettings, pendingChanges, currentValues); + + delta.put( + "pendingChanges", pendingChanges); // Values that will take effect after restart + delta.put("currentValues", currentValues); // Current runtime values + delta.put("hasPendingChanges", !pendingChanges.isEmpty()); + + log.debug( + "Admin requested settings delta - found {} pending changes", + pendingChanges.size()); + return ResponseEntity.ok(delta); + + } catch (IOException e) { + log.error("Failed to calculate settings delta: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to calculate settings delta: " + e.getMessage()); + } catch (Exception e) { + log.error("Unexpected error calculating settings delta: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Unexpected error calculating settings delta"); + } + } + @PutMapping @Operation( summary = "Update application settings (delta updates)", @@ -489,4 +556,50 @@ public class AdminSettingsController { throw new NoSuchFieldException("Property not accessible: " + propertyPath); } } + + /** Recursively compare two maps to find differences between runtime and saved settings */ + @SuppressWarnings("unchecked") + private void findDifferences( + String keyPrefix, + Map runtime, + Map saved, + Map pendingChanges, + Map currentValues) { + + // Check all keys in saved settings (these are the pending changes) + for (Map.Entry entry : saved.entrySet()) { + String key = entry.getKey(); + String fullKey = keyPrefix.isEmpty() ? key : keyPrefix + "." + key; + Object savedValue = entry.getValue(); + Object runtimeValue = runtime.get(key); + + if (savedValue instanceof Map && runtimeValue instanceof Map) { + // Recursively check nested objects + findDifferences( + fullKey, + (Map) runtimeValue, + (Map) savedValue, + pendingChanges, + currentValues); + } else { + // Compare values - if they're different, savedValue is pending + if (!java.util.Objects.equals(runtimeValue, savedValue)) { + pendingChanges.put(fullKey, savedValue); + currentValues.put(fullKey, runtimeValue); + } + } + } + + // Check for keys that exist in runtime but not in saved (these would be removed on restart) + for (Map.Entry entry : runtime.entrySet()) { + String key = entry.getKey(); + String fullKey = keyPrefix.isEmpty() ? key : keyPrefix + "." + key; + + if (!saved.containsKey(key)) { + // This runtime setting would be lost on restart + pendingChanges.put(fullKey + " (will be removed)", null); + currentValues.put(fullKey, entry.getValue()); + } + } + } }