net.morimekta.config.format.TomlConfigParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of config-util Show documentation
Show all versions of config-util Show documentation
Configuration Utilities.
NOTE: This module is deprecated and will be removed at the end of the
v2.x versions of the utilities. Preferred config system after that is
either to use true type-safe config with `net.morimekta.providence:providence-config`
or to use a simple JSON or YAML library or java properties files. The
semi-typesafe layered config did not really solve the problems I had
hoped it would, and in essence this was just a helper for merging maps
and getting pre-cast values out of it.
/*
* Copyright (c) 2016, Stein Eldar Johnsen
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.config.format;
import com.google.common.collect.ImmutableList;
import net.morimekta.config.Config;
import net.morimekta.config.ConfigBuilder;
import net.morimekta.config.ConfigException;
import net.morimekta.config.impl.ImmutableConfig;
import net.morimekta.config.impl.SimpleConfig;
import net.morimekta.util.Strings;
import net.morimekta.util.io.IOUtils;
import net.morimekta.util.json.JsonException;
import net.morimekta.util.json.JsonToken;
import net.morimekta.util.json.JsonTokenizer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Config formatting for parsing (and formatting) .INI style config files.
* It follows more or less the TOML-lang
* syntax with some exceptions.
*
* The syntax is somewhat simpler than the TOML
*
*
* - The Structure is completely 2-tier, section and property.
* - No support for multi-line basic strings (triple-quoted).
* - No support for literal strings (quoted with "'").
* - Integers follow JSON syntax only, so no '_' separators, no hex.
*
* - Property keys are never quoted, but supports containing
* '.' regardless.
* - The end result is a flat map of
*
section-key + '.' + property-key = value
. Or
* property-key = value
for properties before the first section.
*
*
*
* TOML IntelliJ plugin
*
* Example .toml file.
*
*
* # comment
* key.outside.section = 5
*
* [section]
* key.string = "value with\n\t - escaping"
* key.sequence = [ "sequence", "follows", "JSON", "syntax" ]
*
* # comments can be anywhere, as long as it's on it's own line.
* key.int = 1234567890
*
*
* The keys inside a section is prepended with the section name and '.'. This
* way the real key of the 'key.string' property is going to be
* 'section.key.string'. The structure is completely flattened, no section
* structure is kept.
*/
public class TomlConfigParser implements ConfigParser {
@Override
public Config parse(InputStream in) {
try {
ConfigBuilder config = new SimpleConfig();
// Part 1: Strip comments.
String all = IOUtils.readString(in);
String[] lines = all.split("[\\n]");
for (int i = 0; i < lines.length; ++i) {
lines[i] = stripComment(lines[i]);
}
// Insert double newlines, to make it easier to detect garbage.
all = String.join("\n\n", (CharSequence[]) lines);
// Part 2: Parse remaining data.
ByteArrayInputStream bais = new ByteArrayInputStream(all.getBytes(UTF_8));
JsonTokenizer tokenizer = new JsonTokenizer(bais);
String currentSection = null;
JsonToken token = tokenizer.next();
while (token != null) {
if (token.isSymbol('[')) {
currentSection = tokenizer.expect("section name").asString();
tokenizer.expectSymbol("end of section", ']');
String rest = tokenizer.restOfLine().replaceAll("#.*", "");
if (rest.length() > 0) {
throw new ConfigException("Garbage after section: " + Strings.escape(rest));
}
token = tokenizer.next();
continue;
}
String key = entryKey(currentSection, token.asString());
tokenizer.expectSymbol("key/value separator", ':', '=');
try {
config.put(key, parseValue(tokenizer));
String rest = tokenizer.restOfLine().replaceAll("#.*", "");
if (rest.length() > 0) {
throw new ConfigException("Garbage after value: " + Strings.escape(rest));
}
} catch (JsonException je) {
// TOML dates are invalid JSON, but if we pick up a
// JsonException which has detected the year-mm-dd
// separator char, we can manage to parse this
// correctly.
if (je.getMessage().startsWith("Wrongly terminated JSON number:")) {
IOUtils.readString(bais, "\n");
// Since the tokenizer is in a bad state (unconsumed
// token char that should be ignored) it has to be
// reset.
tokenizer = new JsonTokenizer(bais);
// Hack out the value part of the line.
String date = je.getLine().split("[=]", 2)[1].trim();
try {
LocalDateTime time;
if (date.endsWith("Z")) {
time = LocalDateTime.parse(date,
DateTimeFormatter.ISO_INSTANT.withZone(Clock.systemUTC()
.getZone()));
} else {
time = LocalDateTime.parse(date,
DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(Clock.systemUTC()
.getZone()));
}
config.put(key,
new Date(time.atZone(Clock.systemUTC().getZone())
.toInstant()
.toEpochMilli()));
} catch (RuntimeException e) {
throw new ConfigException(je, je.getMessage());
}
} else {
throw new ConfigException(je, je.getMessage());
}
}
token = tokenizer.next();
}
return ImmutableConfig.copyOf(config);
} catch (JsonException | IOException e) {
throw new ConfigException(e, e.getMessage());
}
}
private Object parseValue(JsonTokenizer tokenizer) throws IOException, JsonException {
JsonToken token = tokenizer.expect("TOML value");
if (token.isSymbol('[')) {
ImmutableList.Builder