ConfigParser.java
/*
* Copyright 2017 Providence Authors
*
* 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.providence.config.parser;
import net.morimekta.providence.PEnumValue;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PType;
import net.morimekta.providence.config.parser.ConfigUtil.Stage;
import net.morimekta.providence.config.util.ContentResolver;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PList;
import net.morimekta.providence.descriptor.PMap;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PSet;
import net.morimekta.providence.types.TypeRegistry;
import net.morimekta.util.Binary;
import net.morimekta.util.Pair;
import net.morimekta.util.io.Utf8StreamReader;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static net.morimekta.providence.config.parser.ConfigUtil.AS;
import static net.morimekta.providence.config.parser.ConfigUtil.DEF;
import static net.morimekta.providence.config.parser.ConfigUtil.FALSE;
import static net.morimekta.providence.config.parser.ConfigUtil.IDENTIFIER_SEP;
import static net.morimekta.providence.config.parser.ConfigUtil.INCLUDE;
import static net.morimekta.providence.config.parser.ConfigUtil.RESERVED_WORDS;
import static net.morimekta.providence.config.parser.ConfigUtil.TRUE;
import static net.morimekta.providence.config.parser.ConfigUtil.UNDEFINED;
import static net.morimekta.providence.config.parser.ConfigUtil.asType;
import static net.morimekta.providence.config.parser.ConfigUtil.consumeValue;
import static net.morimekta.providence.config.parser.ConfigUtil.nextNotSeparator;
import static net.morimekta.providence.types.TypeReference.parseType;
/**
* This parser parses config files. The class in itself should be stateless, so
* can safely be used in multiple threads safely. This is a utility class created
* in order to simplify testing.
*/
public class ConfigParser {
/**
* Create a providence config parser instance.
*
* @param registry The type registry used.
* @param contentResolver Content resolver.
* @param warningHandler Handle parse warnings.
* @param strict If config should be parsed and handled strictly.
*/
public ConfigParser(TypeRegistry registry,
ContentResolver contentResolver,
Consumer<ConfigWarning> warningHandler,
boolean strict) {
this.registry = registry;
this.warningHandler = warningHandler;
this.contentResolver = contentResolver;
this.strict = strict;
}
/**
* Parse a providence config into a message.
*
* @param configFile The config file to be parsed.
* @param parent The parent config message.
* @param <M> The config message type.
* @return Pair of parsed config and set of included file paths.
* @throws ConfigException If parsing failed.
*/
@Nonnull
public <M extends PMessage<M>> Pair<M, Set<String>> parseConfig(
@Nonnull Path configFile,
@Nullable M parent)
throws ConfigException {
Path referenceLocation;
try {
referenceLocation = contentResolver.canonical(configFile.getParent());
configFile = contentResolver.canonical(configFile);
} catch (IOException e) {
throw new ConfigException(e, "Unable to resolve config file %s", configFile)
.setFile(configFile.getFileName()
.toString());
}
return checkAndParseInternal(configFile, referenceLocation, parent);
}
// --- private
private <M extends PMessage<M>>
Pair<M, Set<String>> checkAndParseInternal(@Nonnull Path configFile,
@Nonnull Path referenceLocation,
@Nullable M parent,
String... includeStack)
throws ConfigException {
try {
// So we map actual loaded files by the absolute canonical location.
String canonicalFile = contentResolver.canonical(configFile).toString();
List<String> stackList = new ArrayList<>();
Collections.addAll(stackList, includeStack);
if (Arrays.binarySearch(includeStack, canonicalFile) >= 0) {
stackList.add(canonicalFile);
throw new ConfigException("Circular includes detected: " +
stackList.stream()
.map(p -> new File(p).getName())
.collect(Collectors.joining(" -> ")));
}
stackList.add(canonicalFile);
return parseConfigRecursively(configFile, referenceLocation, parent, stackList.toArray(new String[0]));
} catch (ConfigException te) {
if (te.getFile() == null) {
te.setFile(configFile.getFileName().toString());
}
throw te;
} catch (IOException e) {
throw new ConfigException(e, e.getMessage())
.setFile(configFile.getFileName().toString());
}
}
@Nullable
@SuppressWarnings("unchecked")
private <M extends PMessage<M>>
Pair<M, Set<String>> parseConfigRecursively(@Nonnull Path file,
@Nonnull Path referenceLocation,
M parent,
String[] stack)
throws IOException {
ConfigLexer lexer;
try (BufferedInputStream in = new BufferedInputStream(contentResolver.open(file))) {
// Non-enclosed content, meaning we should read the whole file immediately.
lexer = new ConfigLexer(new Utf8StreamReader(in));
}
ConfigContext context = new ConfigContext();
Set<String> includedFilePaths = new TreeSet<>();
includedFilePaths.add(contentResolver.canonical(file).toString());
Stage lastStage = Stage.INCLUDES;
M result = null;
ConfigToken token = lexer.peek();
while (token != null) {
lexer.next();
if (lastStage == Stage.MESSAGE) {
throw new ConfigException(token, "Unexpected token '" + token.toString() + "', expected end of file.");
} else if (INCLUDE.equals(token.toString())) {
// if include && stage == INCLUDES --> INCLUDES
if (lastStage != Stage.INCLUDES) {
throw new ConfigException(
token, "Include added after defines or message. Only one def block allowed.");
}
token = lexer.expect("file to be included", ConfigTokenType.STRING);
String includedFilePath = token.decodeString(strict);
PMessage included;
Path includedFile;
try {
includedFile = contentResolver.canonical(contentResolver.reference(referenceLocation, file, includedFilePath));
Path resolvedReference = contentResolver.referenceLocationPath(referenceLocation, file, includedFilePath);
Pair<PMessage, Set<String>> tmp = checkAndParseInternal(includedFile, resolvedReference, null, stack);
if (tmp != null) {
includedFilePaths.add(includedFile.toString());
includedFilePaths.addAll(tmp.second);
included = tmp.first;
} else {
warn(file, token, "No content in included file: %s", includedFile.getFileName().toString());
included = null;
}
} catch (FileNotFoundException e) {
throw new ConfigException(token, "Included file \"%s\" not found.", includedFilePath);
}
token = lexer.expect("the token 'as'", ConfigToken::isIdentifier);
if (!AS.equals(token.toString())) {
throw new ConfigException(token,
"Expected token 'as' after included file \"%s\".",
includedFilePath);
}
token = lexer.expect("Include alias", ConfigToken::isIdentifier);
String alias = token.toString();
if (RESERVED_WORDS.contains(alias)) {
throw new ConfigException(token, "Alias \"%s\" is a reserved word.", alias);
}
if (context.containsReference(alias)) {
throw new ConfigException(token, "Alias \"%s\" is already used.", alias);
}
context.setInclude(alias, included);
} else if (DEF.equals(token.toString())) {
// if params && stage == DEF --> DEF
lastStage = Stage.DEFINES;
parseDefinitions(file, context, lexer);
} else if (token.isQualifiedIdentifier()) {
// if a.b (type identifier) --> MESSAGE
lastStage = Stage.MESSAGE;
PMessageDescriptor<M> descriptor;
try {
descriptor = registry.requireMessageType(parseType(token.toString()));
} catch (IllegalArgumentException e) {
// Unknown declared type. Fail if:
// - strict mode, all files must be of known types.
// - top of the stack. This is the config requested by the user. It should fail
// even in non-strict mode.
if (strict || stack.length == 1) {
throw new ConfigException(token, "Unknown declared type: %s", token.toString());
}
warn(file, token, "Unknown declared type: %s", token.toString());
return null;
}
result = parseConfigMessage(file, lexer, context, descriptor.builder(), parent);
} else {
throw new ConfigException(token,
"Unexpected token '" + token.toString() +
"'. Expected include, defines or message type");
}
token = lexer.peek();
}
if (result == null) {
throw new ConfigException("No message in config: " + file.getFileName().toString());
}
return Pair.create(result, includedFilePaths);
}
private void parseDefinitions(Path file, ConfigContext context, ConfigLexer lexer) throws IOException {
ConfigToken token = lexer.expect("defines group start or identifier");
if (token.isIdentifier()) {
String name = context.initReference(token);
lexer.expectSymbol("def value sep", ConfigToken.kFieldValueSep);
context.setReference(name, parseDefinitionValue(file, context, lexer));
} else if (token.isSymbol(ConfigToken.kMessageStart)) {
token = lexer.expect("define or end");
while (!token.isSymbol(ConfigToken.kMessageEnd)) {
if (!token.isIdentifier()) {
throw new ConfigException(token, "Token '%s' is not valid reference name.", token.toString());
}
String name = context.initReference(token);
lexer.expectSymbol("def value sep", ConfigToken.kFieldValueSep);
context.setReference(name, parseDefinitionValue(file, context, lexer));
token = lexer.expect("next define or end");
}
} else {
throw new ConfigException(token, "Unexpected token after def: '%s'", token.toString());
}
}
@SuppressWarnings("unchecked")
private Object parseDefinitionValue(Path file,
ConfigContext context,
ConfigLexer lexer) throws IOException {
ConfigToken token = lexer.expect("Start of def value");
if (token.isReal()) {
return Double.parseDouble(token.toString());
} else if (token.isInteger()) {
return Long.parseLong(token.toString());
} else if (token.isString()) {
return token.decodeString(strict);
} else if (TRUE.equalsIgnoreCase(token.toString())) {
return Boolean.TRUE;
} else if (FALSE.equalsIgnoreCase(token.toString())) {
return Boolean.FALSE;
} else if (ConfigToken.B64.equals(token.toString())) {
lexer.expectSymbol("binary data enclosing start", ConfigToken.kParamsStart);
ConfigToken binary = lexer.readBinary(ConfigToken.kParamsEnd);
if (binary == null) return Binary.empty();
return Binary.fromBase64(binary.toString().replaceAll("[\\s=]", ""));
} else if (ConfigToken.HEX.equals(token.toString())) {
lexer.expectSymbol("binary data enclosing start", ConfigToken.kParamsStart);
ConfigToken binary = lexer.readBinary(ConfigToken.kParamsEnd);
if (binary == null) return Binary.empty();
return Binary.fromHexString(binary.toString().replaceAll("[\\s]", ""));
} else if (token.isDoubleQualifiedIdentifier()) {
// this may be an enum reference, must be
// - package.EnumType.IDENTIFIER
String id = token.toString();
int l = id.lastIndexOf(ConfigToken.kIdentifierSep);
try {
PEnumDescriptor ed = registry.requireEnumType(parseType(id.substring(0, l)));
PEnumValue val = ed.findByName(id.substring(l + 1));
if (val == null) {
if(strict) {
throw new ConfigException(token, "Unknown %s value: %s", id.substring(0, l), id.substring(l + 1));
} else {
warn(file, token, "Unknown %s value: %s", id.substring(0, l), id.substring(l + 1));
}
}
// Note that unknown enum value results in null. Therefore we don't catch null values here.
return val;
} catch (IllegalArgumentException e) {
// No such declared type.
if (strict) {
throw new ConfigException(token, "Unknown enum identifier: %s", id.substring(0, l));
} else {
warn(file, token, "Unknown enum identifier: %s", id.substring(0, l));
}
consumeValue(lexer, token);
} catch (ClassCastException e) {
// Not an enum.
throw new ConfigException(token, "Identifier " + id + " does not reference an enum, from " + token.toString());
}
} else if (token.isQualifiedIdentifier()) {
// Message type.
PMessageDescriptor descriptor;
try {
descriptor = registry.requireMessageType(parseType(token.toString()));
} catch (IllegalArgumentException e) {
// Unknown declared type. Fail if:
// - strict mode: all types must be known.
if (strict) {
throw new ConfigException(token, "Unknown declared type: %s", token.toString());
}
consumeValue(lexer, token);
return null;
}
PMessageBuilder builder = descriptor.builder();
if (lexer.expectSymbol("message start or inherits", '{', ':').isSymbol(':')) {
token = lexer.expect("inherits reference", ConfigToken::isReferenceIdentifier);
PMessage inheritsFrom = resolve(context, token, descriptor);
if (inheritsFrom == null) {
throw new ConfigException(token, "Inheriting from null reference: %s", token.toString());
}
builder.merge(inheritsFrom);
lexer.expectSymbol("message start", '{');
}
return parseMessage(file, lexer, context, builder);
} else {
throw new ConfigException(token, "Invalid define value " + token.toString());
}
return null;
}
private <M extends PMessage<M>> M parseConfigMessage(Path file,
ConfigLexer lexer,
ConfigContext context,
PMessageBuilder<M> builder,
M parent) throws IOException {
if (lexer.expectSymbol("extension marker", ConfigToken.kKeyValueSep, ConfigToken.kMessageStart).isSymbol(ConfigToken.kKeyValueSep)) {
ConfigToken token = lexer.expect("extension object");
if (parent != null) {
throw new ConfigException(token, "Config has both defined parent and inherits from");
}
if (token.isReferenceIdentifier() && !RESERVED_WORDS.contains(token.toString())) {
try {
builder.merge(resolveRequired(context, token, builder.descriptor()));
} catch (ClassCastException e) {
throw new ConfigException(token, "Config type mismatch, expected " + builder.descriptor().getQualifiedName());
}
lexer.expectSymbol("object begin", ConfigToken.kMessageStart);
} else {
throw new ConfigException(
token, "Unexpected token " + token.toString() + ", expected reference identifier");
}
} else if (parent != null) {
if (!builder.descriptor().equals(parent.descriptor())) {
throw new ConfigException("Loaded config type %s does not match parent %s",
parent.descriptor().getQualifiedName(),
builder.descriptor().getQualifiedName());
}
builder.merge(parent);
}
return parseMessage(file, lexer, context, builder);
}
@SuppressWarnings("unchecked")
private <M extends PMessage<M>>
M parseMessage(@Nonnull Path file,
@Nonnull ConfigLexer lexer,
@Nonnull ConfigContext context,
@Nonnull PMessageBuilder<M> builder) throws IOException {
PMessageDescriptor<M> descriptor = builder.descriptor();
ConfigToken token = lexer.expect("object end or field");
while (!token.isSymbol(ConfigToken.kMessageEnd)) {
if (!token.isIdentifier()) {
throw new ConfigException(token, "Invalid field name: " + token.toString());
}
PField field = descriptor.findFieldByName(token.toString());
if (field == null) {
if (strict) {
throw lexer.failure(token, "No such field " + token.toString() + " in " + descriptor.getQualifiedName());
} else {
warn(file, token, "No such field " + token.toString() + " in " + descriptor.getQualifiedName());
token = lexer.expect("field value sep, message start or reference start");
if (token.isSymbol(ConfigToken.kFieldValueSep)) {
token = lexer.expect("value declaration");
} else if (!token.isSymbol(ConfigToken.kMessageStart)) {
throw new ConfigException(token, "Expected field-value separator or inherited message");
}
// Non-strict will just consume unknown fields, this way
// we can be forward-compatible when reading config.
consumeValue(lexer, token);
token = nextNotSeparator(lexer, "field or message end");
continue;
}
}
if (field.getType() == PType.MESSAGE) {
// go recursive with optional
char symbol = lexer.expectSymbol("Message assigner or start",
ConfigToken.kFieldValueSep,
ConfigToken.kMessageStart)
.charAt(0);
PMessageBuilder bld;
if (symbol == ConfigToken.kFieldValueSep) {
token = lexer.expect("reference or message start");
if (UNDEFINED.equals(token.toString())) {
// unset.
builder.clear(field.getId());
// special casing this, as we don't want to duplicate the parse line below.
token = nextNotSeparator(lexer, "field or message end");
continue;
}
// overwrite with new.
bld = ((PMessageDescriptor) field.getDescriptor()).builder();
if (token.isReferenceIdentifier() && !RESERVED_WORDS.contains(token.toString())) {
// Inherit from reference.
PMessage ref = resolveRequired(context, token, field.getDescriptor());
bld.merge(ref);
token = lexer.expect("after message reference");
// if the following symbol is *not* message start,
// we assume a new field or end of current message.
if (!token.isSymbol(ConfigToken.kMessageStart)) {
builder.set(field.getId(), bld.build());
continue;
}
} else if (!token.isSymbol(ConfigToken.kMessageStart)) {
throw new ConfigException(token,
"Unexpected token " + token.toString() +
", expected message start");
}
} else {
// extend in-line.
bld = builder.mutator(field.getId());
}
builder.set(field.getId(), parseMessage(file, lexer, context, bld));
} else if (field.getType() == PType.MAP) {
// maps can be extended the same way as
token = lexer.expect("field sep or value start");
Map baseValue = new LinkedHashMap<>();
if (token.isSymbol(ConfigToken.kFieldValueSep)) {
token = lexer.expect("field id or start");
if (UNDEFINED.equals(token.toString())) {
builder.clear(field.getId());
token = lexer.expect("message end or field");
continue;
} else if (token.isReferenceIdentifier() && !RESERVED_WORDS.contains(token.toString())) {
try {
baseValue = resolve(context, token, field.getDescriptor());
} catch (ConfigException e) {
throw new ConfigException(token, e.getMessage()).initCause(e);
}
token = lexer.expect("map start or next field");
if (!token.isSymbol(ConfigToken.kMessageStart)) {
builder.set(field.getId(), baseValue);
continue;
} else if (baseValue == null) {
baseValue = new LinkedHashMap<>();
}
}
} else {
baseValue.putAll(builder.build().get(field.getId()));
}
if (!token.isSymbol(ConfigToken.kMessageStart)) {
throw new ConfigException(token, "Expected map start, but got '%s'", token.toString());
}
Map map = parseMapValue(file, lexer, context, (PMap) field.getDescriptor(), baseValue);
builder.set(field.getId(), map);
} else {
// Simple fields *must* have the '=' separation, may have '&' reference.
lexer.expectSymbol("field value sep", ConfigToken.kFieldValueSep);
token = lexer.expect("field value");
if (UNDEFINED.equals(token.toString())) {
builder.clear(field.getId());
} else {
Object value = parseFieldValue(file, token, lexer, context, field.getDescriptor(), strict);
builder.set(field.getId(), value);
}
}
token = nextNotSeparator(lexer, "field or message end");
}
return builder.build();
}
@SuppressWarnings("unchecked")
private Map parseMapValue(Path file,
ConfigLexer lexer,
ConfigContext context,
PMap descriptor,
Map builder) throws IOException {
ConfigToken next = lexer.expect("map key or end");
while (!next.isSymbol(ConfigToken.kMessageEnd)) {
Object key = parseFieldValue(file, next, lexer, context, descriptor.keyDescriptor(), true);
lexer.expectSymbol("map key value sep", ConfigToken.kKeyValueSep);
next = lexer.expect("map value");
if (UNDEFINED.equals(next.toString())) {
builder.remove(key);
} else {
Object value;
if (next.isReferenceIdentifier() &&
!RESERVED_WORDS.contains(next.toString()) &&
// The last check is needed to avoid thinking enums values are references.
context.containsReference(next.toString())) {
value = context.getReference(next.toString(), next);
} else {
value = parseFieldValue(file, next, lexer, context, descriptor.itemDescriptor(), strict);
}
if (value != null) {
builder.put(key, value);
}
}
// maps do *not* require separator, but allows ',' separator, and separator after last.
next = lexer.expect("map key, end or sep");
if (next.isSymbol(ConfigToken.kEntrySep)) {
next = lexer.expect("map key or end");
}
}
return descriptor.builder(builder.size()).putAll(builder).build();
}
@SuppressWarnings("unchecked")
private Object parseFieldValue(Path file,
ConfigToken next,
ConfigLexer lexer,
ConfigContext context,
PDescriptor descriptor,
boolean requireEnumValue) throws IOException {
try {
switch (descriptor.getType()) {
case BOOL:
if (TRUE.equals(next.toString())) {
return true;
} else if (FALSE.equals(next.toString())) {
return false;
} else if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
}
break;
case BYTE:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isInteger()) {
return (byte) next.parseInteger();
}
break;
case I16:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isInteger()) {
return (short) next.parseInteger();
}
break;
case I32:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isInteger()) {
return (int) next.parseInteger();
}
break;
case I64:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isInteger()) {
return next.parseInteger();
}
break;
case DOUBLE:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isInteger() || next.isReal()) {
return next.parseDouble();
}
break;
case STRING:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isString()) {
return next.decodeString(strict);
}
break;
case BINARY:
if (ConfigToken.B64.equals(next.toString())) {
lexer.expectSymbol("binary data enclosing start", ConfigToken.kParamsStart);
ConfigToken binary = lexer.readBinary(ConfigToken.kParamsEnd);
if (binary == null) return Binary.empty();
return Binary.fromBase64(binary.toString());
} else if (ConfigToken.HEX.equals(next.toString())) {
lexer.expectSymbol("binary data enclosing start", ConfigToken.kParamsStart);
ConfigToken binary = lexer.readBinary(ConfigToken.kParamsEnd);
if (binary == null) return Binary.empty();
return Binary.fromHexString(binary.toString());
} else if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
}
break;
case ENUM: {
PEnumDescriptor ed = (PEnumDescriptor) descriptor;
PEnumValue value;
String name = next.toString();
if (next.isInteger()) {
value = ed.findById((int) next.parseInteger());
} else if (next.isIdentifier()) {
value = ed.findByName(name);
if (value == null && context.containsReference(name)) {
value = resolve(context, next, ed);
}
} else if (next.isReferenceIdentifier()) {
value = resolve(context, next, descriptor);
} else {
break;
}
if (value == null) {
PEnumValue option = null;
if (next.isIdentifier()) {
for (PEnumValue o : ed.getValues()) {
if (o.asString().equalsIgnoreCase(name)) {
option = o;
break;
}
}
}
if(strict || requireEnumValue) {
if (option != null) {
throw lexer.failure(next, "No such enum value '%s' for %s, did you mean '%s'?",
name,
ed.getQualifiedName(),
option.toString());
}
throw lexer.failure(next, "No such enum value '%s' for %s.",
name,
ed.getQualifiedName());
} else {
if (option != null) {
warn(file, next, "No such enum value '%s' for %s, did you mean '%s'?",
name,
ed.getQualifiedName(),
option.toString());
}
warn(file, next, "No such enum value '%s' for %s.",
name,
ed.getQualifiedName());
}
}
return value;
}
case MESSAGE:
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isSymbol(ConfigToken.kMessageStart)) {
return parseMessage(file, lexer, context, ((PMessageDescriptor) descriptor).builder());
}
break;
case MAP: {
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
Map resolved;
try {
// Make sure the reference is to a map.
resolved = resolve(context, next, descriptor);
} catch (ClassCastException e) {
throw new ConfigException(next, "Reference %s is not a map field ", next.toString());
}
return resolved;
} else if (next.isSymbol(ConfigToken.kMessageStart)) {
return parseMapValue(file, lexer, context, (PMap) descriptor, new LinkedHashMap());
}
break;
}
case SET: {
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isSymbol(ConfigToken.kListStart)) {
@SuppressWarnings("unchecked")
PSet<Object> ct = (PSet) descriptor;
PSet.Builder<Object> value = ((PSet) descriptor).builder(4);
next = lexer.expect("set value or end");
while (!next.isSymbol(ConfigToken.kListEnd)) {
Object item = parseFieldValue(file, next, lexer, context, ct.itemDescriptor(), strict);
if (item != null) {
value.add(item);
}
// sets require separator, and allows separator after last.
if (lexer.expectSymbol("set separator or end",
ConfigToken.kEntrySep, ConfigToken.kListEnd)
.isSymbol(ConfigToken.kListEnd)) {
break;
}
next = lexer.expect("set value or end");
}
return value.build();
}
break;
}
case LIST: {
if (next.isReferenceIdentifier() && !RESERVED_WORDS.contains(next.toString())) {
return resolve(context, next, descriptor);
} else if (next.isSymbol(ConfigToken.kListStart)) {
@SuppressWarnings("unchecked")
PList<Object> ct = (PList) descriptor;
PList.Builder<Object> builder = ct.builder(4);
next = lexer.expect("list value or end");
while (!next.isSymbol(ConfigToken.kListEnd)) {
Object item = parseFieldValue(file, next, lexer, context, ct.itemDescriptor(), strict);
if (item != null) {
builder.add(item);
}
// lists require separator, and allows separator after last.
if (lexer.expectSymbol("list separator or end",
ConfigToken.kEntrySep, ConfigToken.kListEnd)
.isSymbol(ConfigToken.kListEnd)) {
break;
}
next = lexer.expect("list value or end");
}
return builder.build();
}
break;
}
default: {
throw new ConfigException(next, descriptor.getType() + " not supported!");
}
}
} catch (ConfigException e) {
throw new ConfigException(next, e.getMessage());
}
throw new ConfigException(next, "Unhandled value \"%s\" for type %s",
next.toString(),
descriptor.getType());
}
@Nonnull
private static <V> V resolveRequired(ConfigContext context, ConfigToken token, PDescriptor descriptor)
throws ConfigException {
V result = resolve(context, token, descriptor);
if (result == null) {
throw new ConfigException(token, "No such reference " + token.toString());
}
return result;
}
/**
* Resolve a value reference.
*
* @param context The parsing context.
* @param token The ID token to look for.
* @param descriptor The item descriptor.
* @return The value at the given key, or exception if not found.
*/
@SuppressWarnings("unchecked")
private static <V> V resolve(ConfigContext context,
ConfigToken token,
PDescriptor descriptor) throws ConfigException {
if (!RESERVED_WORDS.contains(token.toString())) {
Object value = resolveAny(context, token);
if (value == null) {
return null;
}
return (V) asType(descriptor, value);
}
return null;
}
private static Object resolveAny(ConfigContext context, ConfigToken token)
throws ConfigException {
String key = token.toString();
String name = key;
String subKey = null;
if (key.contains(IDENTIFIER_SEP)) {
int idx = key.indexOf(IDENTIFIER_SEP);
name = key.substring(0, idx);
subKey = key.substring(idx + 1);
}
Object value = context.getReference(name, token);
if (subKey != null) {
if (!(value instanceof PMessage)) {
throw new ConfigException(token, "Reference name " + key + " not declared");
}
try {
return ConfigUtil.getInMessage((PMessage) value, subKey, null);
} catch (ConfigException e) {
throw new ConfigException(token, e.getMessage()).initCause(e);
}
}
return value;
}
private void warn(Path file, ConfigToken token, String format, Object... args) {
warningHandler.accept(new ConfigWarning(token, format, args)
.setFile(file.getFileName().toString()));
}
/**
* Type registry for looking up the base config types.
*/
private final TypeRegistry registry;
/**
* If config should be parsed strictly.
*/
private final boolean strict;
/**
* Content resolver resolves files and includes.
*/
private final ContentResolver contentResolver;
/**
* Handle warnings, which may print them to console or to other handlers.
*/
private final Consumer<ConfigWarning> warningHandler;
}