ControlMapping.java

1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright 2026 Egothor
3
// Copyright 2026 Accenture
4
package org.egothor.methodatlas.coverage;
5
6
import java.io.IOException;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.Collections;
10
import java.util.LinkedHashMap;
11
import java.util.List;
12
import java.util.Map;
13
14
import tools.jackson.core.JacksonException;
15
import tools.jackson.databind.DeserializationFeature;
16
import tools.jackson.databind.JsonNode;
17
import tools.jackson.databind.ObjectMapper;
18
import tools.jackson.databind.json.JsonMapper;
19
20
/**
21
 * User-authored mapping from taxonomy tags to compliance-control requirement
22
 * IDs, loaded from JSON.
23
 *
24
 * <p>
25
 * The mapping is the user's responsibility; the tool records what the file
26
 * says and does not pass judgement on compliance claims. Validation is
27
 * structural only — see {@link #load(Path)} for the exact rules.
28
 * </p>
29
 *
30
 * <p>
31
 * Package-private because the only consumers — {@link
32
 * ControlCoverageCollector} and {@link ControlCoverageWriter} — live in the
33
 * same package. The {@code MethodAtlasApp} entry point talks to the package
34
 * exclusively through {@link CoverageFacade}.
35
 * </p>
36
 *
37
 * @param framework        compliance framework label (e.g. {@code "ASVS"});
38
 *                         non-blank
39
 * @param frameworkVersion framework version (e.g. {@code "4.0"}); non-blank
40
 * @param source           absolute path of the mapping file as a string;
41
 *                         used for provenance in the output report
42
 * @param tagToControls    immutable mapping from taxonomy tag to a list of
43
 *                         control requirements; non-empty
44
 */
