From d17325fc0c9ec514b1be96a00b7f2ca50a6e7526 Mon Sep 17 00:00:00 2001 From: Jacob Hansson Date: Fri, 4 Dec 2015 21:56:09 -0600 Subject: [PATCH] Introduce grouped configuration. 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. --- .../neo4j/kernel/configuration/Config.java | 95 ++++++++++++++- .../kernel/configuration/ConfigValues.java | 52 ++++++++ .../kernel/configuration/ConfigView.java | 38 ++++++ .../{TestConfig.java => ConfigTest.java} | 112 +++++++++++++++++- 4 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigValues.java create mode 100644 community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigView.java rename community/kernel/src/test/java/org/neo4j/kernel/configuration/{TestConfig.java => ConfigTest.java} (63%) diff --git a/community/kernel/src/main/java/org/neo4j/kernel/configuration/Config.java b/community/kernel/src/main/java/org/neo4j/kernel/configuration/Config.java index 8f2a26cc71c14..f4bb321c90081 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/configuration/Config.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/configuration/Config.java @@ -29,9 +29,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; 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.helpers.Pair; import org.neo4j.helpers.collection.Iterables; import org.neo4j.kernel.info.DiagnosticsPhase; import org.neo4j.kernel.info.DiagnosticsProvider; @@ -42,6 +44,7 @@ import static java.lang.Character.isDigit; import static java.util.Arrays.asList; 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 @@ -53,11 +56,11 @@ * 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. */ -public class Config implements DiagnosticsProvider +public class Config implements DiagnosticsProvider, ConfigView { private final List listeners = new CopyOnWriteArrayList<>(); private final Map params = new ConcurrentHashMap<>( ); - private final Function settingsFunction; + private final ConfigValues settingsFunction; // Messages to this log get replayed into a real logger once logging has been // instantiated. @@ -85,8 +88,8 @@ public Config( Map inputParams, Class... settingsClasses ) public Config( Map inputParams, Iterable> settingsClasses ) { - this.settingsFunction = Functions.map( params ); this.params.putAll( inputParams ); + this.settingsFunction = new ConfigValues( params ); registerSettingsClasses( settingsClasses ); } @@ -114,11 +117,22 @@ public Map getParams() /** * Retrieve a configuration property. */ + @Override public T get( Setting setting ) { 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 view( Function projection ) + { + return projection.apply( settingsFunction ); + } + /** * Use {@link Config#applyChanges(java.util.Map)} instead, so changes are applied in * bulk and the ConfigurationChangeListeners can process the changes in one go. @@ -302,6 +316,79 @@ else if ( unit.equalsIgnoreCase( "g" ) ) 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 ..} + * + * 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: + * + *
+     *     dbms.books.0.name=Hansel & Gretel
+     *     dbms.books.0.author=JJ Abrams
+     *     dbms.books.1.name=NKJV
+     *     dbms.books.1.author=Jesus
+     * 
+ * + * We can then access these config values as groups: + * + *
+     * {@code
+     *     Setting 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");
+     * }
+     * 
+ * + * @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> groups( String baseName ) + { + Pattern pattern = Pattern.compile( Pattern.quote( baseName ) + "\\.(\\d+)\\.(.+)" ); + + return ( values ) -> { + Map> groups = new HashMap<>(); + for ( Pair 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 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 get( Setting setting ) + { + return setting.apply( m::get ); + } + }) + .collect( toList() ); + }; + } + + /** * @return index of first non-digit character in {@code numberWithPotentialUnit}. If all digits then * {@code numberWithPotentialUnit.length()} is returned. diff --git a/community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigValues.java b/community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigValues.java new file mode 100644 index 0000000000000..85d057a0e4f0d --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigValues.java @@ -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 . + */ +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 +{ + private final Map raw; + + public ConfigValues( Map raw ) + { + this.raw = raw; + } + + @Override + public String apply( String s ) + { + return raw.get( s ); + } + + public List> rawConfiguration() + { + return raw.entrySet().stream() + .map( e -> pair( e.getKey(), e.getValue() ) ) + .collect( toList() ); + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigView.java b/community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigView.java new file mode 100644 index 0000000000000..68ec1af386d5d --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/configuration/ConfigView.java @@ -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 . + */ +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 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 get( Setting setting ); +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/configuration/TestConfig.java b/community/kernel/src/test/java/org/neo4j/kernel/configuration/ConfigTest.java similarity index 63% rename from community/kernel/src/test/java/org/neo4j/kernel/configuration/TestConfig.java rename to community/kernel/src/test/java/org/neo4j/kernel/configuration/ConfigTest.java index 3d4dae8a9b006..4df918116b153 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/configuration/TestConfig.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/configuration/ConfigTest.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -34,14 +35,17 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; 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.INTEGER; import static org.neo4j.kernel.configuration.Settings.setting; import static org.neo4j.helpers.collection.MapUtil.stringMap; -public class TestConfig +public class ConfigTest { public static class MyMigratingSettings @@ -205,4 +209,110 @@ public void shouldBeAbleToAgumentConfig() throws Exception assertThat( config.get( setting("non-overlapping", STRING, "") ), equalTo( "huzzah" ) ); 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 name = setting( "user.name", STRING, NO_DEFAULT ); + Setting age = setting( "user.age", INTEGER, NO_DEFAULT ); + + // When + List 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 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 name = setting( "user.name", STRING, NO_DEFAULT ); + Setting age = setting( "user.age", INTEGER, NO_DEFAULT ); + + // When + List emptyStrViews = config.view( Config.groups( "" ) ); + List 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 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 name = setting( "name", STRING, "No name given to this poor user" ); + Setting age = setting( "age", INTEGER, NO_DEFAULT ); + + // When + List 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 ) ); + + } }