Skip to content

Commit

Permalink
Introduce grouped configuration.
Browse files Browse the repository at this point in the history
This allows configuration options like:

    dbms.books.0.name=Lord of the Rings
    dbms.books.0.author=Unknown
    dbms.books.1.name=Dr Who
    dbms.books.1.author=Spielberg, Steven

This commit does not contain the user-facing java API part of this, but it
will simply be a way to help build these setting strings, like:

    setConfig( books(0, book_name ), "Lordi" )

Eg. it ends up existing only once we use this API, which we will do for
Bolt in upcoming commits.
  • Loading branch information
jakewins authored and Zhen committed Dec 11, 2015
1 parent b067336 commit d17325f
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 5 deletions.
Expand Up @@ -29,9 +29,11 @@
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


import org.neo4j.function.Functions;
import org.neo4j.graphdb.config.Setting; import org.neo4j.graphdb.config.Setting;
import org.neo4j.helpers.Pair;
import org.neo4j.helpers.collection.Iterables; import org.neo4j.helpers.collection.Iterables;
import org.neo4j.kernel.info.DiagnosticsPhase; import org.neo4j.kernel.info.DiagnosticsPhase;
import org.neo4j.kernel.info.DiagnosticsProvider; import org.neo4j.kernel.info.DiagnosticsProvider;
Expand All @@ -42,6 +44,7 @@
import static java.lang.Character.isDigit; import static java.lang.Character.isDigit;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;