45
/* default */ record ControlMapping(
46
        String framework,
47
        String frameworkVersion,
48
        String source,
49
        Map<String, List<ControlEntry>> tagToControls) {
50
51
    /** Schema version this implementation accepts. */
52
    private static final String SCHEMA_VERSION = "1";
53
54
    /** Prefix used in every validation error message. */
55
    private static final String ERROR_PREFIX = "Mapping file '";
56
57
    /** Closing token following the file path in every validation error message. */
58
    private static final String ERROR_SUFFIX = "' ";
59
60
    /**
61
     * Loads and validates a control mapping from a JSON file on disk.
62
     *
63
     * <p>
64
     * The loader is tolerant of unknown top-level fields (forward
65
     * compatibility) but strict about the documented schema. Violations
66
     * surface as {@link IllegalArgumentException} with a message that
67
     * names the file and the failed constraint, so the CLI can render a
68
     * clear stderr message before exiting with code {@code 2}.
69
     * </p>
70
     *
71
     * <h2>Validation rules</h2>
72
     * <ol>
73
     *   <li>{@code schemaVersion} must equal {@code "1"}.</li>
74
     *   <li>{@code framework} and {@code frameworkVersion} must be present
75
     *       and non-blank.</li>
76
     *   <li>{@code tagToControls} must be a non-empty JSON object whose
77
     *       values are arrays of control entries.</li>
78
     *   <li>Every entry must have a non-blank {@code id}; {@code chapter}
79
     *       and {@code chapterTitle} are optional.</li>
80
     * </ol>
81
     *
82
     * <p>
83
     * The {@link #source()} field of the returned mapping captures the
84
     * absolute path string so the resulting report can document exactly
85
     * which file produced the claim being made.
86
     * </p>
87
     *
88
     * @param file path to the mapping JSON; must not be {@code null}
89
     * @return validated, deeply-unmodifiable mapping
90
     * @throws IOException              if the file cannot be read or parsed
91
     * @throws IllegalArgumentException if any validation rule fails
92
     */
93
    /* default */ static ControlMapping load(Path file) throws IOException {
94
        ObjectMapper mapper = JsonMapper.builder()
95
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build();
96
        JsonNode root;
97
        try {
98
            root = mapper.readTree(file.toFile());
99
        } catch (JacksonException e) {
100
            throw new IOException("Cannot read or parse control mapping file '" + file + "'", e);
101
        }
102
103 1 1. load : removed call to org/egothor/methodatlas/coverage/ControlMapping::validateSchemaVersion → KILLED
        validateSchemaVersion(root, file);
104
        String framework = requireString(root, "framework", file);
105
        String frameworkVersion = requireString(root, "frameworkVersion", file);
106
        Map<String, List<ControlEntry>> tagToControls = parseTagMap(root, file);
107
108 1 1. load : replaced return value with null for org/egothor/methodatlas/coverage/ControlMapping::load → KILLED
        return new ControlMapping(framework, frameworkVersion,
109
                file.toAbsolutePath().toString(),
110
                Collections.unmodifiableMap(tagToControls));
111
    }
112
113
    /**
114
     * Asserts that {@code root.schemaVersion} equals {@value #SCHEMA_VERSION}.
115
     *
116
     * @param root parsed JSON root
117
     * @param file source file (used for error context)
118
     */
119
    private static void validateSchemaVersion(JsonNode root, Path file) {
120
        JsonNode node = root.path("schemaVersion");
121 4 1. validateSchemaVersion : removed conditional - replaced equality check with true → SURVIVED
2. validateSchemaVersion : removed conditional - replaced equality check with true → KILLED
3. validateSchemaVersion : removed conditional - replaced equality check with false → KILLED
4. validateSchemaVersion : removed conditional - replaced equality check with false → KILLED
        if (!node.isString() || !SCHEMA_VERSION.equals(node.asString())) {
122
            throw new IllegalArgumentException(ERROR_PREFIX + file + ERROR_SUFFIX
123
                    + "has unsupported schemaVersion; expected \"" + SCHEMA_VERSION + "\"");
124
        }
125
    }
126
127
    /**
128
     * Extracts a required non-blank string field from {@code root}.
129
     *
130
     * @param root  parsed JSON root
131
     * @param field field name to fetch
132
     * @param file  source file (used for error context)
133
     * @return the field's text value
134
     */
135
    private static String requireString(JsonNode root, String field, Path file) {
136
        JsonNode node = root.path(field);
137 4 1. requireString : removed conditional - replaced equality check with true → SURVIVED
2. requireString : removed conditional - replaced equality check with false → KILLED
3. requireString : removed conditional - replaced equality check with false → KILLED
4. requireString : removed conditional - replaced equality check with true → KILLED
        if (!node.isString() || node.asString().isBlank()) {
138
            throw new IllegalArgumentException(ERROR_PREFIX + file + ERROR_SUFFIX + "is missing required "
139
                    + "non-blank field '" + field + "'");
140
        }
141 1 1. requireString : replaced return value with "" for org/egothor/methodatlas/coverage/ControlMapping::requireString → KILLED
        return node.asString();
142
    }
143
144
    /**
145
     * Parses the {@code tagToControls} object into a {@link LinkedHashMap}
146
     * preserving JSON declaration order, validating every entry along the way.
147
     *
148
     * @param root parsed JSON root
149
     * @param file source file (used for error context)
150
     * @return populated map; never empty
151
     */
152
    private static Map<String, List<ControlEntry>> parseTagMap(JsonNode root, Path file) {
153
        JsonNode tagNode = root.path("tagToControls");
154 4 1. parseTagMap : removed conditional - replaced equality check with true → SURVIVED
2. parseTagMap : removed conditional - replaced equality check with false → KILLED
3. parseTagMap : removed conditional - replaced equality check with true → KILLED
4. parseTagMap : removed conditional - replaced equality check with false → KILLED
        if (!tagNode.isObject() || tagNode.isEmpty()) {
155
            throw new IllegalArgumentException("Mapping file '" + file
156
                    + "' must contain a non-empty 'tagToControls' object");
157
        }
158
        Map<String, List<ControlEntry>> result = new LinkedHashMap<>();
159 1 1. parseTagMap : removed call to java/util/Set::forEach → KILLED
        tagNode.properties().forEach(entry ->
160
                result.put(entry.getKey(), parseControlList(entry.getKey(), entry.getValue(), file)));
161 1 1. parseTagMap : replaced return value with Collections.emptyMap for org/egothor/methodatlas/coverage/ControlMapping::parseTagMap → KILLED
        return result;
162
    }
163
164
    /**
165
     * Parses one tag's control-array value, validating every element.
166
     *
167
     * @param tag    tag name (for error context)
168
     * @param array  JSON array node
169
     * @param file   source file (for error context)
170
     * @return immutable list of control entries
171
     */
172
    private static List<ControlEntry> parseControlList(String tag, JsonNode array, Path file) {
173 2 1. parseControlList : removed conditional - replaced equality check with false → SURVIVED
2. parseControlList : removed conditional - replaced equality check with true → KILLED
        if (!array.isArray()) {
174
            throw new IllegalArgumentException(ERROR_PREFIX + file + ERROR_SUFFIX + "tag '" + tag
175
                    + "' must map to a JSON array of control entries");
176
        }
177
        List<ControlEntry> entries = new ArrayList<>(array.size());
178
        for (JsonNode element : array) {
179
            entries.add(parseControlEntry(tag, element, file));
180
        }
181 1 1. parseControlList : replaced return value with Collections.emptyList for org/egothor/methodatlas/coverage/ControlMapping::parseControlList → KILLED
        return Collections.unmodifiableList(entries);
182
    }
183
184
    /**
185
     * Parses a single {@code {id, chapter, chapterTitle}} object.
186
     *
187
     * @param tag     tag name (for error context)
188
     * @param node    JSON object node
189
     * @param file    source file (for error context)
190
     * @return populated entry
191
     */
192
    private static ControlEntry parseControlEntry(String tag, JsonNode node, Path file) {
193 2 1. parseControlEntry : removed conditional - replaced equality check with false → SURVIVED
2. parseControlEntry : removed conditional - replaced equality check with true → KILLED
        if (!node.isObject()) {
194
            throw new IllegalArgumentException(ERROR_PREFIX + file + ERROR_SUFFIX + "tag '" + tag
195
                    + "' entries must be JSON objects");
196
        }
197
        JsonNode idNode = node.path("id");
198 4 1. parseControlEntry : removed conditional - replaced equality check with true → SURVIVED
2. parseControlEntry : removed conditional - replaced equality check with false → KILLED
3. parseControlEntry : removed conditional - replaced equality check with false → KILLED
4. parseControlEntry : removed conditional - replaced equality check with true → KILLED
        if (!idNode.isString() || idNode.asString().isBlank()) {
199
            throw new IllegalArgumentException(ERROR_PREFIX + file + ERROR_SUFFIX + "tag '" + tag
200
                    + "' has an entry with missing or blank 'id'");
201
        }
202
        String chapter = optionalString(node, "chapter");
203
        String chapterTitle = optionalString(node, "chapterTitle");
204 1 1. parseControlEntry : replaced return value with null for org/egothor/methodatlas/coverage/ControlMapping::parseControlEntry → KILLED
        return new ControlEntry(idNode.asString(), chapter, chapterTitle);
205
    }
206
207
    /**
208
     * Reads an optional textual field, returning {@code null} when absent or
209
     * explicitly JSON-null. Blank strings are preserved so authors can supply
210
     * empty placeholders intentionally.
211
     *
212
     * @param node  JSON object node
213
     * @param field field name
214
     * @return field value or {@code null}
215
     */
216
    private static String optionalString(JsonNode node, String field) {
217
        JsonNode child = node.get(field);
218 6 1. optionalString : removed conditional - replaced equality check with false → SURVIVED
2. optionalString : removed conditional - replaced equality check with true → SURVIVED
3. optionalString : removed conditional - replaced equality check with false → KILLED
4. optionalString : removed conditional - replaced equality check with true → KILLED
5. optionalString : removed conditional - replaced equality check with false → KILLED
6. optionalString : removed conditional - replaced equality check with true → KILLED
        if (child == null || child.isNull() || !child.isString()) {
219 1 1. optionalString : replaced return value with "" for org/egothor/methodatlas/coverage/ControlMapping::optionalString → SURVIVED
            return null;
220
        }
221 1 1. optionalString : replaced return value with "" for org/egothor/methodatlas/coverage/ControlMapping::optionalString → KILLED
        return child.asString();
222
    }
223
}

Mutations

103

1.1
Location : load
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_wrongSchemaVersion_throwsIllegalArgument(java.nio.file.Path)]
removed call to org/egothor/methodatlas/coverage/ControlMapping::validateSchemaVersion → KILLED

108

1.1
Location : load
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_referenceTemplate_parsesCleanly()]
replaced return value with null for org/egothor/methodatlas/coverage/ControlMapping::load → KILLED

121

1.1
Location : validateSchemaVersion
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_emptyTagToControls_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : validateSchemaVersion
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_wrongSchemaVersion_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

3.3
Location : validateSchemaVersion
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

4.4
Location : validateSchemaVersion
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_emptyTagToControls_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

137

1.1
Location : requireString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankFrameworkVersion_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : requireString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_emptyTagToControls_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

3.3
Location : requireString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_emptyTagToControls_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

4.4
Location : requireString
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

141

1.1
Location : requireString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_referenceTemplate_parsesCleanly()]
replaced return value with "" for org/egothor/methodatlas/coverage/ControlMapping::requireString → KILLED

154

1.1
Location : parseTagMap
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankId_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : parseTagMap
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankId_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

3.3
Location : parseTagMap
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

4.4
Location : parseTagMap
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_emptyTagToControls_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

159

1.1
Location : parseTagMap
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankId_throwsIllegalArgument(java.nio.file.Path)]
removed call to java/util/Set::forEach → KILLED

