All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.configuration.Config Maven / Gradle / Ivy

There is a newer version: 5.26.0
Show newest version
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.configuration;

import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.text.StringTokenizer;
import org.apache.commons.text.matcher.StringMatcherFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclEntryType;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.UserPrincipal;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.neo4j.annotations.api.IgnoreApiCheck;
import org.neo4j.graphdb.config.Configuration;
import org.neo4j.graphdb.config.Setting;
import org.neo4j.internal.helpers.Exceptions;
import org.neo4j.logging.Log;
import org.neo4j.service.Services;
import org.neo4j.util.Preconditions;

import static java.lang.String.format;
import static java.lang.System.lineSeparator;
import static java.util.Objects.requireNonNull;
import static org.neo4j.configuration.BootloaderSettings.additional_jvm;
import static org.neo4j.configuration.GraphDatabaseInternalSettings.config_command_evaluation_timeout;
import static org.neo4j.configuration.GraphDatabaseSettings.strict_config_validation;

@IgnoreApiCheck
public class Config implements Configuration
{
    public static final String DEFAULT_CONFIG_FILE_NAME = "neo4j.conf";
    public static final String DEFAULT_CONFIG_DIR_NAME = "conf";

    public static final class Builder
    {
        private final Collection> settingsClasses = new HashSet<>();
        private final Collection> groupSettingClasses = new HashSet<>();
        private final Collection settingMigrators = new HashSet<>();
        private final Map settingValueStrings = new HashMap<>();
        private final Map settingValueObjects = new HashMap<>();
        private final Map overriddenDefaults = new HashMap<>();
        private final List configFiles = new ArrayList<>();
        private Config fromConfig;
        private final Log log = new BufferingLog();
        private boolean expandCommands;

        private static  boolean allowedToOverrideValues( String setting, T value, Map settingValues )
        {
            if ( Objects.equals( setting, additional_jvm.name() ) )
            {
                T oldValue = settingValues.get( setting );
                if ( oldValue != null )
                {
                    if ( value instanceof String && oldValue instanceof String )
                    {
                        String newValue = oldValue + System.lineSeparator() + value;
                        //noinspection unchecked
                        settingValues.put( setting, (T) newValue ); //need to keep all jvm additionals
                    }
                    else
                    {
                        throw new IllegalArgumentException( additional_jvm.name() + " can only be provided as raw Strings if provided multiple times" );
                    }
                }
                return false;
            }
            return true;
        }

        private  void overrideSettingValue( String setting, T value, Map settingValues )
        {
            if ( !settingValueStrings.containsKey( setting ) && !settingValueObjects.containsKey( setting ) )
            {
                settingValues.put( setting, value );
            }
            else if ( allowedToOverrideValues( setting, value, settingValues ) )
            {
                log.warn( "The '%s' setting is overridden. Setting value changed from '%s' to '%s'.", setting,
                        settingValueStrings.containsKey( setting ) ? settingValueStrings.remove( setting ) : settingValueObjects.remove( setting ), value );
                settingValues.put( setting, value );
            }
        }

        private Builder setRaw( String setting, String value )
        {
            overrideSettingValue( setting, value, settingValueStrings );
            return this;
        }

        private Builder set( String setting, Object value )
        {
            overrideSettingValue( setting, value, settingValueObjects );
            return this;
        }

        public Builder setRaw( Map settingValues )
        {
            settingValues.forEach( this::setRaw );
            return this;
        }

        public  Builder set( Setting setting, T value )
        {
            return set( setting.name(), value );
        }

        public Builder set( Map,Object> settingValues )
        {
            settingValues.forEach( ( setting, value ) -> set( setting.name(), value )  );
            return this;
        }

        private Builder setDefault( String setting, Object value )
        {
            if ( !overriddenDefaults.containsKey( setting ) )
            {
                overriddenDefaults.put( setting, value );
            }
            else if ( allowedToOverrideValues( setting, value, overriddenDefaults ) )
            {
                log.warn( "The overridden default value of '%s' setting is overridden. Setting value changed from '%s' to '%s'.", setting,
                        overriddenDefaults.get( setting ), value );
                overriddenDefaults.put( setting, value );
            }
            return this;
        }

        public Builder setDefaults( Map, Object> overriddenDefaults )
        {
            overriddenDefaults.forEach( ( setting, value ) -> setDefault( setting.name(), value )  );
            return this;
        }

        public  Builder setDefault( Setting setting, T value )
        {
            return setDefault( setting.name(), value );
        }

        public Builder remove( Setting setting )
        {
            settingValueStrings.remove( setting.name() );
            settingValueObjects.remove( setting.name() );
            return this;
        }

