org.neo4j.configuration.Config Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of neo4j-configuration Show documentation
Show all versions of neo4j-configuration Show documentation
Code responsible for parsing the Neo4j configuration.
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://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 static java.lang.String.format;
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.GraphDatabaseInternalSettings.strict_config_validation_allow_duplicates;
import static org.neo4j.configuration.GraphDatabaseSettings.strict_config_validation;
import static org.neo4j.internal.helpers.ProcessUtils.executeCommandWithOutput;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
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.Comparator;
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.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemProperties;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.neo4j.graphdb.config.Configuration;
import org.neo4j.graphdb.config.Setting;
import org.neo4j.internal.helpers.Exceptions;
import org.neo4j.logging.InternalLog;
import org.neo4j.service.Services;
import org.neo4j.util.Preconditions;
public class Config implements Configuration {
public static final String DEFAULT_CONFIG_FILE_NAME = "neo4j.conf";
public static final String DEFAULT_CONFIG_DIR_NAME = "conf";
private static final String STRICT_FAILURE_MESSAGE =
String.format(" Cleanup the config or disable '%s' to continue.", strict_config_validation.name());
private static final String LEGACY_4_X_DBMS_JVM_ADDITIONAL = "dbms.jvm.additional";
public static final String APOC_NAMESPACE = "apoc.";
private static final List SUPPORTED_NAMESPACES = List.of(
"dbms.",
"db.",
"browser.",
"server.",
"internal.",
"client.",
"initial.",
"fabric.",
"gds.",
APOC_NAMESPACE);
@SuppressWarnings("unchecked")
private static final Collection> DEFAULT_SETTING_CLASSES =
Services.loadAll(SettingsDeclaration.class).stream()
.map(c -> (Class) c.getClass())
.toList();
@SuppressWarnings("unchecked")
private static final Collection> DEFAULT_GROUP_SETTING_CLASSES =
Services.loadAll(GroupSetting.class).stream()
.map(c -> (Class) c.getClass())
.toList();
private static final Collection DEFAULT_SETTING_MIGRATORS =
Services.loadAll(SettingMigrator.class);
public static final class Builder {
public static final String ENV_CONFIG_FILE_CHARSET = "NEO4J_CONFIG_FILE_CHARSET";
// We use tree sets with comparators for setting classes and migrators to have
// some defined order in which settings classes are processed and migrators are applied
private final Collection> settingsClasses =
new TreeSet<>(Comparator.comparing(Class::getName));
private final Collection> groupSettingClasses =
new TreeSet<>(Comparator.comparing(Class::getName));
private final Collection settingMigrators =
new TreeSet<>(Comparator.comparing(o -> o.getClass().getName()));
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 InternalLog log = new BufferingLog();
private boolean expandCommands;
private Charset fileCharset = StandardCharsets.ISO_8859_1;
private String strictDuplicateDeclarationWarningMessage;
private static boolean allowedToOverrideValues(String setting, T value, Map settingValues) {
if (allowedMultipleDeclarations(setting)) {
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(
setting + " can only be provided as raw Strings if provided multiple times");
}
}
return false;
}
return true;
}
public static boolean allowedMultipleDeclarations(String setting) {
return Objects.equals(setting, additional_jvm.name())
|| Objects.equals(setting, LEGACY_4_X_DBMS_JVM_ADDITIONAL);
}
private void overrideSettingValue(String setting, T value, Map settingValues, boolean force) {
if (!settingValueStrings.containsKey(setting) && !settingValueObjects.containsKey(setting)) {
settingValues.put(setting, value);
} else if (force // force has to be checked first as the other method has side effects
|| 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) {
setRaw(setting, value, false);
return this;
}
private Builder setRaw(String setting, String value, boolean forceOverride) {
overrideSettingValue(setting, value, settingValueStrings, forceOverride);
return this;
}
private Builder set(String setting, Object value) {
overrideSettingValue(setting, value, settingValueObjects, false);
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 extends SettingsDeclaration> settingsClass) {
this.settingsClasses.add(settingsClass);
return this;
}
Builder addGroupSettingClass(Class extends GroupSetting> groupSettingClass) {
this.groupSettingClasses.add(groupSettingClass);
return this;
}
public Builder addMigrator(SettingMigrator migrator) {
this.settingMigrators.add(migrator);
return this;
}
public Builder setFileCharset(Charset charset) {
fileCharset = charset;
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 (Reader reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(file), fileCharset))) {
new Properties() {
private final Set duplicateDetection = new HashSet<>();
@Override
public synchronized Object put(Object key, Object value) {
String setting = key.toString();
if (filter.test(setting)) {
// We do the duplicate detection here (instead of in setRaw), as we still allow
// override using multiple files or in embedded
boolean forceDuplicateOverride = false;
if (!duplicateDetection.add(setting)) {
if (!allowedMultipleDeclarations(setting)) {
strictDuplicateDeclarationWarningMessage =
setting + " declared multiple times.";
}
} else {
if (allowedMultipleDeclarations(setting)) {
// This is the first occurrence of a possible multi-declaration setting in
// a file. If this setting has been already added from lower-priority files,
// this setting should override those instead of chaining with them.
forceDuplicateOverride = true;
}
}
setRaw(setting, value.toString(), forceDuplicateOverride);
}
return null;
}
}.load(reader);
}
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() {
String charsetOverride = System.getenv(ENV_CONFIG_FILE_CHARSET);
if (charsetOverride != null) {
try {
this.fileCharset = Charset.forName(charsetOverride);
} catch (Exception e) {
log.warn("Could not use requested configuration file charset '" + charsetOverride + "'", e);
}
}
}
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,
strictDuplicateDeclarationWarningMessage);
}
// 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 = SystemProperties.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();
}
/**
* Start construction of a config. Settings will be located using the current {@link ClassLoader}.
*
* @return a new builder.
*/
public static Builder newBuilder() {
Builder builder = new Builder();
DEFAULT_SETTING_CLASSES.forEach(builder::addSettingsClass);
DEFAULT_GROUP_SETTING_CLASSES.forEach(builder::addGroupSettingClass);
DEFAULT_SETTING_MIGRATORS.forEach(builder::addMigrator);
return builder;
}
/**
* Start construction of a config. Settings will be located using the provided {@link ClassLoader}.
*
* @param classLoader class loader to use when searching for settings.
* @return a new builder.
*/
public static Builder newBuilder(ClassLoader classLoader) {
Builder builder = new Builder();
Services.loadAll(classLoader, SettingsDeclaration.class)
.forEach(decl -> builder.addSettingsClass(decl.getClass()));
Services.loadAll(classLoader, GroupSetting.class)
.forEach(decl -> builder.addGroupSettingClass(decl.getClass()));
Services.loadAll(classLoader, SettingMigrator.class).forEach(builder::addMigrator);
return builder;
}
/**
* Empty builder used for testing.
*/
static Builder emptyBuilder() {
return new Builder();
}
protected final Map> settings = new HashMap<>();
private final Map, Map> allGroupInstances = new HashMap<>();
private InternalLog 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,
InternalLog log,
boolean expandCommands,
String strictDuplicateDeclarationWarningMessage) {
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());
}
}
// evaluate strict_config_validation setting first, as we need it when validating other settings
boolean strict = strict_config_validation.defaultValue();
if (keys.remove(strict_config_validation.name())) {
evaluateSetting(
strict_config_validation,
settingValueStrings,
settingValueObjects,
fromConfig,
overriddenDefaultStrings,
overriddenDefaultObjects,
false);
strict = get(strict_config_validation);
}
boolean allowDuplicates = strict_config_validation_allow_duplicates.defaultValue();
if (strict) {
if (keys.remove(strict_config_validation_allow_duplicates.name())) {
evaluateSetting(
strict_config_validation_allow_duplicates,
settingValueStrings,
settingValueObjects,
fromConfig,
overriddenDefaultStrings,
overriddenDefaultObjects,
strict);
allowDuplicates = get(strict_config_validation_allow_duplicates);
}
}
if (strict && !allowDuplicates && StringUtils.isNotEmpty(strictDuplicateDeclarationWarningMessage)) {
throw new IllegalArgumentException(strictDuplicateDeclarationWarningMessage);
}
if (keys.remove(config_command_evaluation_timeout.name())) {
evaluateSetting(
config_command_evaluation_timeout,
settingValueStrings,
settingValueObjects,
fromConfig,
overriddenDefaultStrings,
overriddenDefaultObjects,
strict);
commandEvaluationTimeout = get(config_command_evaluation_timeout);
}
newSettings.addAll(getActiveSettings(keys, definedGroups, definedSettings, strict));
evaluateSettingValues(
newSettings,
settingValueStrings,
settingValueObjects,
overriddenDefaultStrings,
overriddenDefaultObjects,
fromConfig,
strict);
}
@SuppressWarnings("unchecked")
private void evaluateSettingValues(
Collection> settingsToEvaluate,
Map settingValueStrings,
Map settingValueObjects,
Map overriddenDefaultStrings,
Map overriddenDefaultObjects,
Config fromConfig,
boolean strict) {
Deque> newSettings = new ArrayDeque<>(settingsToEvaluate);
while (!newSettings.isEmpty()) {
boolean modified = false;
SettingImpl> last = newSettings.peekLast();
SettingImpl
© 2015 - 2024 Weber Informatics LLC | Privacy Policy