161

1.1
Location : parseTagMap
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_referenceTemplate_parsesCleanly()]
replaced return value with Collections.emptyMap for org/egothor/methodatlas/coverage/ControlMapping::parseTagMap → KILLED

173

1.1
Location : parseControlList
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankId_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : parseControlList
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

181

1.1
Location : parseControlList
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_validMapping_populatesFieldsAndSource(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/coverage/ControlMapping::parseControlList → KILLED

193

1.1
Location : parseControlEntry
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

2.2
Location : parseControlEntry
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankId_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

198

1.1
Location : parseControlEntry
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_referenceTemplate_parsesCleanly()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : parseControlEntry
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_blankId_throwsIllegalArgument(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

3.3
Location : parseControlEntry
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

4.4
Location : parseControlEntry
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_referenceTemplate_parsesCleanly()]
removed conditional - replaced equality check with true → KILLED

204

1.1
Location : parseControlEntry
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_validMapping_populatesFieldsAndSource(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/coverage/ControlMapping::parseControlEntry → KILLED

218

1.1
Location : optionalString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_validMapping_populatesFieldsAndSource(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : optionalString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_tagToControls_isUnmodifiable(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

3.3
Location : optionalString
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

4.4
Location : optionalString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_validMapping_populatesFieldsAndSource(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

5.5
Location : optionalString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_validMapping_populatesFieldsAndSource(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

6.6
Location : optionalString
Killed by : none
removed conditional - replaced equality check with true → SURVIVED Covering tests

219

1.1
Location : optionalString
Killed by : none
replaced return value with "" for org/egothor/methodatlas/coverage/ControlMapping::optionalString → SURVIVED
Covering tests

221

1.1
Location : optionalString
Killed by : org.egothor.methodatlas.coverage.ControlMappingTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.coverage.ControlMappingTest]/[method:load_validMapping_populatesFieldsAndSource(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/coverage/ControlMapping::optionalString → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1