Util.java

package com.cloudforgeci.api.core;

import com.cloudforge.core.interfaces.FrameworkRules;
import com.cloudforgeci.api.core.rules.FrameworkLoader;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;


class Util {
    private static final Logger LOG = Logger.getLogger(Util.class.getName());

    private static ObjectMapper getMapper() {
        return new ObjectMapper();
    }

    /**
     * Creates a DeploymentContext from the 'cfc' context object.
     *
     * <p>This is the entry point for context creation and framework config merging.
     * (Renamed from {@code extractDeploymentContext} for clarity.)</p>
     *
     * @param cfc the raw context object (Map, JSON string, or POJO)
     * @return a fully constructed DeploymentContext with merged framework config
     */
    public static DeploymentContext createDeploymentContext(Object cfc) {
        Map<String, Object> map = convertToContext(cfc);

        // Merge framework-required configuration before constructing DeploymentContext
        map = mergeFrameworkConfiguration(map);

        return new DeploymentContext(map);
    }

    /**
     * @deprecated Use {@link #createDeploymentContext(Object)} instead.
     */
    @Deprecated
    public static DeploymentContext extractDeploymentContext(Object cfc) {
        return createDeploymentContext(cfc);
    }

    /**
     * Merge compliance framework configuration requirements into the deployment context.
     *
     * <p><b>Precedence order:</b></p>
     * <ol>
     *   <li>User-provided explicit configuration (cdk.json)</li>
     *   <li>Framework-required configuration (from getRequiredConfiguration())</li>
     *   <li>Security profile defaults (applied later in SecurityProfileConfiguration)</li>
     * </ol>
     *
     * @param userConfig user-provided configuration map
     * @return merged configuration map with framework requirements
     */
    private static Map<String, Object> mergeFrameworkConfiguration(Map<String, Object> userConfig) {
        // Get enabled frameworks from user config. convertToContext(...) may legitimately
        // materialize this as a List (e.g. from a raw JSON array, "complianceFrameworks":
        // ["soc2", "hipaa"]) rather than the comma-joined string form the rest of this pipeline
        // expects (ComplianceFrameworkType.toCommaSeparated's own canonical shape) — a blind
        // (String) cast throws ClassCastException on that otherwise-valid input, before
        // DeploymentConfig.fromMap(...) ever gets a chance to normalize it.
        Object frameworksValue = userConfig.get("complianceFrameworks");
        String frameworksStr = frameworksValue instanceof List<?> list
            ? list.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(","))
            : (String) frameworksValue;
        if (frameworksStr == null || frameworksStr.trim().isEmpty()) {
            return userConfig; // No frameworks enabled, return as-is
        }

        // Parse enabled framework IDs
        String[] frameworkIds = frameworksStr.split(",");

        // Discover all available frameworks
        List<FrameworkRules<SystemContext>> allFrameworks = FrameworkLoader.discover();

        // Collect configuration from enabled frameworks
        Map<String, Object> frameworkConfig = new HashMap<>();
        for (FrameworkRules<SystemContext> framework : allFrameworks) {
            String frameworkId = framework.frameworkId();

            // Check if this framework is enabled (case-insensitive match)
            boolean isEnabled = false;
            for (String enabledId : frameworkIds) {
                if (enabledId.trim().equalsIgnoreCase(frameworkId)) {
                    isEnabled = true;
                    break;
                }
            }

            if (isEnabled) {
                Map<String, Object> required = framework.getRequiredConfiguration();
                if (required != null && !required.isEmpty()) {
                    LOG.info("Applying configuration from " + framework.displayName() +
                            " (" + frameworkId + "): " + required);

                    // Merge framework config (later frameworks override earlier ones if conflicts)
                    frameworkConfig.putAll(required);
                }
            }
        }

        // Merge: user config takes precedence over framework config
        Map<String, Object> merged = new HashMap<>(frameworkConfig);
        merged.putAll(userConfig); // User values override framework values

        // Log what was applied
        if (!frameworkConfig.isEmpty()) {
            frameworkConfig.forEach((key, frameworkValue) -> {
                Object userValue = userConfig.get(key);
                if (userValue != null && !userValue.equals(frameworkValue)) {
                    LOG.info("  " + key + ": Framework default (" + frameworkValue +
                            ") overridden by user config (" + userValue + ")");
                } else if (userValue == null) {
                    LOG.info("  " + key + ": Applying framework default (" + frameworkValue + ")");
                }
            });
        }

        return merged;
    }

    @SuppressWarnings("unchecked")
    private static Map<String, Object> convertToContext(Object obj) {
        if (obj == null) return java.util.Collections.emptyMap();

        if (obj instanceof Map<?, ?> m) {
            Map<String, Object> out = new java.util.HashMap<>();
            m.forEach((k, v) -> out.put(String.valueOf(k), v));
            return out;
        }

        if (obj instanceof String s) {
            String json = s.trim();
            if (json.isEmpty()) return java.util.Collections.emptyMap();
            // A leading '@' means "read the JSON from this file" instead of treating the string
            // itself as JSON (matches the convention `cdk synth --context cfc=@file.json`
            // callers expect, e.g. comprehensive-synth-test.sh) -- resolved relative to the
            // JVM's working directory, same as where `cdk synth` itself was invoked from.
            if (json.startsWith("@")) {
                java.nio.file.Path path = java.nio.file.Path.of(json.substring(1));
                try {
                    json = java.nio.file.Files.readString(path).trim();
                } catch (java.io.IOException e) {
                    throw new RuntimeException("Failed to read context file: " + path, e);
                }
            }
            try {
                return getMapper().readValue(json, new TypeReference<Map<String, Object>>() {});
            } catch (Exception e) {
                throw new RuntimeException("Failed to parse context JSON: " + json, e);
            }
        }

        return getMapper().convertValue(obj, new TypeReference<Map<String, Object>>() {});
    }
}