        public Builder removeDefault( Setting setting )
        {
            overriddenDefaults.remove( setting.name() );
            return this;
        }

        Builder addSettingsClass( Class settingsClass )
        {
            this.settingsClasses.add( settingsClass );
            return this;
        }

        Builder addGroupSettingClass( Class groupSettingClass )
        {
            this.groupSettingClasses.add( groupSettingClass );
            return this;
        }

        public Builder addMigrator( SettingMigrator migrator )
        {
            this.settingMigrators.add( migrator );
            return this;
        }

        public Builder fromConfig( Config config )
        {
            if ( fromConfig != null )
            {
                throw new IllegalArgumentException( "Can only build a config from one other config." );
            }
            while ( config instanceof DatabaseConfig )
            {
                config = ((DatabaseConfig) config).getGlobalConfig();
            }
            fromConfig = config;
            return this;
        }

        public Builder fromFileNoThrow( Path path )
        {
            if ( path != null )
            {
                fromFile( path, false, s -> true );
            }
            return this;
        }

        public Builder fromFile( Path cfg )
        {
            return fromFile( cfg, true, s -> true );
        }

        public Builder fromFile( Path file, boolean allowThrow, Predicate filter )
        {
            if ( file == null || Files.notExists( file ) )
            {
                if ( allowThrow )
                {
                    throw new IllegalArgumentException( new IOException( "Config file [" + file + "] does not exist." ) );
                }
                log.warn( "Config file [%s] does not exist.", file );
                return this;
            }

            try
            {
                if ( Files.isDirectory( file ) )
                {
                    Files.walkFileTree( file, new ConfigDirectoryFileVisitor( file ) );
                }
                else
                {
                    try ( InputStream stream = Files.newInputStream( file ) )
                    {
                        new Properties()
                        {
                            @Override
                            public synchronized Object put( Object key, Object value )
                            {
                                String setting = key.toString();
                                if ( filter.test( setting ) )
                                {
                                    setRaw( setting, value.toString() );
                                }
                                return null;
                            }
                        }.load( stream );
                    }
                    configFiles.add( file );
                }
            }
            catch ( IOException e )
            {
                if ( allowThrow )
                {
                    throw new IllegalArgumentException( "Unable to load config file [" + file + "].", e );
                }
                log.error( "Unable to load config file [%s]: %s", file, e.getMessage() );
            }
            return this;
        }

        public Builder allowCommandExpansion()
        {
            return commandExpansion( true );
        }

        public Builder commandExpansion( boolean expandCommands )
        {
            this.expandCommands = expandCommands;
            return this;
        }

        private Builder()
        {
        }

        public Config build()
        {
            expandCommands |= fromConfig != null && fromConfig.expandCommands; //inherit expandCommands from another config
            if ( expandCommands )
            {
                validateFilePermissionForCommandExpansion( configFiles );
            }
            return new Config( settingsClasses, groupSettingClasses, settingMigrators, settingValueStrings, settingValueObjects, overriddenDefaults,
                    fromConfig, log, expandCommands );
        }

