/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.collect.io;
import java.io.UncheckedIOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.CharSource;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.MapStream;
import com.opengamma.strata.collect.Unchecked;
/**
* An INI file.
* <p>
* Represents an INI file together with the ability to parse it from a {@link CharSource}.
* <p>
* The INI file format used here is deliberately simple.
* There are two elements - key-value pairs and sections.
* <p>
* The basic element is a key-value pair.
* The key is separated from the value using the '=' symbol.
* Duplicate keys are allowed.
* For example 'key = value'.
* The equals sign and value may be omitted, in which case the value is an empty string.
* <p>
* All properties are grouped into named sections.
* The section name occurs on a line by itself surrounded by square brackets.
* Duplicate section names are not allowed.
* For example '[section]'.
* <p>
* Keys, values and section names are trimmed.
* Blank lines are ignored.
* Whole line comments begin with hash '#' or semicolon ';'.
* No escape format is available.
* Lookup is case sensitive.
* <p>
* This example explains the format:
* <pre>
* # line comment
* [foo]
* key = value
*
* [bar]
* key = value
* month = January
* </pre>
* <p>
* The aim of this class is to parse the basic format.
* Interpolation of variables is not supported.
*/
public final class IniFile {
/**
* The INI sections.
*/
private final ImmutableMap<String, PropertySet> sectionMap;
//-------------------------------------------------------------------------
/**
* Parses the specified source as an INI file.
* <p>
* This parses the specified character source expecting an INI file format.
* The resulting instance can be queried for each section in the file.
*
* @param source the INI file resource
* @return the INI file
* @throws UncheckedIOException if an IO exception occurs
* @throws IllegalArgumentException if the file cannot be parsed
*/
public static IniFile of(CharSource source) {
ArgChecker.notNull(source, "source");
ImmutableList<String> lines = Unchecked.wrap(() -> source.readLines());
ImmutableMap<String, ImmutableListMultimap<String, String>> parsedIni = parse(lines);
ImmutableMap.Builder<String, PropertySet> builder = ImmutableMap.builder();
parsedIni.forEach((sectionName, sectionData) -> builder.put(sectionName, PropertySet.of(sectionData)));
return new IniFile(builder.build());
}
//-------------------------------------------------------------------------
// parses the INI file format
private static ImmutableMap<String, ImmutableListMultimap<String, String>> parse(ImmutableList<String> lines) {
// cannot use ArrayListMultiMap as it does not retain the order of the keys
// whereas ImmutableListMultimap does retain the order of the keys
Map<String, ImmutableListMultimap.Builder<String, String>> ini = new LinkedHashMap<>();
ImmutableListMultimap.Builder<String, String> currentSection = null;
int lineNum = 0;
for (String line : lines) {
lineNum++;
line = line.trim();
if (line.length() == 0 || line.startsWith("#") || line.startsWith(";")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
String sectionName = line.substring(1, line.length() - 1).trim();
if (ini.containsKey(sectionName)) {
throw new IllegalArgumentException("Invalid INI file, duplicate section not allowed, line " + lineNum);
}
currentSection = ImmutableListMultimap.builder();
ini.put(sectionName, currentSection);
} else if (currentSection == null) {
throw new IllegalArgumentException("Invalid INI file, properties must be within a [section], line " + lineNum);
} else {
int equalsPosition = line.indexOf('=');
String key = (equalsPosition < 0 ? line.trim() : line.substring(0, equalsPosition).trim());
String value = (equalsPosition < 0 ? "" : line.substring(equalsPosition + 1).trim());
if (key.length() == 0) {
throw new IllegalArgumentException("Invalid INI file, empty key, line " + lineNum);
}
currentSection.put(key, value);
}
}
return MapStream.of(ini).mapValues(b -> b.build()).toMap();
}
//-------------------------------------------------------------------------
/**
* Obtains an instance, specifying the map of section to properties.
*
* @param sectionMap the map of sections
* @return the INI file
*/
public static IniFile of(Map<String, PropertySet> sectionMap) {
return new IniFile(ImmutableMap.copyOf(sectionMap));
}
//-------------------------------------------------------------------------
/**
* Restricted constructor.
*
* @param sectionMap the sections
*/
private IniFile(ImmutableMap<String, PropertySet> sectionMap) {
this.sectionMap = sectionMap;
}
//-------------------------------------------------------------------------
/**
* Returns the set of sections of this INI file.
*
* @return the set of sections
*/
public ImmutableSet<String> sections() {
return sectionMap.keySet();
}
/**
* Returns the INI file as a map.
* <p>
* The iteration order of the map matches that of the original file.
*
* @return the INI file sections
*/
public ImmutableMap<String, PropertySet> asMap() {
return sectionMap;
}
//-------------------------------------------------------------------------
/**
* Checks if this INI file contains the specified section.
*
* @param name the section name
* @return true if the section exists
*/
public boolean contains(String name) {
ArgChecker.notNull(name, "name");
return sectionMap.containsKey(name);
}
/**
* Gets a single section of this INI file.
* <p>
* This returns the section associated with the specified name.
* If the section does not exist an exception is thrown.
*
* @param name the section name
* @return the INI file section
* @throws IllegalArgumentException if the section does not exist
*/
public PropertySet section(String name) {
ArgChecker.notNull(name, "name");
if (contains(name) == false) {
throw new IllegalArgumentException("Unknown INI file section: " + name);
}
return sectionMap.get(name);
}
//-------------------------------------------------------------------------
/**
* Checks if this INI file equals another.
* <p>
* The comparison checks the content.
*
* @param obj the other file, null returns false
* @return true if equal
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof IniFile) {
return sectionMap.equals(((IniFile) obj).sectionMap);
}
return false;
}
/**
* Returns a suitable hash code for the INI file.
*
* @return the hash code
*/
@Override
public int hashCode() {
return sectionMap.hashCode();
}
/**
* Returns a string describing the INI file.
*
* @return the descriptive string
*/
@Override
public String toString() {
return sectionMap.toString();
}
}