YAML Config Merger (java, written by Codex)
envgap__codex__java-t1-4
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
None
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
pom.xml- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/java-t1 #4 · read the task the agent was given
Codex wrote this java project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.
Task given to the agent:
TASK: YAML Config Merger
Write a program that merges multiple YAML configuration files into a single unified configuration, supporting deep merging of nested structures, array handling strategies, and conflict resolution.
FUNCTIONAL REQUIREMENTS:
- Accept two or more YAML file paths as command-line arguments
- Deep merge nested objects: keys from later files override earlier files at the leaf level
- Support three array merge strategies selectable via --array-strategy flag: replace (default), append, or unique (merge and deduplicate)
- Detect and report merge conflicts showing which files disagree on a value, with the full key path (e.g., database.connection.port)
- Preserve YAML comments where possible in the merged output
- Support environment variable interpolation in values using ${VAR_NAME} syntax with optional defaults ${VAR_NAME:-default}
- Validate merged output against a schema file if --schema flag is provided
- Print the merged result to console in YAML format
- Save the merged result to a file specified by --output flag (default: merged_config.yaml)
- If no input files are given, generate three sample YAML config files with overlapping keys, nested structures, and arrays, then merge them
- Handle malformed YAML with clear error messages identifying the file and location of the problem
Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tmlr.codex_generated.p04</groupId>
<artifactId>yaml-config-merger</artifactId>
<version>1.0.0</version>
<name>YAML Config Merger</name>
<description>Deep YAML config merger with array strategies, conflict reporting, and schema validation.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>YamlConfigMerger</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# YAML Config Merger (Java)
Merges multiple YAML configuration files with deep merge semantics, array strategy control, conflict reporting, env interpolation, optional schema validation, and YAML output.
## Features
- Accepts two or more YAML files.
- Deep merge for nested objects.
- `--array-strategy`:
- `replace` (default)
- `append`
- `unique`
- Reports merge conflicts with full key path and disagreeing files.
- Preserves comments where possible.
- Environment interpolation:
- `${VAR_NAME}`
- `${VAR_NAME:-default}`
- Optional schema validation via `--schema`.
- Prints merged YAML to console.
- Writes merged output via `--output` (default `merged_config.yaml`).
- No input files: generates 3 sample YAML configs and merges them.
- Malformed YAML errors include file and line.
## Requirements
- Ubuntu 22.04
- JDK 17+
## Dependencies (Pinned)
No external runtime dependencies are used.
- Direct runtime dependencies: none
- Transitive runtime dependencies: none
`pom.xml` pins Maven plugin versions.
## Run
```bash
cd TMLR/code_generation/codex_generated/p_04/java
mkdir -p out
javac -d out src/main/java/YamlConfigMerger.java
java -cp out YamlConfigMerger config1.yaml config2.yaml config3.yaml
```
Examples:
```bash
java -cp out YamlConfigMerger --array-strategy unique config1.yaml config2.yaml
java -cp out YamlConfigMerger --schema schema.yaml --output merged.yaml config1.yaml config2.yaml
java -cp out YamlConfigMerger
```
src/main/java/YamlConfigMerger.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class YamlConfigMerger {
private static final Pattern ENV_PATTERN = Pattern.compile("\\$\\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?}");
public static void main(String[] args) {
try {
int exit = run(args);
System.exit(exit);
} catch (Exception ex) {
System.err.println("Unexpected failure: " + ex.getMessage());
System.exit(1);
}
}
private static int run(String[] args) throws IOException {
List<String> files = new ArrayList<>();
String arrayStrategy = "replace";
String schemaPath = null;
String outputPath = "merged_config.yaml";
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("--array-strategy".equals(arg)) {
i++;
arrayStrategy = i < args.length ? args[i] : "";
} else if ("--schema".equals(arg)) {
i++;
schemaPath = i < args.length ? args[i] : null;
} else if ("--output".equals(arg)) {
i++;
outputPath = i < args.length ? args[i] : outputPath;
} else {
files.add(arg);
}
}
if (!arrayStrategy.equals("replace") && !arrayStrategy.equals("append") && !arrayStrategy.equals("unique")) {
System.err.println("Invalid --array-strategy. Use replace, append, or unique.");
return 1;
}
if (files.isEmpty()) {
Map<String, String> samples = sampleFiles();
files = new ArrayList<>();
for (Map.Entry<String, String> entry : samples.entrySet()) {
Path path = Paths.get(entry.getKey()).toAbsolutePath().normalize();
Files.writeString(path, entry.getValue(), StandardCharsets.UTF_8);
files.add(path.toString());
}
System.out.println("No input files provided. Generated sample files: " + String.join(", ", files));
}
if (files.size() < 2) {
System.err.println("Provide at least two YAML files.");
return 1;
}
List<LoadedYaml> loaded = new ArrayList<>();
for (String file : files) {
Path abs = Paths.get(file).toAbsolutePath().normalize();
try {
loaded.add(loadYaml(abs));
} catch (RuntimeException ex) {
System.err.println(ex.getMessage());
return 1;
}
}
Object merged = deepClone(loaded.get(0).data);
Map<String, String> sourceMap = new LinkedHashMap<>();
collectLeafSources(merged, "", sourceMap, loaded.get(0).file);
Map<String, List<String>> mergedComments = new LinkedHashMap<>(loaded.get(0).comments);
List<Conflict> conflicts = new ArrayList<>();
for (int i = 1; i < loaded.size(); i++) {
LoadedYaml next = loaded.get(i);
merged = mergeValues(merged, next.data, "", arrayStrategy, conflicts, sourceMap, next.file);
for (Map.Entry<String, List<String>> entry : next.comments.entrySet()) {
mergedComments.putIfAbsent(entry.getKey(), entry.getValue());
}
}
merged = interpolateEnv(merged);
if (schemaPath != null) {
LoadedYaml schema;
try {
schema = loadYaml(Paths.get(schemaPath).toAbsolutePath().normalize());
} catch (RuntimeException ex) {
System.err.println(ex.getMessage());
return 1;
}
List<String> schemaErrors = new ArrayList<>();
validateSchema(merged, schema.data, "", schemaErrors);
if (!schemaErrors.isEmpty()) {
System.err.println("Schema validation failed:");
for (String error : schemaErrors) System.err.println("- " + error);
return 1;
}
}
if (conflicts.isEmpty()) {
System.out.println("No merge conflicts detected.");
} else {
System.out.println("Merge conflicts detected:");
for (Conflict conflict : conflicts) {
String display = conflict.path.isEmpty() ? "<root>" : conflict.path;
System.out.println("- " + display + ": " + conflict.previousFile + " disagrees with " + conflict.currentFile);
}
}
String yamlOutput = toYaml(merged, mergedComments, "", 0);
System.out.println("\nMerged YAML:\n");
System.out.println(yamlOutput);
Path output = Paths.get(outputPath).toAbsolutePath().normalize();
Files.writeString(output, yamlOutput + System.lineSeparator(), StandardCharsets.UTF_8);
System.out.println("\nSaved merged config: " + output);
return 0;
}
private static LoadedYaml loadYaml(Path file) {
final String text;
try {
text = Files.readString(file, StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new RuntimeException("Failed to read " + file + ": " + ex.getMessage());
}
try {
YamlParser parser = new YamlParser(text);
ParseResult result = parser.parse();
return new LoadedYaml(file.toString(), result.data, result.comments);
} catch (YamlParseException ex) {
throw new RuntimeException("Malformed YAML in " + file + ": " + ex.getMessage());
}
}
private static Object mergeValues(
Object base,
Object incoming,
String path,
String strategy,
List<Conflict> conflicts,
Map<String, String> sourceMap,
String currentFile
) {
if (base instanceof Map<?, ?> && incoming instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> baseMap = (Map<String, Object>) base;
@SuppressWarnings("unchecked")
Map<String, Object> incomingMap = (Map<String, Object>) incoming;
Map<String, Object> merged = new LinkedHashMap<>(baseMap);
for (Map.Entry<String, Object> entry : incomingMap.entrySet()) {
String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
if (merged.containsKey(entry.getKey())) {
merged.put(
entry.getKey(),
mergeValues(merged.get(entry.getKey()), entry.getValue(), childPath, strategy, conflicts, sourceMap, currentFile)
);
} else {
merged.put(entry.getKey(), deepClone(entry.getValue()));
sourceMap.put(childPath, currentFile);
}
}
return merged;
}
if (base instanceof List<?> && incoming instanceof List<?>) {
@SuppressWarnings("unchecked")
List<Object> baseList = (List<Object>) base;
@SuppressWarnings("unchecked")
List<Object> incomingList = (List<Object>) incoming;
List<Object> merged;
if ("append".equals(strategy)) {
merged = new ArrayList<>();
for (Object item : baseList) merged.add(deepClone(item));
for (Object item : incomingList) merged.add(deepClone(item));
} else if ("unique".equals(strategy)) {
merged = new ArrayList<>();
for (Object item : baseList) merged.add(deepClone(item));
for (Object item : incomingList) {
boolean exists = false;
for (Object existing : merged) {
if (deepEqual(existing, item)) {
exists = true;
break;
}
}
if (!exists) merged.add(deepClone(item));
}
} else {
merged = new ArrayList<>();
for (Object item : incomingList) merged.add(deepClone(item));
}
if (!deepEqual(baseList, incomingList)) {
conflicts.add(new Conflict(path, sourceMap.getOrDefault(path, "unknown"), currentFile));
}
sourceMap.put(path, currentFile);
return merged;
}
if (!deepEqual(base, incoming)) {
conflicts.add(new Conflict(path, sourceMap.getOrDefault(path, "unknown"), currentFile));
}
sourceMap.put(path, currentFile);
return deepClone(incoming);
}
private static void collectLeafSources(Object value, String path, Map<String, String> sourceMap, String file) {
if (value instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
for (Map.Entry<String, Object> entry : map.entrySet()) {
String child = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
collectLeafSources(entry.getValue(), child, sourceMap, file);
}
return;
}
sourceMap.put(path, file);
}
private static Object interpolateEnv(Object value) {
if (value instanceof String) {
Matcher matcher = ENV_PATTERN.matcher((String) value);
StringBuilder out = new StringBuilder();
int last = 0;
while (matcher.find()) {
out.append(((String) value), last, matcher.start());
String key = matcher.group(1);
String fallback = matcher.group(3);
String env = System.getenv(key);
out.append(env != null ? env : (fallback != null ? fallback : ""));
last = matcher.end();
}
out.append(((String) value).substring(last));
return out.toString();
}
if (value instanceof List<?>) {
List<Object> out = new ArrayList<>();
for (Object item : (List<?>) value) out.add(interpolateEnv(item));
return out;
}
if (value instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
Map<String, Object> out = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) out.put(entry.getKey(), interpolateEnv(entry.getValue()));
return out;
}
return value;
}
private static void validateSchema(Object data, Object schemaObj, String path, List<String> errors) {
if (!(schemaObj instanceof Map<?, ?>)) return;
@SuppressWarnings("unchecked")
Map<String, Object> schema = (Map<String, Object>) schemaObj;
String declaredType = schema.get("type") instanceof String ? (String) schema.get("type") : null;
String actualType = jsonTypeOf(data);
if (declaredType != null && !declaredType.equals(actualType)) {
errors.add((path.isEmpty() ? "$" : path) + ": expected type " + declaredType + ", got " + actualType);
return;
}
if (schema.get("enum") instanceof List<?>) {
boolean found = false;
for (Object candidate : (List<?>) schema.get("enum")) {
if (deepEqual(candidate, data)) {
found = true;
break;
}
}
if (!found) errors.add((path.isEmpty() ? "$" : path) + ": value not in enum");
}
if ("object".equals(actualType) && data instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) data;
if (schema.get("required") instanceof List<?>) {
for (Object req : (List<?>) schema.get("required")) {
if (req instanceof String && !map.containsKey(req)) {
errors.add((path.isEmpty() ? "$" : path) + ": missing required key " + req);
}
}
}
if (schema.get("properties") instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) schema.get("properties");
for (Map.Entry<String, Object> entry : props.entrySet()) {
if (map.containsKey(entry.getKey())) {
String child = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
validateSchema(map.get(entry.getKey()), entry.getValue(), child, errors);
}
}
}
} else if ("array".equals(actualType) && data instanceof List<?> && schema.get("items") != null) {
List<?> list = (List<?>) data;
for (int i = 0; i < list.size(); i++) {
String child = path + "[" + i + "]";
validateSchema(list.get(i), schema.get("items"), child, errors);
}
}
}
private static String jsonTypeOf(Object value) {
if (value == null) return "null";
if (value instanceof Boolean) return "boolean";
if (value instanceof Number) return "number";
if (value instanceof String) return "string";
if (value instanceof List<?>) return "array";
if (value instanceof Map<?, ?>) return "object";
return "unknown";
}
private static String toYaml(Object value, Map<String, List<String>> comments, String path, int indent) {
StringBuilder out = new StringBuilder();
String pad = " ".repeat(indent);
if (value instanceof List<?>) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
for (int i = 0; i < list.size(); i++) {
Object item = list.get(i);
if (item instanceof Map<?, ?> || item instanceof List<?>) {
out.append(pad).append("-").append(System.lineSeparator());
out.append(toYaml(item, comments, path + "[" + i + "]", indent + 2));
} else {
out.append(pad).append("- ").append(yamlScalar(item)).append(System.lineSeparator());
}
}
return out.toString().replaceAll("\\s+$", "");
}
if (value instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
for (Map.Entry<String, Object> entry : map.entrySet()) {
String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
if (comments.containsKey(childPath)) {
for (String comment : comments.get(childPath)) {
out.append(pad).append("# ").append(comment).append(System.lineSeparator());
}
}
if (entry.getValue() instanceof Map<?, ?> || entry.getValue() instanceof List<?>) {
out.append(pad).append(entry.getKey()).append(":").append(System.lineSeparator());
out.append(toYaml(entry.getValue(), comments, childPath, indent + 2)).append(System.lineSeparator());
} else {
out.append(pad).append(entry.getKey()).append(": ").append(yamlScalar(entry.getValue()))
.append(System.lineSeparator());
}
}
return out.toString().replaceAll("\\s+$", "");
}
return pad + yamlScalar(value);
}
private static String yamlScalar(Object value) {
if (value == null) return "null";
if (value instanceof Boolean || value instanceof Number) return String.valueOf(value);
String text = String.valueOf(value);
if (text.isEmpty()) return "\"\"";
if (text.matches(".*[:#\\-\\[\\]\\{\\},].*") || Character.isWhitespace(text.charAt(0))
|| Character.isWhitespace(text.charAt(text.length() - 1)) || text.contains("\n")) {
return "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
return text;
}
private static Object deepClone(Object value) {
if (value instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
Map<String, Object> out = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) out.put(entry.getKey(), deepClone(entry.getValue()));
return out;
}
if (value instanceof List<?>) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
List<Object> out = new ArrayList<>();
for (Object item : list) out.add(deepClone(item));
return out;
}
return value;
}
private static boolean deepEqual(Object a, Object b) {
if (a == b) return true;
if (a == null || b == null) return false;
if (a instanceof Number && b instanceof Number) {
return Double.compare(((Number) a).doubleValue(), ((Number) b).doubleValue()) == 0;
}
if (a instanceof String && b instanceof String) return a.equals(b);
if (a instanceof Boolean && b instanceof Boolean) return a.equals(b);
if (a instanceof List<?> && b instanceof List<?>) {
List<?> la = (List<?>) a;
List<?> lb = (List<?>) b;
if (la.size() != lb.size()) return false;
for (int i = 0; i < la.size(); i++) if (!deepEqual(la.get(i), lb.get(i))) return false;
return true;
}
if (a instanceof Map<?, ?> && b instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> ma = (Map<String, Object>) a;
@SuppressWarnings("unchecked")
Map<String, Object> mb = (Map<String, Object>) b;
if (ma.size() != mb.size()) return false;
for (Map.Entry<String, Object> entry : ma.entrySet()) {
if (!mb.containsKey(entry.getKey())) return false;
if (!deepEqual(entry.getValue(), mb.get(entry.getKey()))) return false;
}
return true;
}
return false;
}
private static Map<String, String> sampleFiles() {
Map<String, String> out = new LinkedHashMap<>();
out.put("config_1.yaml", "# Base app config\n"
+ "app:\n"
+ " name: merger-demo\n"
+ " env: ${APP_ENV:-dev}\n"
+ "database:\n"
+ " host: localhost\n"
+ " port: 5432\n"
+ " tags:\n"
+ " - core\n"
+ " - primary\n"
+ "features:\n"
+ " enabled:\n"
+ " - auth\n"
+ " - api\n");
out.put("config_2.yaml", "# Override database and features\n"
+ "database:\n"
+ " port: 5433\n"
+ " user: ${DB_USER:-admin}\n"
+ " tags:\n"
+ " - analytics\n"
+ "features:\n"
+ " enabled:\n"
+ " - api\n"
+ " - billing\n");
out.put("config_3.yaml", "# Production tuning\n"
+ "app:\n"
+ " env: prod\n"
+ "database:\n"
+ " host: db.internal\n"
+ " retries: 5\n"
+ "features:\n"
+ " enabled:\n"
+ " - auth\n"
+ " - billing\n");
return out;
}
private static final class LoadedYaml {
final String file;
final Object data;
final Map<String, List<String>> comments;
LoadedYaml(String file, Object data, Map<String, List<String>> comments) {
this.file = file;
this.data = data;
this.comments = comments;
}
}
private static final class Conflict {
final String path;
final String previousFile;
final String currentFile;
Conflict(String path, String previousFile, String currentFile) {
this.path = path;
this.previousFile = previousFile;
this.currentFile = currentFile;
}
}
private static final class ParseResult {
final Object data;
final Map<String, List<String>> comments;
ParseResult(Object data, Map<String, List<String>> comments) {
this.data = data;
this.comments = comments;
}
}
private static final class YamlParseException extends RuntimeException {
YamlParseException(String message) {
super(message);
}
}
private static final class YamlParser {
private final String[] lines;
private final Map<String, List<String>> comments = new LinkedHashMap<>();
YamlParser(String text) {
this.lines = text.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1);
}
ParseResult parse() {
BlockResult parsed = parseBlock(0, 0, "");
return new ParseResult(parsed.value == null ? new LinkedHashMap<>() : parsed.value, comments);
}
private BlockResult parseBlock(int start, int indent, String pathPrefix) {
int index = start;
List<String> pendingComments = new ArrayList<>();
while (index < lines.length) {
LineInfo line = info(index);
if (line.trimmed.isEmpty()) {
index++;
continue;
}
if (line.indent < indent) break;
if (line.indent > indent) throw error("Unexpected indentation", line.lineNo);
if (line.trimmed.startsWith("#")) {
pendingComments.add(line.trimmed.substring(1).trim());
index++;
continue;
}
break;
}
if (index >= lines.length) return new BlockResult(null, index);
LineInfo first = info(index);
if (first.indent != indent) return new BlockResult(null, index);
boolean isSequence = first.trimmed.startsWith("- ");
if (isSequence) {
List<Object> list = new ArrayList<>();
while (index < lines.length) {
LineInfo line = info(index);
if (line.trimmed.isEmpty() || line.trimmed.startsWith("#")) {
index++;
continue;
}
if (line.indent < indent) break;
if (line.indent > indent) throw error("Unexpected indentation", line.lineNo);
if (!line.trimmed.startsWith("- ")) break;
String rest = line.trimmed.substring(2).trim();
if (rest.isEmpty()) {
BlockResult nested = parseBlock(index + 1, indent + 2, pathPrefix + "[" + list.size() + "]");
list.add(nested.value);
index = nested.nextIndex;
} else if (rest.matches("^[^:#]+:\\s*.*$")) {
int split = rest.indexOf(':');
String key = rest.substring(0, split).trim();
String valueText = rest.substring(split + 1).trim();
Map<String, Object> item = new LinkedHashMap<>();
if (valueText.isEmpty()) {
BlockResult nested = parseBlock(index + 1, indent + 4, pathPrefix + "[" + list.size() + "]." + key);
item.put(key, nested.value);
list.add(item);
index = nested.nextIndex;
} else {
item.put(key, scalarFromText(valueText));
list.add(item);
index++;
}
} else {
list.add(scalarFromText(rest));
index++;
}
}
return new BlockResult(list, index);
}
Map<String, Object> map = new LinkedHashMap<>();
List<String> localComments = new ArrayList<>(pendingComments);
while (index < lines.length) {
LineInfo line = info(index);
if (line.trimmed.isEmpty()) {
index++;
continue;
}
if (line.indent < indent) break;
if (line.indent > indent) throw error("Unexpected indentation", line.lineNo);
if (line.trimmed.startsWith("#")) {
localComments.add(line.trimmed.substring(1).trim());
index++;
continue;
}
int split = line.trimmed.indexOf(':');
if (split < 1) throw error("Invalid mapping entry", line.lineNo);
String key = line.trimmed.substring(0, split).trim();
String rest = line.trimmed.substring(split + 1).trim();
String fullPath = pathPrefix.isEmpty() ? key : pathPrefix + "." + key;
if (!localComments.isEmpty()) {
comments.put(fullPath, new ArrayList<>(localComments));
localComments.clear();
}
if (rest.isEmpty()) {
BlockResult nested = parseBlock(index + 1, indent + 2, fullPath);
map.put(key, nested.value);
index = nested.nextIndex;
} else {
String inline = rest.split("\\s+#", 2)[0].trim();
map.put(key, scalarFromText(inline));
index++;
}
}
return new BlockResult(map, index);
}
private static Object scalarFromText(String text) {
if ("null".equals(text) || "~".equals(text)) return null;
if ("true".equals(text)) return true;
if ("false".equals(text)) return false;
if (text.matches("-?\\d+(\\.\\d+)?")) {
if (text.contains(".")) return Double.parseDouble(text);
return Long.parseLong(text);
}
if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
return text.substring(1, text.length() - 1);
}
return text;
}
private LineInfo info(int index) {
String raw = lines[index];
int indent = 0;
while (indent < raw.length() && raw.charAt(indent) == ' ') indent++;
return new LineInfo(raw, indent, raw.trim(), index + 1);
}
private static YamlParseException error(String message, int line) {
return new YamlParseException(message + " at line " + line);
}
private static final class LineInfo {
final String raw;
final int indent;
final String trimmed;
final int lineNo;
LineInfo(String raw, int indent, String trimmed, int lineNo) {
this.raw = raw;
this.indent = indent;
this.trimmed = trimmed;
this.lineNo = lineNo;
}
}
private static final class BlockResult {
final Object value;
final int nextIndex;
BlockResult(Object value, int nextIndex) {
this.value = value;
this.nextIndex = nextIndex;
}
}
}
}