        // Public so APOC can use this for its command expansion
        public static void validateFilePermissionForCommandExpansion( List files )
        {
            if ( files.isEmpty() )
            {
                return;
            }
            if ( SystemUtils.IS_OS_UNIX )
            {

                for ( Path path : files )
                {
                    try
                    {
                        final Set unixPermission640 = Set.of( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
                                                                                   PosixFilePermission.GROUP_READ );
                        PosixFileAttributes attrs = Files.getFileAttributeView( path, PosixFileAttributeView.class ).readAttributes();
                        Set permissions = attrs.permissions();
                        if ( !unixPermission640.containsAll( permissions ) ) //actual permission is a subset of required ones
                        {
                            throw new IllegalArgumentException(
                                    format( "%s does not have the correct file permissions to evaluate commands. Has %s, requires at most %s.", path,
                                            permissions, unixPermission640 ) );
                        }
                    }
                    catch ( IOException | UnsupportedOperationException e )
                    {
                        throw new IllegalStateException( "Unable to access file permissions for " + path, e );
                    }
                }
            }
            else if ( SystemUtils.IS_OS_WINDOWS )
            {
                String processOwner = SystemUtils.getUserName();
                for ( Path path : files )
                {
                    try
                    {
                        AclFileAttributeView attrs = Files.getFileAttributeView( path, AclFileAttributeView.class );
                        UserPrincipal owner = attrs.getOwner();

                        final Set windowsUserNoExecute = Set.of( //All but execute for owner
                                AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA,
                                AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.WRITE_ATTRIBUTES,
                                AclEntryPermission.READ_NAMED_ATTRS, AclEntryPermission.WRITE_NAMED_ATTRS,
                                AclEntryPermission.READ_ACL, AclEntryPermission.WRITE_ACL,
                                AclEntryPermission.DELETE, AclEntryPermission.DELETE_CHILD,
                                AclEntryPermission.WRITE_OWNER, AclEntryPermission.SYNCHRONIZE
                        );
                        for ( AclEntry acl : attrs.getAcl() )
                        {
                            Set permissions = acl.permissions();
                            if ( AclEntryType.ALLOW.equals( acl.type() ) )
                            {
                                if ( acl.principal().equals( owner ) )
                                {
                                    if ( !windowsUserNoExecute.containsAll( permissions ) )
                                    {
                                        throw new IllegalArgumentException(
                                                format( "%s does not have the correct ACL for owner to evaluate commands. Has %s for %s, requires at most %s.",
                                                        path, permissions, acl.principal().getName(), windowsUserNoExecute ) );
                                    }
                                }
                                else
                                {
                                    if ( !permissions.isEmpty() )
                                    {
                                        throw new IllegalArgumentException(
                                                format( "%s does not have the correct ACL. Has %s for %s, should be none for all except owner.",
                                                        path, permissions, acl.principal().getName() ) );
                                    }
                                }
                            }
                        }

                        String domainAndName = owner.getName();
                        String fileOwner = domainAndName.contains( "\\" ) ? domainAndName.split( "\\\\" )[1] : domainAndName; //remove domain
                        if ( !fileOwner.equals( processOwner ) )
                        {
                            throw new IllegalArgumentException(
                                    format( "%s does not have the correct file owner to evaluate commands. Has %s, requires %s.", path, domainAndName,
                                            processOwner ) );
                        }
                    }
                    catch ( IOException | UnsupportedOperationException e )
                    {
                        throw new IllegalStateException( "Unable to access file permissions for " + path, e );
                    }
                }
            }
            else
            {
                throw new IllegalStateException( "Configuration command expansion not supported for " + SystemUtils.OS_NAME );
            }
        }

        private class ConfigDirectoryFileVisitor implements FileVisitor
        {
            private final Path root;

            ConfigDirectoryFileVisitor( Path root )
            {
                this.root = root;
            }

            private boolean isRoot( Path dir )
            {
                return root.equals( dir );
            }

            private boolean isNotHidden( Path file )
            {
                return !file.getFileName().toString().startsWith( "." );
            }

            private boolean isFile( Path file, BasicFileAttributes attrs )
            {
                return attrs.isRegularFile() || Files.isRegularFile( file );
            }

            @Override
            public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs )
            {
                if ( isRoot( dir ) )
                {
                    return FileVisitResult.CONTINUE;
                }
                else
                {
                    // We don't go into subdirectories, it's too risky
                    if ( isNotHidden( dir ) )
                    {
                        log.warn( "Ignoring subdirectory in config directory [" + dir + "]." );
                    }
                    return FileVisitResult.SKIP_SUBTREE;
                }
            }

            @Override
            public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) throws IOException
            {
                if ( isNotHidden( file ) && isFile( file, attrs ) )
                {
                    String key = file.getFileName().toString();
                    String value = Files.readString( file );
                    setRaw( key, value );
                    configFiles.add( file );
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed( Path file, IOException exc ) throws IOException
            {
                throw exc != null ? exc : new IOException( "Unknown failure loading config file [" + file.toAbsolutePath() + "]" );
            }

            @Override
            public FileVisitResult postVisitDirectory( Path dir, IOException exc ) throws IOException
            {
                if ( exc != null )
                {
                    throw exc;
                }
                return FileVisitResult.CONTINUE;
            }
        }
    }

    public static Config defaults()
    {
        return defaults( Map.of() );
    }

    public static  Config defaults( Setting setting, T value )
    {
        return defaults( Map.of( setting, value ) );
    }

    public static Config defaults( Map,Object> settingValues )
    {
        return Config.newBuilder().set( settingValues ).build();
    }

    public static Builder newBuilder()
    {
        Builder builder = new Builder();
        Services.loadAll( SettingsDeclaration.class ).forEach( decl -> builder.addSettingsClass( decl.getClass() ) );
        Services.loadAll( GroupSetting.class ).forEach( decl -> builder.addGroupSettingClass( decl.getClass() ) );
        Services.loadAll( SettingMigrator.class ).forEach( builder::addMigrator );
        return builder;
    }

    public static Builder emptyBuilder()
    {
        return new Builder();
    }

    protected final Map> settings = new HashMap<>();
    private final Map, Map> allGroupInstances = new HashMap<>();
    private Log log;
    private final boolean expandCommands;
    private final Configuration validationConfig = new ValidationConfig();
    private Duration commandEvaluationTimeout = config_command_evaluation_timeout.defaultValue();

    protected Config()
    {
        expandCommands = false;
    }

    private Config( Collection> settingsClasses,
            Collection> groupSettingClasses,
            Collection settingMigrators,
            Map settingValueStrings,
            Map settingValueObjects,
            Map overriddenDefaultObjects,
            Config fromConfig,
            Log log,
            boolean expandCommands )
    {
        this.log = log;
        this.expandCommands = expandCommands;

        if ( expandCommands )
        {
            log.info( "Command expansion is explicitly enabled for configuration" );
        }

        Map overriddenDefaultStrings = new HashMap<>();
        try
        {
            settingMigrators.forEach( migrator -> migrator.migrate( settingValueStrings, overriddenDefaultStrings, log )  );
        }
        catch ( RuntimeException e )
        {
            throw new IllegalArgumentException( "Error while migrating settings, please see the exception cause", e );
        }

        Map> definedSettings = getDefinedSettings( settingsClasses );
        Map> definedGroups = getDefinedGroups( groupSettingClasses );
        Set keys = new HashSet<>( definedSettings.keySet() );
        keys.addAll( settingValueStrings.keySet() );
        keys.addAll( settingValueObjects.keySet() );

        List> newSettings = new ArrayList<>();

        if ( fromConfig != null ) //When building from another config, extract values
        {
            //fromConfig.log is ignored, until different behaviour is expected
            fromConfig.allGroupInstances.forEach( ( cls, fromGroupMap ) -> {
                Map groupMap = allGroupInstances.computeIfAbsent( cls, k -> new HashMap<>() );
                groupMap.putAll( fromGroupMap );
            } );
            for ( Map.Entry> entry : fromConfig.settings.entrySet() )
            {
                newSettings.add( entry.getValue().setting );
                keys.remove( entry.getKey() );
            }
        }

        boolean strict = strict_config_validation.defaultValue();
        if ( keys.remove( strict_config_validation.name() ) ) //evaluate strict_config_validation setting first, as we need it when validating other settings
        {
            evaluateSetting( strict_config_validation, settingValueStrings, settingValueObjects,
                    fromConfig, overriddenDefaultStrings, overriddenDefaultObjects );
            strict = get( strict_config_validation );
        }

        if ( keys.remove( config_command_evaluation_timeout.name() ) )
        {
            evaluateSetting( config_command_evaluation_timeout, settingValueStrings, settingValueObjects,
                    fromConfig, overriddenDefaultStrings, overriddenDefaultObjects );
            commandEvaluationTimeout = get( config_command_evaluation_timeout );
        }

        newSettings.addAll( getActiveSettings( keys, definedGroups, definedSettings, strict ) );

        evaluateSettingValues( newSettings, settingValueStrings, settingValueObjects, overriddenDefaultStrings, overriddenDefaultObjects, fromConfig );
    }

    @SuppressWarnings( "unchecked" )
    private void evaluateSettingValues( Collection> settingsToEvaluate, Map settingValueStrings,
            Map settingValueObjects,Map overriddenDefaultStrings, Map overriddenDefaultObjects, Config fromConfig )
    {
        Deque> newSettings = new ArrayDeque<>( settingsToEvaluate );
        while ( !newSettings.isEmpty() )
        {
            boolean modified = false;
            SettingImpl last = newSettings.peekLast();
            SettingImpl setting;
            Map,Setting> dependencies = new HashMap<>();
            do
            {
                setting = (SettingImpl) requireNonNull( newSettings.pollFirst() );

                boolean retry = false;
                if ( setting.dependency() != null && !settings.containsKey( setting.dependency().name() ) )
                {
                    //dependency not yet evaluated
                    dependencies.put( setting, setting.dependency() );
                    retry = true;
                }
                else
                {
                    try
                    {
                        evaluateSetting( setting, settingValueStrings, settingValueObjects, fromConfig, overriddenDefaultStrings, overriddenDefaultObjects );
                        modified = true;
                    }
                    catch ( AccessDuringEvaluationException e )
                    {
                        //Constraint with internal dependencies yet not evaluated
                        dependencies.put( setting, e.getAttemptedAccess() );
                        retry = true;
                    }
                }
                if ( retry )
                {
                    newSettings.addLast( setting );
                }
            }
            while ( setting != last );

            if ( !modified && !newSettings.isEmpty() )
            {
                //Settings left depend on settings not present in this config.
                String unsolvable = newSettings.stream()
                        .map( s -> format("'%s'->'%s'", s.name(), dependencies.get( s ).name() ) )
                        .collect( Collectors.joining(",\n","[","]"));
                throw new IllegalArgumentException(
                        format( "Can not resolve setting dependencies. %s depend on settings not present in config, or are in a circular dependency ",
                                unsolvable ) );
            }
        }
    }

    private Collection> getActiveSettings( Set settingNames, Map> definedGroups,
            Map> declaredSettings, boolean strict )
    {
        List> newSettings = new ArrayList<>();
        for ( String key : settingNames )
        {
            // Try to find in settings
            SettingImpl setting = declaredSettings.get( key );
            if ( setting != null )
            {
                newSettings.add( setting );
            }
            else
            {
                // Not found, could be a group setting, e.g "dbms.ssl.policy.*"
                var groupEntryOpt = definedGroups.entrySet().stream().filter( e -> key.startsWith( e.getKey() + '.' ) ).findAny();
                if ( groupEntryOpt.isEmpty() )
                {
                    String msg = format( "Unrecognized setting. No declared setting with name: %s", key );
                    if ( strict )
                    {
                        throw new IllegalArgumentException( msg );
                    }
                    log.warn( msg );
                    continue;
                }
                var groupEntry = groupEntryOpt.get();

                String prefix = groupEntry.getKey();
                String keyWithoutPrefix = key.substring( prefix.length() + 1 );
                int dotIndex = keyWithoutPrefix.indexOf( '.' );
                String id = dotIndex == -1 ? keyWithoutPrefix : keyWithoutPrefix.substring( 0, dotIndex );
                if ( id.isEmpty() )
                {
                    String msg = format( "Malformed group setting name: '%s', does not match any setting in its group.", key );
                    if ( strict )
                    {
                        throw new IllegalArgumentException( msg );
                    }
                    log.warn( msg );
                    continue;
                }

                Map groupInstances = allGroupInstances.computeIfAbsent( groupEntry.getValue(), k -> new HashMap<>() );
                if ( !groupInstances.containsKey( id ) )
                {

                    GroupSetting group;
                    try
                    {
                        group = createStringInstance( groupEntry.getValue(), id );
                    }
                    catch ( IllegalArgumentException e )
                    {
                        String msg = format( "Unrecognized setting. No declared setting with name: %s", key );
                        if ( strict )
                        {
                            throw new IllegalArgumentException( msg );
                        }
                        log.warn( msg );
                        continue;
                    }
                    groupInstances.put( id, group );
                    //Add all settings from created groups, to get possible default values.
                    newSettings.addAll( getDefinedSettings( group.getClass(), group ).values() );
                }
            }
        }
        return newSettings;
    }

    @SuppressWarnings( "unchecked" )
    private void evaluateSetting( Setting untypedSetting, Map settingValueStrings, Map settingValueObjects, Config fromConfig,
            Map overriddenDefaultStrings, Map overriddenDefaultObjects )
    {
        SettingImpl setting = (SettingImpl) untypedSetting;
        String key = setting.name();

        try
        {
            Object defaultValue;
            if ( overriddenDefaultObjects.containsKey( key ) ) // Map default value
            {
                defaultValue = overriddenDefaultObjects.get( key );
            }
            else if ( overriddenDefaultStrings.containsKey( key ) )
            {
                defaultValue = setting.parse( evaluateIfCommand( key, overriddenDefaultStrings.get( key ) ) );
            }
            else
            {
                defaultValue = setting.defaultValue();
                if ( fromConfig != null && fromConfig.settings.containsKey( key ) )
                {
                    Object fromDefault = fromConfig.settings.get( key ).defaultValue;
                    if ( !Objects.equals( defaultValue, fromDefault ) )
                    {
                        defaultValue = fromDefault;
                    }
                }
            }

            Object value = null;
            if ( settingValueObjects.containsKey( key ) )
            {
                value = settingValueObjects.get( key );
            }
            else if ( settingValueStrings.containsKey( key ) ) // Map value
            {
                value = setting.parse( evaluateIfCommand( key, settingValueStrings.get( key ) ) );
            }
            else if ( fromConfig != null && fromConfig.settings.containsKey( key ) )
            {
                Entry entry = fromConfig.settings.get( key );
                value = entry.isDefault ? null : entry.value;
            }

            value = setting.solveDefault( value, defaultValue );

            settings.put( key, createEntry( setting, value, defaultValue ) );
        }
        catch ( AccessDuringEvaluationException exception )
        {
            throw exception; //Bubble up
        }
        catch ( RuntimeException exception )
        {
            String msg = format( "Error evaluating value for setting '%s'. %s", setting.name(), exception.getMessage() );
            throw new IllegalArgumentException( msg, exception );
        }
    }

    private String evaluateIfCommand( String settingName, String entry )
    {
        if ( isCommand( entry ) )
        {
            Preconditions.checkArgument( expandCommands,
                    format( "%s is a command, but config is not explicitly told to expand it. (Missing --expand-commands argument?)", entry ) );
            String str = entry.trim();
            String command = str.substring( 2, str.length() - 1 );
            log.info( "Executing external script to retrieve value of setting " + settingName );
            return executeCommand( command, commandEvaluationTimeout );
        }
        return entry;
    }

    // Public so APOC can use this for its command expansion
    public static boolean isCommand( String entry )
    {
        String str = entry.trim();
        return str.length() > 3 && str.charAt( 0 ) == '$' && str.charAt( 1 ) == '(' && str.charAt( str.length() - 1 ) == ')';
    }

    // Needed for APOC to be able to check for command expansion
    public boolean expandCommands()
    {
        return this.expandCommands;
    }

    // Public so APOC can use this for its command expansion
    public static String executeCommand( String command, Duration timeout )
    {
        Process process = null;
        try
        {
            String[] commands =
                    new StringTokenizer( command, StringMatcherFactory.INSTANCE.splitMatcher(), StringMatcherFactory.INSTANCE.quoteMatcher() ).getTokenArray();
            process = new ProcessBuilder( commands ).start();
            BufferedReader out = new BufferedReader( new InputStreamReader( process.getInputStream() ) );
            BufferedReader err = new BufferedReader( new InputStreamReader( process.getErrorStream() ) );
            if ( !process.waitFor( timeout.toMillis(), TimeUnit.MILLISECONDS ) )
            {
                throw new IllegalArgumentException( format( "Timed out executing command `%s`", command ) );
            }
            String output = out.lines().collect( Collectors.joining( lineSeparator() ) );

            int exitCode = process.exitValue();
            if ( exitCode != 0 )
            {
                String errOutput =  err.lines().collect( Collectors.joining( lineSeparator() ) );
                throw new IllegalArgumentException( format( "Command `%s` failed with exit code %s.%n%s%n%s", command, exitCode, output, errOutput ) );
            }
            return output;
        }
        catch ( InterruptedException e )
        {
            Thread.currentThread().interrupt();
            throw new IllegalArgumentException( "Interrupted while executing command", e );
        }
        catch ( IOException e )
        {
            throw new IllegalArgumentException( e );
        }
        finally
        {
            if ( process != null && process.isAlive() )
            {
                process.destroyForcibly();
            }
        }
    }

    @SuppressWarnings( "unchecked" )
    private  Entry createEntry( SettingImpl setting, T value, T defaultValue )
    {
        if ( setting.dependency() != null )
        {
            var dep = settings.get( setting.dependency().name() );
            T solvedValue = setting.solveDependency( value != null ? value : defaultValue, (T) dep.getValue() );
            return new DepEntry<>( setting, value, defaultValue, solvedValue );
        }
        return new Entry<>( setting, value, defaultValue );
    }

    @SuppressWarnings( "unchecked" )
    public  Map getGroups( Class group )
    {
        return new HashMap<>( (Map) allGroupInstances.getOrDefault( group, new HashMap<>() ) );
    }

    @SuppressWarnings( "unchecked" )
    public  Map,Map> getGroupsFromInheritance( Class parentClass )
    {
        return allGroupInstances.keySet().stream()
                .filter( parentClass::isAssignableFrom )
                .map( childClass -> (Class) childClass )
                .collect( Collectors.toMap( childClass -> childClass, this::getGroups ) );
    }

    private static  T createInstance( Class classObj )
    {

        T instance;
        try
        {
            instance = createStringInstance( classObj, null );
        }
        catch ( Exception first )
        {
            try
            {
                Constructor constructor = classObj.getDeclaredConstructor();
                constructor.setAccessible( true );
                instance = constructor.newInstance();
            }
            catch ( Exception second )
            {
                String name = classObj.getSimpleName();
                String msg = format( "Failed to create instance of: %s, please see the exception cause", name );
                throw new IllegalArgumentException( msg, Exceptions.chain( second, first ) );
            }

        }
        return instance;
    }

    @Override
    public  T get( org.neo4j.graphdb.config.Setting setting )
    {
        return getObserver( setting ).getValue();
    }

    @SuppressWarnings( "unchecked" )
    public  SettingObserver getObserver( Setting setting )
    {
        SettingObserver observer = (SettingObserver) settings.get( setting.name() );
        if ( observer != null )
        {
            return observer;
        }
        throw new IllegalArgumentException( format( "Config has no association with setting: '%s'", setting.name() ) );
    }

    public  void setDynamic( Setting setting, T value, String scope )
    {
        Entry entry = (Entry) getObserver( setting );
        SettingImpl actualSetting = entry.setting;
        if ( !actualSetting.dynamic() )
        {
            throw new IllegalArgumentException( format("Setting '%s' is not dynamic and can not be changed at runtime", setting.name() ) );
        }
        set( setting, value );
        log.info( "%s changed to %s, by %s", setting.name(), actualSetting.valueToString( value ), scope );

    }

    public  void set( Setting setting, T value )
    {
        Entry entry = (Entry) getObserver( setting );
        SettingImpl actualSetting = entry.setting;
        if ( actualSetting.immutable() )
        {
            throw new IllegalArgumentException( format("Setting '%s' immutable (final). Can not amend", actualSetting.name() ) );
        }
        entry.setValue( value );
    }

    public  void setIfNotSet( Setting setting, T value )
    {
        Entry entry = (Entry) getObserver( setting );
        if ( entry == null || entry.isDefault )
        {
            set( setting, value );
        }
    }

    public boolean isExplicitlySet( Setting setting )
    {
        if ( settings.containsKey( setting.name() ) )
        {
            return !settings.get( setting.name() ).isDefault;
        }
        return false;
    }

    @Override
    public String toString()
    {
        return toString( true );
    }

    @SuppressWarnings( "unchecked" )
    public String toString( boolean includeNullValues )
    {
        StringBuilder sb = new StringBuilder();
        settings.entrySet().stream()
                .sorted( Map.Entry.comparingByKey() )
                .forEachOrdered( e ->
                {
                    SettingImpl setting = (SettingImpl) e.getValue().setting;
                    Object valueObj = e.getValue().getValue();
                    if ( valueObj != null || includeNullValues )
                    {
                        String value = setting.valueToString( valueObj );
                        sb.append( format( "%s=%s%n", e.getKey(), value ) );
                    }
                } );
        return sb.toString();
    }

    public void setLogger( Log log )
    {
        if ( this.log instanceof BufferingLog )
        {
            ((BufferingLog) this.log).replayInto( log );
        }
        this.log = log;
    }

    @SuppressWarnings( "unchecked" )
    public Map,Object> getValues()
    {
        Map,Object> values = new HashMap<>();
        settings.forEach( ( s, entry ) -> values.put( (Setting) entry.setting, entry.value ) );
        return values;
    }

    @SuppressWarnings( "unchecked" )
    public Setting getSetting( String name )
    {
        if ( !settings.containsKey( name ) )
        {
            throw new IllegalArgumentException( format( "Setting `%s` not found", name ) );
        }
        return (Setting) settings.get( name ).setting;
    }

    @SuppressWarnings( "unchecked" )
    public Map> getDeclaredSettings()
    {
        return settings.entrySet().stream().collect( Collectors.toMap( Map.Entry::getKey, entry -> (Setting) entry.getValue().setting ) );
    }

    private static Map> getDefinedGroups( Collection> groupSettingClasses )
    {
        return groupSettingClasses.stream().collect( Collectors.toMap( cls -> createInstance( cls ).getPrefix(), cls -> cls ) );
    }

    private static  T createStringInstance( Class cls, String id )
    {
        try
        {
            Constructor constructor = cls.getDeclaredConstructor( String.class );
            constructor.setAccessible( true );
            return constructor.newInstance( id );
        }
        catch ( Exception e )
        {
            if ( e.getCause() instanceof  IllegalArgumentException )
            {
                throw new IllegalArgumentException( "Could not create instance with id: " + id, e );
            }
            String msg = format( "'%s' must have a ( String ) constructor, be static & non-abstract", cls.getSimpleName() );
            throw new RuntimeException( msg, e );
        }
    }

    private static Map> getDefinedSettings( Collection> settingsClasses )
    {
        Map> settings = new HashMap<>();
        settingsClasses.forEach( c -> settings.putAll( getDefinedSettings( c, null ) ) );
        return settings;
    }

    private static Map> getDefinedSettings( Class settingClass, Object fromObject )
    {
        Map> settings = new HashMap<>();
        Arrays.stream( FieldUtils.getAllFields( settingClass ) )
                .filter( f -> f.getType().isAssignableFrom( SettingImpl.class ) )
                .forEach( field ->
                {
                    try
                    {
                        field.setAccessible( true );
                        SettingImpl setting = (SettingImpl) field.get( fromObject );
                        if ( field.isAnnotationPresent( Description.class ) )
                        {
                            setting.setDescription( field.getAnnotation( Description.class ).value() );
                        }
                        if ( field.isAnnotationPresent( DocumentedDefaultValue.class ) )
                        {
                            setting.setDocumentedDefaultValue( field.getAnnotation( DocumentedDefaultValue.class ).value() );
                        }
                        if ( field.isAnnotationPresent( Internal.class ) )
                        {
                            setting.setInternal();
                        }
                        if ( field.isAnnotationPresent( Deprecated.class ) )
                        {
                            setting.setDeprecated();
                        }
                        settings.put( setting.name(), setting );
                    }
                    catch ( Exception e )
                    {
                        throw new RuntimeException( format( "%s %s, from %s is not accessible.", field.getType(), field.getName(),
                                settingClass.getSimpleName() ), e );
                    }
                } );
        return settings;
    }

    public  void addListener( Setting setting, SettingChangeListener listener )
    {
        Entry entry = (Entry) getObserver( setting );
        entry.addListener( listener );
    }

    public  void removeListener( Setting setting, SettingChangeListener listener )
    {
        Entry entry = (Entry) getObserver( setting );
        entry.removeListener( listener );
    }

    private class DepEntry extends Entry
    {
        private volatile T solved;

        private DepEntry( SettingImpl setting, T value, T defaultValue, T solved )
        {
            super( setting, value, defaultValue, false );
            this.solved = solved;
            setting.validate( solved, validationConfig );
        }

        @Override
        public T getValue()
        {
            return solved;
        }

        @Override
        synchronized void setValue( T value )
        {
            T oldValue = solved;
            solved = setting.solveDependency( value != null ? value : defaultValue, getObserver( setting.dependency() ).getValue() );
            setting.validate( solved, validationConfig );
            internalSetValue( value );
            notifyListeners( oldValue, solved );
        }
    }

    private class Entry implements SettingObserver
    {
        protected final SettingImpl setting;
        protected final T defaultValue;
        private final boolean validate;
        private final Collection> updateListeners = new ConcurrentLinkedQueue<>();
        private volatile T value;
        private volatile boolean isDefault;

        private Entry( SettingImpl setting, T value, T defaultValue )
        {
            this( setting, value, defaultValue, true );
        }

        private Entry( SettingImpl setting, T value, T defaultValue, boolean validate )
        {
            this.setting = setting;
            this.defaultValue = defaultValue;
            this.validate = validate;
            internalSetValue( value );
        }

        @Override
        public T getValue()
        {
            return value;
        }

        synchronized void setValue( T value )
        {
            T oldValue = this.value;
            internalSetValue( value );
            notifyListeners( oldValue, this.value );
        }

        void internalSetValue( T value )
        {
            this.isDefault = value == null;
            T newValue = isDefault ? defaultValue : value;
            if ( validate )
            {
                setting.validate( newValue, validationConfig );
            }
            this.value = newValue;
        }

        protected void notifyListeners( T oldValue, T newValue )
        {
            updateListeners.forEach( listener -> listener.accept( oldValue, newValue ) );
        }

        private void addListener( SettingChangeListener listener )
        {
            if ( !setting.dynamic() )
            {
                throw new IllegalArgumentException( "Setting is not dynamic and will not change" );
            }
            updateListeners.add( listener );
        }

        private void removeListener( SettingChangeListener listener )
        {
            updateListeners.remove( listener );
        }

        @Override
        public String toString()
        {
            return setting.valueToString( value ) + (isDefault ? " (default)" : " (configured)");
        }
    }

    private static class AccessDuringEvaluationException extends RuntimeException
    {
        private final Setting attemptedAccess;

        AccessDuringEvaluationException( Setting attemptedAccess )
        {
            super( format( "AccessDuringEvaluationException{ Tried to access %s in config during construction }", attemptedAccess.name() ) );
            this.attemptedAccess = attemptedAccess;
        }

        Setting getAttemptedAccess()
        {
            return attemptedAccess;
        }
    }

    private class ValidationConfig implements Configuration
    {
        @Override
        public  T get( Setting setting )
        {
            if ( setting.dynamic() )
            {
                throw new IllegalArgumentException( "Can not depend on dynamic setting:" + setting.name() );
            }
            if ( !settings.containsKey( setting.name() ) )
            {
                throw new AccessDuringEvaluationException( setting );
            }
            return Config.this.get( setting );
        }
    }
}