/** /**
* This class holds the overall configuration of a Neo4j database instance. Use the accessors * This class holds the overall configuration of a Neo4j database instance. Use the accessors
Expand All @@ -53,11 +56,11 @@
* UI's can change configuration by calling applyChanges. Any listener, such as services that use * UI's can change configuration by calling applyChanges. Any listener, such as services that use
* this configuration, can be notified of changes by implementing the {@link ConfigurationChangeListener} interface. * this configuration, can be notified of changes by implementing the {@link ConfigurationChangeListener} interface.
*/ */
public class Config implements DiagnosticsProvider public class Config implements DiagnosticsProvider, ConfigView
{ {
private final List<ConfigurationChangeListener> listeners = new CopyOnWriteArrayList<>(); private final List<ConfigurationChangeListener> listeners = new CopyOnWriteArrayList<>();
private final Map<String, String> params = new ConcurrentHashMap<>( ); private final Map<String, String> params = new ConcurrentHashMap<>( );
private final Function<String, String> settingsFunction; private final ConfigValues settingsFunction;


// Messages to this log get replayed into a real logger once logging has been // Messages to this log get replayed into a real logger once logging has been
// instantiated. // instantiated.
Expand Down Expand Up @@ -85,8 +88,8 @@ public Config( Map<String, String> inputParams, Class<?>... settingsClasses )


public Config( Map<String, String> inputParams, Iterable<Class<?>> settingsClasses ) public Config( Map<String, String> inputParams, Iterable<Class<?>> settingsClasses )
{ {
this.settingsFunction = Functions.map( params );
this.params.putAll( inputParams ); this.params.putAll( inputParams );
this.settingsFunction = new ConfigValues( params );
registerSettingsClasses( settingsClasses ); registerSettingsClasses( settingsClasses );
} }


Expand Down Expand Up @@ -114,11 +117,22 @@ public Map<String, String> getParams()
/** /**
* Retrieve a configuration property. * Retrieve a configuration property.
*/ */
@Override
public <T> T get( Setting<T> setting ) public <T> T get( Setting<T> setting )
{ {
return setting.apply( settingsFunction ); return setting.apply( settingsFunction );
} }


/**
* Unlike the public {@link Setting} instances, the function passed in here has access to
* the raw setting data, meaning it can provide functionality that cross multiple settings
* and other more advanced use cases.
*/
public <T> T view( Function<ConfigValues,T> projection )
{
return projection.apply( settingsFunction );
}

/** /**
* Use {@link Config#applyChanges(java.util.Map)} instead, so changes are applied in * Use {@link Config#applyChanges(java.util.Map)} instead, so changes are applied in
* bulk and the ConfigurationChangeListeners can process the changes in one go. * bulk and the ConfigurationChangeListeners can process the changes in one go.
Expand Down Expand Up @@ -302,6 +316,79 @@ else if ( unit.equalsIgnoreCase( "g" ) )
return Long.parseLong( number ) * multiplier; return Long.parseLong( number ) * multiplier;
} }


/**
* This mechanism can be used as an argument to {@link #view(Function)} to view a set of config options that share a common base config key as a group.
* This specific version handles multiple groups, so the common base key should be followed by a number denoting the group, followed by the group config
* values, eg:
*
* {@code <base name>.<group key>.<config key>}
*
* The config of each group can then be accessed as if the {@code config key} in the pattern above was the entire config key. For example, given the
* following configuration:
*
* <pre>
* dbms.books.0.name=Hansel & Gretel
* dbms.books.0.author=JJ Abrams
* dbms.books.1.name=NKJV
* dbms.books.1.author=Jesus
* </pre>
*
* We can then access these config values as groups:
*
* <pre>
* {@code
* Setting<String> bookName = setting("name", STRING); // note that the key here is only 'name'
*
* ConfigView firstBook = config.view( groups("dbms.books") ).get(0);
*
* assert firstBook.get(bookName).equals("Hansel & Gretel");
* }
* </pre>
*
* @param baseName the base name for the groups, this will be the first part of the config key, followed by a grouping number, followed by the group
* config options
* @return a list of grouped config options
*/
public static Function<ConfigValues,List<ConfigView>> groups( String baseName )
{
Pattern pattern = Pattern.compile( Pattern.quote( baseName ) + "\\.(\\d+)\\.(.+)" );

return ( values ) -> {
Map<String,Map<String,String>> groups = new HashMap<>();
for ( Pair<String,String> entry : values.rawConfiguration() )
{
Matcher matcher = pattern.matcher( entry.first() );

if( matcher.matches() )
{
String index = matcher.group( 1 );
String configName = matcher.group( 2 );
String value = entry.other();

Map<String,String> groupConfig = groups.get( index );
if ( groupConfig == null )
{
groupConfig = new HashMap<>();
groups.put( index, groupConfig );
}
groupConfig.put( configName, value );
}
}

return groups.values().stream()
.map( m -> new ConfigView()
{
@Override
public <T> T get( Setting<T> setting )
{
return setting.apply( m::get );
}
})
.collect( toList() );
};
}


/** /**
* @return index of first non-digit character in {@code numberWithPotentialUnit}. If all digits then * @return index of first non-digit character in {@code numberWithPotentialUnit}. If all digits then
* {@code numberWithPotentialUnit.length()} is returned. * {@code numberWithPotentialUnit.length()} is returned.
Expand Down
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2002-2015 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.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 <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.configuration;

import java.util.List;
import java.util.Map;
import java.util.function.Function;

import org.neo4j.helpers.Pair;

import static java.util.stream.Collectors.toList;
import static org.neo4j.helpers.Pair.pair;

public class ConfigValues implements Function<String, String>
{
private final Map<String, String> raw;

public ConfigValues( Map<String,String> raw )
{
this.raw = raw;
}

@Override
public String apply( String s )
{
return raw.get( s );
}

public List<Pair<String,String>> rawConfiguration()
{
return raw.entrySet().stream()
.map( e -> pair( e.getKey(), e.getValue() ) )
.collect( toList() );
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2002-2015 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.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 <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.configuration;

import org.neo4j.graphdb.config.Setting;

/**
* Provide the basic operation that one could perform on a set of configurations.
*/
public interface ConfigView
{
/**
* Retrieve the value of a configuration property from this {@link ConfigView}.
*
* @param setting The configuration property
* @param <T> The type of the configuration property
* @return The value of the configuration property if the property is found, otherwise, return the default value
* of the given property.
*/
<T> T get( Setting<T> setting );
}
Expand Up @@ -24,6 +24,7 @@
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;


Expand All @@ -34,14 +35,17 @@
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.neo4j.kernel.configuration.Settings.BOOLEAN; import static org.neo4j.kernel.configuration.Settings.BOOLEAN;
import static org.neo4j.kernel.configuration.Settings.NO_DEFAULT;
import static org.neo4j.kernel.configuration.Settings.STRING; import static org.neo4j.kernel.configuration.Settings.STRING;
import static org.neo4j.kernel.configuration.Settings.INTEGER;
import static org.neo4j.kernel.configuration.Settings.setting; import static org.neo4j.kernel.configuration.Settings.setting;
import static org.neo4j.helpers.collection.MapUtil.stringMap; import static org.neo4j.helpers.collection.MapUtil.stringMap;


public class TestConfig public class ConfigTest
{ {


public static class MyMigratingSettings public static class MyMigratingSettings
Expand Down Expand Up @@ -205,4 +209,110 @@ public void shouldBeAbleToAgumentConfig() throws Exception
assertThat( config.get( setting("non-overlapping", STRING, "") ), equalTo( "huzzah" ) ); assertThat( config.get( setting("non-overlapping", STRING, "") ), equalTo( "huzzah" ) );
assertThat( config.get( setting("unrelated", STRING, "") ), equalTo( "hello" ) ); assertThat( config.get( setting("unrelated", STRING, "") ), equalTo( "hello" ) );
} }

@Test
public void shouldProvideViewOfGroups() throws Throwable
{
// Given
Config config = new Config( stringMap(
"my.users.0.user.name", "Bob",
"my.users.0.user.age", "81",
"my.users.1.user.name", "Greta",
"my.users.1.user.age", "82" ) );

Setting<String> name = setting( "user.name", STRING, NO_DEFAULT );
Setting<Integer> age = setting( "user.age", INTEGER, NO_DEFAULT );

// When
List<ConfigView> views = config.view( Config.groups( "my.users" ) );

// Then
assertThat( views.size(), equalTo( 2 ) );

ConfigView bob = views.get( 0 );
assertThat( bob.get( name ), equalTo( "Bob" ) );
assertThat( bob.get( age ), equalTo( 81 ) );

ConfigView greta = views.get( 1 );
assertThat( greta.get( name ), equalTo( "Greta" ) );
assertThat( greta.get( age ), equalTo( 82 ) );

// however given the full name, the config could still be accessed outside the group
Setting<String> name0 = setting( "my.users.0.user.name", STRING, NO_DEFAULT );
assertThat( config.get( name0 ), equalTo( "Bob" ) );

}

@Test
public void shouldFindNoGroupViewWhenGroupNameIsMissing() throws Throwable
{
// Given
Config config = new Config( stringMap(
"0.user.name", "Bob",
"0.user.age", "81",
"1.user.name", "Greta",
"1.user.age", "82" ) );

Setting<String> name = setting( "user.name", STRING, NO_DEFAULT );
Setting<Integer> age = setting( "user.age", INTEGER, NO_DEFAULT );

// When
List<ConfigView> emptyStrViews = config.view( Config.groups( "" ) );
List<ConfigView> numViews = config.view( Config.groups( "0" ) );

// Then
assertThat( emptyStrViews.size(), equalTo( 0 ) );
assertThat( numViews.size(), equalTo( 0 ) );
assertThat( config.get( setting( "0.user.name", STRING, NO_DEFAULT ) ), equalTo( "Bob" ) );
}

@Test
public void shouldFindNoGroupViewWhenGroupNameIsWrong() throws Throwable
{
// Given
Config config = new Config( stringMap(
"my.users.0.name", "Bob",
"my.users.0.age", "81",
"my.users.1.name", "Greta",
"my.users.1.age", "82" ) );

// When
List<ConfigView> views = config.view( Config.groups( "my" ) );

// Then
assertThat( views.size(), equalTo( 0 ) );
}

@Test
public void shouldOnlyReadInsideGroupWhileAccessingSettingsInAGroup() throws Throwable
{
// Given
Config config = new Config( stringMap(
"name", "lemon",
"my.users.0.user.name", "Bob",
"my.users.0.user.age", "81",
"my.users.1.user.name", "Greta",
"my.users.1.user.age", "82" ) );

Setting<String> name = setting( "name", STRING, "No name given to this poor user" );
Setting<Integer> age = setting( "age", INTEGER, NO_DEFAULT );

// When
List<ConfigView> views = config.view( Config.groups( "my.users" ) );

// Then
assertThat( views.size(), equalTo( 2 ) );

ConfigView bob = views.get( 0 );
assertThat( bob.get( name ), equalTo( "No name given to this poor user" ) );
assertNull( bob.get( age ) );

ConfigView greta = views.get( 1 );
assertThat( greta.get( name ), equalTo( "No name given to this poor user" ) );
assertNull( greta.get( age ) );

assertThat( config.get( name ), equalTo( "lemon" ) );
assertNull( config.get( age ) );

}
} }

0 comments on commit d17325f

Please sign in to comment.