All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.config.ini.Ini Maven / Gradle / Ivy

// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.ini;

import com.yahoo.yolean.Exceptions;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Scanner;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Basic INI file parser.
 *
 * 

Supported syntax:

* *
    *
  • Sections. Surrounded with '[' and ']'
  • *
  • Optional quoting of values. When quoted, the quote character '"' can be escaped with '\'
  • *
  • Comments, separate and in-line. Indicated with leading ';' or '#'
  • *
* *

Behaviour:

* *
    *
  • Leading and trailing whitespace is always ignored if the value is unquoted
  • *
  • Sections are sorted in alphabetic order. The same goes for keys within a section
  • *
  • Empty string in the parsed Map holds section-less config keys
  • *
  • Duplicated keys within the same section is an error
  • *
  • Parsing discards comments
  • *
  • No limitations on section or key names
  • *
* * @param entries Entries of the INI file, grouped by section. * * @author mpolden */ public record Ini(SortedMap> entries) { private static final char ESCAPE_C = '\\'; private static final char QUOTE_C = '"'; private static final String QUOTE = String.valueOf(QUOTE_C); public Ini { var copy = new TreeMap<>(entries); copy.replaceAll((k, v) -> Collections.unmodifiableSortedMap(new TreeMap<>(copy.get(k)))); entries = Collections.unmodifiableSortedMap(copy); } /** Write the text representation of this to given output */ public void write(OutputStream output) { PrintStream printer = new PrintStream(output, true); entries.forEach((section, sectionEntries) -> { if (!section.isEmpty()) { printer.printf("[%s]\n", section); } sectionEntries.forEach((key, value) -> { printer.printf("%s = %s\n", key, quote(value)); }); if (!section.equals(entries.lastKey())) { printer.println(); } }); } /** Parse an INI configuration from given input */ public static Ini parse(InputStream input) { SortedMap> entries = new TreeMap<>(); Scanner scanner = new Scanner(input, StandardCharsets.UTF_8); String section = ""; int lineNum = 0; while (scanner.hasNextLine()) { lineNum++; String line = scanner.nextLine().trim(); // Blank line if (line.isEmpty()) { continue; } // Comment if (isComment(line)) { continue; } // Section if (line.startsWith("[") && line.endsWith("]")) { section = line.substring(1, line.length() - 1); continue; } // Key-value entry try { Entry entry = Entry.parse(line); entries.putIfAbsent(section, new TreeMap<>()); String prevValue = entries.computeIfAbsent(section, (k) -> new TreeMap<>()) .put(entry.key, entry.value); if (prevValue != null) { throw new IllegalArgumentException("Key '" + entry.key + "' duplicated in section '" + section + "'"); } } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid entry on line " + lineNum + ": '" + line + "': " + Exceptions.toMessageString(e)); } } return new Ini(entries); } private static boolean isComment(String s) { return s.startsWith(";") || s.startsWith("#"); } private static boolean requiresQuoting(String s) { return s.isEmpty() || s.contains(QUOTE) || !s.equals(s.trim()); } private static boolean unescapedQuoteAt(int index, String s) { return s.charAt(index) == QUOTE_C && (index == 0 || s.charAt(index - 1) != ESCAPE_C); } private static String quote(String s) { if (!requiresQuoting(s)) return s; StringBuilder sb = new StringBuilder(); sb.append(QUOTE); for (int i = 0; i < s.length(); i++) { if (unescapedQuoteAt(i, s)) { sb.append(ESCAPE_C); } sb.append(s.charAt(i)); } sb.append(QUOTE); return sb.toString(); } private record Entry(String key, String value) { static Entry parse(String s) { int equalIndex = s.indexOf('='); if (equalIndex < 0) throw new IllegalArgumentException("Expected key=[value]"); String key = s.substring(0, equalIndex).trim(); String value = s.substring(equalIndex + 1).trim(); return new Entry(key, dequote(value)); } private static String dequote(String s) { boolean quoted = s.startsWith(QUOTE); int end = s.length(); boolean closeQuote = false; for (int i = 0; i < s.length(); i++) { closeQuote = quoted && i > 0 && unescapedQuoteAt(i, s); boolean startComment = !quoted && isComment(String.valueOf(s.charAt(i))); if (closeQuote || startComment) { end = i; if (quoted && end < s.length() - 1) { String trailing = s.substring(end + 1).trim(); if (!isComment(trailing)) { throw new IllegalArgumentException("Additional character(s) after end quote at column " + end); } } break; } } if (quoted && !closeQuote) { throw new IllegalArgumentException("Missing closing quote"); } int start = quoted ? 1 : 0; String value = s.substring(start, end); return quoted ? value : value.trim(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy