com.github.vincentrussell.ini.Ini Maven / Gradle / Ivy
package com.github.vincentrussell.ini;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.text.StringSubstitutor;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.firstNonNull;
/**
* This class is responsible for converting input streams into maps of maps that represent ini files.
*/
public class Ini {
private static final String NO_SECTION = "_NO_SECTION";
private static Pattern SECTION_PATTERN = Pattern.compile( "\\s*\\[([^]]*)\\]\\s*" );
private static Pattern KEY_VALUE_PATTER = Pattern.compile( "\\s*([^=]*)=(.*)" );
private static Pattern COMMENT_LINE = Pattern.compile("^[;|#].*");
private Map> resultMap = new LinkedHashMap<>();
/**
* default constructor
*/
public Ini() {
}
/**
* default constructor with an {@link InputStream}
* @param inputStream the ini file as an input stream
* @throws IOException thrown when there is an error processing the ini.
*/
public void load(final InputStream inputStream) throws IOException {
if (inputStream == null) {
throw new FileNotFoundException("inputStream is null");
}
MutableObject section = new MutableObject<>(NO_SECTION);
try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
parseIniFile(section, bufferedReader);
}
}
/**
* default constructor with {@link File}
* @param file the ini file
* @throws IOException thrown when there is an error processing the ini.
*/
public void load(final File file) throws IOException {
load(new FileInputStream(file));
}
/**
* default constructor with {@link String}
* @param string the contents of the ini file
* @throws IOException thrown when there is an error processing the ini.
*/
public void load(final String string) throws IOException {
load(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)));
}
/**
* merge this {@link Ini} with another {@link Ini}. If there are any merge conflicts the passed in {@link Ini}
* will take precedence.
* @param ini the ini to merge
*/
public void merge(final Ini ini) {
Collection sectionKeys = firstNonNull(ini.getSections(), Collections.emptyList());
for (String sectionKey : sectionKeys) {
Map section = firstNonNull(ini.getSection(sectionKey), Collections.emptyMap());
Set> entrySet = section.entrySet();
for (Map.Entry entry : entrySet ) {
putValue(sectionKey, entry.getKey(), entry.getValue());
}
}
}
private void parseIniFile(final MutableObject section,
final BufferedReader bufferedReader) throws IOException {
final Map variables = new HashMap<>();
variables.putAll(System.getenv());
variables.putAll(new HashMap((Map) System.getProperties()));
String line = null;
String multilineValue = null;
String key = null;
Map stringStringSubstitutorPerSection = new HashMap<>();
while ((line = bufferedReader.readLine()) != null ) {
final Matcher commentMatcher = COMMENT_LINE.matcher(line);
if (commentMatcher.matches()) {
continue;
}
final Matcher sectionMather = SECTION_PATTERN.matcher(line);
if (sectionMather.matches()) {
section.setValue(sectionMather.group(1).trim());
continue;
}
if (line != null) {
line = line.replaceAll("[^\\\\]{1}#.+", "")
.replaceAll("[^\\\\]{1};.+", "")
.replaceAll("\\\\([;#]{1})", "$1");
}
if (StringUtils.isEmpty(line)) {
continue;
}
final Matcher keyValueMatcher = KEY_VALUE_PATTER.matcher(line);
if (keyValueMatcher.matches()) {
key = keyValueMatcher.group(1).trim();
String value = handleEscapedAndSpecialCharacters(keyValueMatcher.group(2).trim()).replaceAll("\\\\$", "\n");
multilineValue = value;
if (line.endsWith("\\")) {
continue;
}
} else if (line.endsWith("\\")) {
multilineValue+= line.replaceAll("\\\\$", "\n");
continue;
} else if (multilineValue != null){
multilineValue+= line.replaceAll("\\\\$", "\n");
}
if (StringUtils.isNotEmpty(key)) {
Object normalizedValue = normalizeValue(multilineValue);
if (String.class.isInstance(normalizedValue) && line.contains("${")) {
StringSubstitutor substitutor = stringStringSubstitutorPerSection.computeIfAbsent(
section.getValue(), s -> {
StringSubstitutor stringSubstitutor = new StringSubstitutor(
new DelegateMapWrapper(variables, getMapForSection(section)));
stringSubstitutor.setEnableSubstitutionInVariables(true);
return stringSubstitutor;
});
normalizedValue = substitutor.replace(normalizedValue.toString());
}
getMapForSection(section).put(key, normalizedValue);
}
key = null;
multilineValue = null;
}
}
private Map getMapForSection(final MutableObject section) {
return resultMap.computeIfAbsent(
section.getValue(), s1 -> new LinkedHashMap<>());
}
private String handleEscapedAndSpecialCharacters(final String string) {
return string.replaceAll("^\"(.*)\"$", "$1")
.replaceAll("^'(.*)'$", "$1")
.replaceAll("\\\\\"", "\"")
.replaceAll("\\\\\'", "\'")
.replaceAll("\\\\\\\\", "\\\\")
.replaceAll("\\\\t", "\t")
.replaceAll("\\\\r", "\r")
.replaceAll("\\\\n", "\n")
.replaceAll("\\\\0", "\0")
.replaceAll("\\\\b", "\b")
.replaceAll("\\\\f", "\f")
.replaceAll("\\\\#", "#")
.replaceAll("\\\\=", "=")
.replaceAll("\\\\:", ":");
}
private Object normalizeValue(final String value) {
if (NumberUtils.isCreatable(value)) {
try {
return NumberFormat.getInstance().parse(value);
} catch (ParseException e) {
return value;
}
}
return value;
}
/**
* return true if the section and section key exists, false otherwise
* @param section the desired section
* @param key the key in the section
* @return true if the section and section key exists, false otherwise
*/
public boolean hasKey(final String section, final String key) {
return (resultMap.containsKey(section) && resultMap.get(section).containsKey(key));
}
/**
* return a value from the nested structure as an object
* @param section the desired section
* @param key the desired key in the section
* @return the value from the nested structure and cast it to the specified type.
*/
public Object getValue(final String section, final String key) {
return getValue(section, key, Object.class);
}
/**
* return a value from the nested structure and cast it to the specified type.
* @param section the desired section
* @param key the key in the section
* @param type the desired type
* @param the generic for the type
* @return the value from the nested structure and cast it to the specified type.
*/
public T getValue(final String section, final String key, final Class type) {
return cast(resultMap.getOrDefault(section, new LinkedHashMap<>()).get(key), type);
}
@SuppressWarnings("unchecked")
private T cast(final Object o, final Class type) {
if (o == null) {
return null;
}
if (type.isInstance(o)) {
return (T) o;
} else if (String.class.equals(type)) {
return (T) o.toString();
} else if (o != null && ClassUtils.isPrimitiveWrapper(o.getClass())
&& o.getClass().equals(ClassUtils.primitiveToWrapper(type))) {
return (T) o;
} else if (Number.class.isInstance(o)) {
if (Long.class.isAssignableFrom(type) || long.class.isAssignableFrom(type)) {
return (T) (Long) ((Number)o).longValue();
} else if (Integer.class.isAssignableFrom(type) || int.class.isAssignableFrom(type)) {
return (T) (Integer) ((Number)o).intValue();
} else if (Double.class.isAssignableFrom(type) || double.class.isAssignableFrom(type)) {
return (T) (Double) ((Number)o).doubleValue();
} else if (Float.class.isAssignableFrom(type) || float.class.isAssignableFrom(type)) {
return (T) (Float) ((Number)o).floatValue();
} else if (Short.class.isAssignableFrom(type) || short.class.isAssignableFrom(type)) {
return (T) (Short) ((Number)o).shortValue();
} else if (Byte.class.isAssignableFrom(type) || byte.class.isAssignableFrom(type)) {
return (T) (Byte) ((Number) o).byteValue();
}
} else if (o != null && (Boolean.class.isAssignableFrom(type) || boolean.class.isAssignableFrom(type))) {
return (T) Boolean.valueOf(o.toString());
} else if (o != null && Character.class.isAssignableFrom(type)) {
return (T) Character.valueOf(o.toString().charAt(0));
} else if (o != null && char.class.isAssignableFrom(type)) {
return (T) Character.valueOf(o.toString().charAt(0));
}
return (T) o;
}
/**
* get the sections from the ini file.
* @return the sections as a collection.
*/
public Collection getSections() {
return resultMap.keySet();
}
/**
* get the keys from a particular section.
* @param section the desired section
* @return the keys for a section or an empty collection.
*/
public Collection getKeys(final String section) {
return resultMap.getOrDefault(section, new LinkedHashMap<>()).keySet();
}
/**
* return the section as a map.
* @param section the desired section
* @return null if not found
*/
public Map getSection(final String section) {
Map map = resultMap.get(section);
if (map == null) {
return null;
}
return Collections.unmodifiableMap(map);
}
/**
* return the section as a map; sorted by key
* @param section the desired section
* @return null if not found
*/
public Map getSectionSortedByKey(final String section) {
Map map = resultMap.get(section);
if (map == null) {
return null;
}
return Collections.unmodifiableMap(new TreeMap<>(map));
}
/**
* get a subset of a section where all the keys match the provided prefix
* @param section the desired section
* @param prefix that the key starts with
* @return a subset of a section where all the keys match the provided prefix
*/
public Map getSectionWithKeysWithPrefix(final String section, final String prefix) {
return getSectionWithKeysThatMatchFunction(section, entry -> entry.getKey().startsWith(prefix));
}
/**
* get a subset of a section where all the keys match the provided filter
* @param section
* @param filter the filter that the Map.Entry in she specified section must match
* @return
*/
public Map getSectionWithKeysThatMatchFunction(final String section,
final Predicate> filter) {
final Map stringObjectMap = firstNonNull(resultMap.get(section), new LinkedHashMap<>());
return stringObjectMap.entrySet().stream()
.filter(map -> filter.test(map))
.collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue()));
}
/**
* get a subset of a section where all the keys match the provided prefix
* @param section the desired section
* @param regex that matches the key
* @return a subset of a section where all the keys match the provided prefix
*/
public Map getSectionWithKeysWithRegex(final String section, final String regex) {
final Pattern pattern = Pattern.compile(regex);
return getSectionWithKeysThatMatchFunction(section, entry -> pattern.matcher(entry.getKey()).matches());
}
/**
* store new values in the ini
* @param section the desired section
* @param key the key in the section
* @param value the value to store for that key
*/
public void putValue(final String section, final String key, final Object value) {
resultMap.computeIfAbsent(section, s -> new LinkedHashMap<>()).put(key, value);
}
/**
* put multiple key/value pairs into a section.
* @param sectionEntries
*/
public void putValues(final String sectionKey, Map sectionEntries) {
resultMap.computeIfAbsent(sectionKey, s -> new LinkedHashMap<>()).putAll(sectionEntries);
}
/**
* store the ini to an outputstream
* @param outputStream the outputstream to write to
* @param comments the comments to put at the top of the file
* @throws IOException if there is an error writing to the outputstream
*/
public void store(final OutputStream outputStream, final String comments) throws IOException {
store(new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)),
comments);
}
/**
* store the ini to a writer
* @param writer the writer to use
* @param comments commments to put at the top of the file.
* @throws IOException if there is an error writing to the writer
*/
public void store(final Writer writer, final String comments) throws IOException {
try (BufferedWriter bufferedWriter = (writer instanceof BufferedWriter)
? (BufferedWriter) writer : new BufferedWriter(writer)) {
doStore(bufferedWriter, comments);
}
}
private void doStore(final BufferedWriter bufferedWriter, final String comments) throws IOException {
writeComments(bufferedWriter, comments);
for (Map.Entry> section : resultMap.entrySet()) {
bufferedWriter.write("[" + section.getKey() + "]");
bufferedWriter.newLine();
for (Map.Entry sectionEntry : section.getValue().entrySet()) {
bufferedWriter.write(sectionEntry.getKey() + " = ");
bufferedWriter.write(sectionEntry.getValue().toString());
bufferedWriter.newLine();
}
bufferedWriter.newLine();
}
}
private static void writeComments(final BufferedWriter bw, final String comments)
throws IOException {
if (StringUtils.isNotEmpty(comments)) {
try (BufferedReader bufferedReader = new BufferedReader(new StringReader(comments))) {
String line = null;
while((line = bufferedReader.readLine()) != null) {
bw.write("#");
IOUtils.write(line, bw);
bw.newLine();
}
}
}
}
/**
* Removes the specified section.
*
* The ini object will not contain a mapping for the specified section once the call returns.
* @param section the section to remove
* @return the previous value associated with key, or null if there was no mapping for key.
*/
public Map removeSection(final String section) {
return resultMap.remove(section);
}
/**
* Removes the specified key in the specified section.
* The ini object will not contain a mapping for the specified section once the call returns.
* @param section the section to remove
* @param key the key in that section to remove
* @return the previous value associated with key, or null if there was no mapping for key.
*/
public Object removeSectionKey(final String section, final String key) {
return resultMap.getOrDefault(section, new LinkedHashMap<>()).remove(key);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy