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;
}