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

org.opensearch.common.settings.Setting Maven / Gradle / Ivy

There is a newer version: 2.18.0
Show newest version
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 */

/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/*
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

package org.opensearch.common.settings;

import org.apache.logging.log4j.Logger;
import org.opensearch.OpenSearchException;
import org.opensearch.OpenSearchParseException;
import org.opensearch.Version;
import org.opensearch.common.Booleans;
import org.opensearch.common.Nullable;
import org.opensearch.common.annotation.PublicApi;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.regex.Regex;
import org.opensearch.common.unit.MemorySizeValue;
import org.opensearch.common.unit.TimeValue;
import org.opensearch.common.xcontent.XContentFactory;
import org.opensearch.core.common.Strings;
import org.opensearch.core.common.io.stream.StreamInput;
import org.opensearch.core.common.io.stream.StreamOutput;
import org.opensearch.core.common.io.stream.Writeable;
import org.opensearch.core.common.unit.ByteSizeUnit;
import org.opensearch.core.common.unit.ByteSizeValue;
import org.opensearch.core.xcontent.DeprecationHandler;
import org.opensearch.core.xcontent.MediaTypeRegistry;
import org.opensearch.core.xcontent.NamedXContentRegistry;
import org.opensearch.core.xcontent.ToXContentObject;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * A setting. Encapsulates typical stuff like default value, parsing, and scope.
 * Some (Settings.Property.Dynamic) can be modified at run time using the API.
 * All settings inside opensearch or in any of the plugins should use this type-safe and generic settings infrastructure
 * together with {@link AbstractScopedSettings}. This class contains several utility methods that makes it straight forward
 * to add settings for the majority of the cases. For instance a simple boolean settings can be defined like this:
 * 
{@code
 * public static final Setting MY_BOOLEAN = Setting.boolSetting("my.bool.setting", true, Setting.Property.NodeScope);}
 * 
* To retrieve the value of the setting a {@link Settings} object can be passed directly to the {@link Setting#get(Settings)} method. *
 * final boolean myBooleanValue = MY_BOOLEAN.get(settings);
 * 
* It's recommended to use typed settings rather than string based settings. For example adding a setting for an enum type: *
{@code
 * public enum Color {
 *     RED, GREEN, BLUE;
 * }
 * public static final Setting MY_BOOLEAN =
 *     new Setting<>("my.color.setting", Color.RED.toString(), Color::valueOf, Setting.Property.NodeScope);
 * }
 * 
* * @opensearch.api */ @PublicApi(since = "1.0.0") public class Setting implements ToXContentObject { /** * Property of the setting * * @opensearch.api */ @PublicApi(since = "1.0.0") public enum Property { /** * should be filtered in some api (mask password/credentials) */ Filtered, /** * iff this setting can be dynamically updateable */ Dynamic, /** * mark this setting as final, not updateable even when the context is not dynamic * ie. Setting this property on an index scoped setting will fail update when the index is closed */ Final, /** * mark this setting as deprecated */ Deprecated, /** * Node scope */ NodeScope, /** * Secure setting values equal on all nodes */ Consistent, /** * Index scope */ IndexScope, /** * Mark this setting as not copyable during an index resize (shrink or split). This property can only be applied to settings that * also have {@link Property#IndexScope}. */ NotCopyableOnResize, /** * Indicates an index-level setting that is managed internally. Such a setting can only be added to an index on index creation but * can not be updated via the update API. */ InternalIndex, /** * Indicates an index-level setting that is privately managed. Such a setting can not even be set on index creation. */ PrivateIndex, /** * Extension scope */ ExtensionScope } private final Key key; protected final Function defaultValue; @Nullable protected final Setting fallbackSetting; protected final Function parser; protected final Validator validator; private final EnumSet properties; private static final EnumSet EMPTY_PROPERTIES = EnumSet.noneOf(Property.class); private Setting( Key key, @Nullable Setting fallbackSetting, Function defaultValue, Function parser, Validator validator, Property... properties ) { assert this instanceof SecureSetting || this.isGroupSetting() || parser.apply(defaultValue.apply(Settings.EMPTY)) != null : "parser returned null"; this.key = key; this.fallbackSetting = fallbackSetting; this.defaultValue = defaultValue; this.parser = parser; this.validator = validator; if (properties == null) { throw new IllegalArgumentException("properties cannot be null for setting [" + key + "]"); } if (properties.length == 0) { this.properties = EMPTY_PROPERTIES; } else { final EnumSet propertiesAsSet = EnumSet.copyOf(Arrays.asList(properties)); if (propertiesAsSet.contains(Property.Dynamic) && propertiesAsSet.contains(Property.Final)) { throw new IllegalArgumentException("final setting [" + key + "] cannot be dynamic"); } checkPropertyRequiresIndexScope(propertiesAsSet, Property.NotCopyableOnResize); checkPropertyRequiresIndexScope(propertiesAsSet, Property.InternalIndex); checkPropertyRequiresIndexScope(propertiesAsSet, Property.PrivateIndex); checkPropertyRequiresNodeScope(propertiesAsSet, Property.Consistent); this.properties = propertiesAsSet; } } private void checkPropertyRequiresIndexScope(final EnumSet properties, final Property property) { if (properties.contains(property) && properties.contains(Property.IndexScope) == false) { throw new IllegalArgumentException("non-index-scoped setting [" + key + "] can not have property [" + property + "]"); } } private void checkPropertyRequiresNodeScope(final EnumSet properties, final Property property) { if (properties.contains(property) && properties.contains(Property.NodeScope) == false) { throw new IllegalArgumentException("non-node-scoped setting [" + key + "] can not have property [" + property + "]"); } } /** * Creates a new Setting instance * @param key the settings key for this setting. * @param defaultValue a default value function that returns the default values string representation. * @param parser a parser that parses the string rep into a complex datatype. * @param properties properties for this setting like scope, filtering... */ public Setting(Key key, Function defaultValue, Function parser, Property... properties) { this(key, defaultValue, parser, v -> {}, properties); } /** * Creates a new {@code Setting} instance. * * @param key the settings key for this setting * @param defaultValue a default value function that results a string representation of the default value * @param parser a parser that parses a string representation into the concrete type for this setting * @param validator a {@link Validator} for validating this setting * @param properties properties for this setting */ public Setting( Key key, Function defaultValue, Function parser, Validator validator, Property... properties ) { this(key, null, defaultValue, parser, validator, properties); } /** * Creates a new Setting instance * @param key the settings key for this setting. * @param defaultValue a default value. * @param parser a parser that parses the string rep into a complex datatype. * @param properties properties for this setting like scope, filtering... */ public Setting(String key, String defaultValue, Function parser, Property... properties) { this(key, s -> defaultValue, parser, properties); } /** * Creates a new {@code Setting} instance. * * @param key the settings key for this setting * @param defaultValue a default value function that results a string representation of the default value * @param parser a parser that parses a string representation into the concrete type for this setting * @param validator a {@link Validator} for validating this setting * @param properties properties for this setting */ public Setting(String key, String defaultValue, Function parser, Validator validator, Property... properties) { this(new SimpleKey(key), s -> defaultValue, parser, validator, properties); } /** * Creates a new Setting instance * @param key the settings key for this setting. * @param defaultValue a default value function that returns the default values string representation. * @param parser a parser that parses the string rep into a complex datatype. * @param properties properties for this setting like scope, filtering... */ public Setting(String key, Function defaultValue, Function parser, Property... properties) { this(new SimpleKey(key), defaultValue, parser, properties); } /** * Creates a new Setting instance * @param key the settings key for this setting. * @param fallbackSetting a setting whose value to fallback on if this setting is not defined * @param parser a parser that parses the string rep into a complex datatype. * @param properties properties for this setting like scope, filtering... */ public Setting(Key key, Setting fallbackSetting, Function parser, Property... properties) { this(key, fallbackSetting, fallbackSetting::getRaw, parser, v -> {}, properties); } /** * Creates a new Setting instance * @param key the settings key for this setting. * @param fallBackSetting a setting to fall back to if the current setting is not set. * @param parser a parser that parses the string rep into a complex datatype. * @param properties properties for this setting like scope, filtering... */ public Setting(String key, Setting fallBackSetting, Function parser, Property... properties) { this(new SimpleKey(key), fallBackSetting, parser, properties); } /** * Returns the settings key or a prefix if this setting is a group setting. * Note: this method should not be used to retrieve a value from a {@link Settings} object. * Use {@link #get(Settings)} instead * * @see #isGroupSetting() */ public final String getKey() { return key.toString(); } /** * Returns the original representation of a setting key. */ public final Key getRawKey() { return key; } /** * Returns true if this setting is dynamically updateable, otherwise false */ public final boolean isDynamic() { return properties.contains(Property.Dynamic); } /** * Returns true if this setting is final, otherwise false */ public final boolean isFinal() { return properties.contains(Property.Final); } public final boolean isInternalIndex() { return properties.contains(Property.InternalIndex); } public final boolean isPrivateIndex() { return properties.contains(Property.PrivateIndex); } /** * Returns the setting properties * @see Property */ public EnumSet getProperties() { return properties; } /** * Returns true if this setting must be filtered, otherwise false */ public boolean isFiltered() { return properties.contains(Property.Filtered); } /** * Returns true if this setting has a node scope, otherwise false */ public boolean hasNodeScope() { return properties.contains(Property.NodeScope); } /** * Returns true if this setting's value can be checked for equality across all nodes. Only {@link SecureSetting} instances * may have this qualifier. */ public boolean isConsistent() { return properties.contains(Property.Consistent); } /** * Returns true if this setting has an index scope, otherwise false */ public boolean hasIndexScope() { return properties.contains(Property.IndexScope); } /** * Returns true if this setting is deprecated, otherwise false */ public boolean isDeprecated() { return properties.contains(Property.Deprecated); } /** * Returns true iff this setting is a group setting. Group settings represent a set of settings rather than a single value. * The key, see {@link #getKey()}, in contrast to non-group settings is a prefix like {@code cluster.store.} that matches all settings * with this prefix. */ boolean isGroupSetting() { return false; } final boolean isListSetting() { return this instanceof ListSetting; } boolean hasComplexMatcher() { return isGroupSetting(); } /** * Validate the current setting value only without dependencies with {@link Setting.Validator#validate(Object)}. * @param settings a settings object for settings that has a default value depending on another setting if available */ void validateWithoutDependencies(Settings settings) { validator.validate(get(settings, false)); } /** * Returns the default value string representation for this setting. * @param settings a settings object for settings that has a default value depending on another setting if available */ public String getDefaultRaw(Settings settings) { return defaultValue.apply(settings); } /** * Returns the default value for this setting. * @param settings a settings object for settings that has a default value depending on another setting if available */ public T getDefault(Settings settings) { return parser.apply(getDefaultRaw(settings)); } /** * Returns true if and only if this setting is present in the given settings instance. Note that fallback settings are excluded. * * @param settings the settings * @return true if the setting is present in the given settings instance, otherwise false */ public boolean exists(final Settings settings) { return exists(settings.keySet()); } public boolean exists(final Settings.Builder builder) { return exists(builder.keys()); } private boolean exists(final Set keys) { return keys.contains(getKey()); } /** * Returns true if and only if this setting including fallback settings is present in the given settings instance. * * @param settings the settings * @return true if the setting including fallback settings is present in the given settings instance, otherwise false */ public boolean existsOrFallbackExists(final Settings settings) { return settings.keySet().contains(getKey()) || (fallbackSetting != null && fallbackSetting.existsOrFallbackExists(settings)); } /** * Returns the settings value. If the setting is not present in the given settings object the default value is returned * instead. */ public T get(Settings settings) { return get(settings, true); } private T get(Settings settings, boolean validate) { String value = getRaw(settings); try { T parsed = parser.apply(value); if (validate) { final Iterator> it = validator.settings(); final Map, Object> map; if (it.hasNext()) { map = new HashMap<>(); while (it.hasNext()) { final Setting setting = it.next(); if (setting instanceof AffixSetting) { // Collect all possible concrete settings AffixSetting as = ((AffixSetting) setting); for (String ns : as.getNamespaces(settings)) { Setting s = as.getConcreteSettingForNamespace(ns); map.put(s, s.get(settings, false)); } } else { map.put(setting, setting.get(settings, false)); // we have to disable validation or we will stack overflow } } } else { map = Collections.emptyMap(); } validator.validate(parsed); validator.validate(parsed, map); validator.validate(parsed, map, exists(settings)); } return parsed; } catch (OpenSearchParseException ex) { throw new IllegalArgumentException(ex.getMessage(), ex); } catch (NumberFormatException ex) { String err = "Failed to parse value" + (isFiltered() ? "" : " [" + value + "]") + " for setting [" + getKey() + "]"; throw new IllegalArgumentException(err, ex); } catch (IllegalArgumentException ex) { throw ex; } catch (Exception t) { String err = "Failed to parse value" + (isFiltered() ? "" : " [" + value + "]") + " for setting [" + getKey() + "]"; throw new IllegalArgumentException(err, t); } } /** * Add this setting to the builder if it doesn't exist in the source settings. * The value added to the builder is taken from the given default settings object. * @param builder the settings builder to fill the diff into * @param source the source settings object to diff * @param defaultSettings the default settings object to diff against */ public void diff(Settings.Builder builder, Settings source, Settings defaultSettings) { if (exists(source) == false) { if (exists(defaultSettings)) { // If the setting is only in the defaults, use the value from the defaults builder.put(getKey(), getRaw(defaultSettings)); } else { // If the setting is in neither `source` nor `default`, get the value // from `source` as it may depend on the value of other settings builder.put(getKey(), getRaw(source)); } } } /** * Returns the raw (string) settings value. If the setting is not present in the given settings object the default value is returned * instead. This is useful if the value can't be parsed due to an invalid value to access the actual value. */ private String getRaw(final Settings settings) { checkDeprecation(settings); return innerGetRaw(settings); } /** * The underlying implementation for {@link #getRaw(Settings)}. Setting specializations can override this as needed to convert the * actual settings value to raw strings. * * @param settings the settings instance * @return the raw string representation of the setting value */ String innerGetRaw(final Settings settings) { SecureSettings secureSettings = settings.getSecureSettings(); if (secureSettings != null && secureSettings.getSettingNames().contains(getKey())) { throw new IllegalArgumentException( "Setting [" + getKey() + "] is a non-secure setting" + " and must be stored inside opensearch.yml, but was found inside the OpenSearch keystore" ); } return settings.get(getKey(), defaultValue.apply(settings)); } /** Logs a deprecation warning if the setting is deprecated and used. */ void checkDeprecation(Settings settings) { // They're using the setting, so we need to tell them to stop if (this.isDeprecated() && this.exists(settings)) { // It would be convenient to show its replacement key, but replacement is often not so simple final String key = getKey(); Settings.DeprecationLoggerHolder.deprecationLogger.deprecate( key, "[{}] setting was deprecated in OpenSearch and will be removed in a future release! " + "See the breaking changes documentation for the next major version.", key ); } } /** * Returns true iff the given key matches the settings key or if this setting is a group setting if the * given key is part of the settings group. * @see #isGroupSetting() */ public final boolean match(String toTest) { return key.match(toTest); } @Override public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field("key", key.toString()); builder.field("properties", properties); builder.field("is_group_setting", isGroupSetting()); builder.field("default", defaultValue.apply(Settings.EMPTY)); builder.endObject(); return builder; } @Override public String toString() { return Strings.toString(MediaTypeRegistry.JSON, this, true, true); } /** * Returns the value for this setting but falls back to the second provided settings object */ public final T get(Settings primary, Settings secondary) { if (exists(primary)) { return get(primary); } if (exists(secondary)) { return get(secondary); } if (fallbackSetting == null) { return get(primary); } if (fallbackSetting.exists(primary)) { return fallbackSetting.get(primary); } return fallbackSetting.get(secondary); } public Setting getConcreteSetting(String key) { // we use startsWith here since the key might be foo.bar.0 if it's an array assert key.startsWith(this.getKey()) : "was " + key + " expected: " + getKey(); return this; } /** * Allows a setting to declare a dependency on another setting being set. Optionally, a setting can validate the value of the dependent * setting. * * @opensearch.api */ @PublicApi(since = "1.0.0") public interface SettingDependency { /** * The setting to declare a dependency on. * * @return the setting */ Setting getSetting(); /** * Validates the dependent setting value. * * @param key the key for this setting * @param value the value of this setting * @param dependency the value of the dependent setting */ default void validate(String key, Object value, Object dependency) { } } /** * Returns a set of settings that are required at validation time. Unless all of the dependencies are present in the settings * object validation of setting must fail. */ public Set getSettingsDependencies(final String key) { return Collections.emptySet(); } /** * Build a new updater with a noop validator. */ final AbstractScopedSettings.SettingUpdater newUpdater(Consumer consumer, Logger logger) { return newUpdater(consumer, logger, (s) -> {}); } /** * Build the updater responsible for validating new values, logging the new * value, and eventually setting the value where it belongs. */ AbstractScopedSettings.SettingUpdater newUpdater(Consumer consumer, Logger logger, Consumer validator) { if (isDynamic()) { return new Updater(consumer, logger, validator); } else { throw new IllegalStateException("setting [" + getKey() + "] is not dynamic"); } } /** * Updates settings that depend on each other. * See {@link AbstractScopedSettings#addSettingsUpdateConsumer(Setting, Setting, BiConsumer)} and its usage for details. */ static AbstractScopedSettings.SettingUpdater> compoundUpdater( final BiConsumer consumer, final BiConsumer validator, final Setting aSetting, final Setting bSetting, Logger logger ) { final AbstractScopedSettings.SettingUpdater aSettingUpdater = aSetting.newUpdater(null, logger); final AbstractScopedSettings.SettingUpdater bSettingUpdater = bSetting.newUpdater(null, logger); return new AbstractScopedSettings.SettingUpdater>() { @Override public boolean hasChanged(Settings current, Settings previous) { return aSettingUpdater.hasChanged(current, previous) || bSettingUpdater.hasChanged(current, previous); } @Override public Tuple getValue(Settings current, Settings previous) { A valueA = aSettingUpdater.getValue(current, previous); B valueB = bSettingUpdater.getValue(current, previous); validator.accept(valueA, valueB); return new Tuple<>(valueA, valueB); } @Override public void apply(Tuple value, Settings current, Settings previous) { if (aSettingUpdater.hasChanged(current, previous)) { logSettingUpdate(aSetting, current, previous, logger); } if (bSettingUpdater.hasChanged(current, previous)) { logSettingUpdate(bSetting, current, previous, logger); } consumer.accept(value.v1(), value.v2()); } @Override public String toString() { return "CompoundUpdater for: " + aSettingUpdater + " and " + bSettingUpdater; } }; } static AbstractScopedSettings.SettingUpdater groupedSettingsUpdater( Consumer consumer, final List> configuredSettings ) { return groupedSettingsUpdater(consumer, configuredSettings, (v) -> {}); } static AbstractScopedSettings.SettingUpdater groupedSettingsUpdater( Consumer consumer, final List> configuredSettings, Consumer validator ) { return new AbstractScopedSettings.SettingUpdater() { private Settings get(Settings settings) { return settings.filter(s -> { for (Setting setting : configuredSettings) { if (setting.key.match(s)) { return true; } } return false; }); } @Override public boolean hasChanged(Settings current, Settings previous) { Settings currentSettings = get(current); Settings previousSettings = get(previous); return currentSettings.equals(previousSettings) == false; } @Override public Settings getValue(Settings current, Settings previous) { validator.accept(current); return get(current); } @Override public void apply(Settings value, Settings current, Settings previous) { consumer.accept(value); } @Override public String toString() { return "Updater grouped: " + configuredSettings.stream().map(Setting::getKey).collect(Collectors.joining(", ")); } }; } /** * Allows an affix setting to declare a dependency on another affix setting. * * @opensearch.api */ @PublicApi(since = "1.0.0") public interface AffixSettingDependency extends SettingDependency { @Override AffixSetting getSetting(); } /** * An affix setting * * @opensearch.api */ @PublicApi(since = "1.0.0") public static class AffixSetting extends Setting { private final AffixKey key; private final BiFunction> delegateFactory; private final Set dependencies; public AffixSetting( AffixKey key, Setting delegate, BiFunction> delegateFactory, AffixSettingDependency... dependencies ) { super(key, delegate.defaultValue, delegate.parser, delegate.properties.toArray(new Property[0])); this.key = key; this.delegateFactory = delegateFactory; this.dependencies = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(dependencies))); } boolean isGroupSetting() { return true; } private Stream matchStream(Settings settings) { return settings.keySet().stream().filter(this::match).map(key::getConcreteString); } /** * Get the raw list of dependencies. This method is exposed for testing purposes and {@link #getSettingsDependencies(String)} * should be preferred for most all cases. * @return the raw list of dependencies for this setting */ public Set getDependencies() { return Collections.unmodifiableSet(dependencies); } @Override public Set getSettingsDependencies(String settingsKey) { if (dependencies.isEmpty()) { return Collections.emptySet(); } else { String namespace = key.getNamespace(settingsKey); return dependencies.stream().map(s -> new SettingDependency() { @Override public Setting getSetting() { return s.getSetting().getConcreteSettingForNamespace(namespace); } @Override public void validate(final String key, final Object value, final Object dependency) { s.validate(key, value, dependency); }; }).collect(Collectors.toSet()); } } AbstractScopedSettings.SettingUpdater, T>> newAffixUpdater( BiConsumer consumer, Logger logger, BiConsumer validator ) { return new AbstractScopedSettings.SettingUpdater, T>>() { @Override public boolean hasChanged(Settings current, Settings previous) { return Stream.concat(matchStream(current), matchStream(previous)).findAny().isPresent(); } @Override public Map, T> getValue(Settings current, Settings previous) { // we collect all concrete keys and then delegate to the actual setting for validation and settings extraction final Map, T> result = new IdentityHashMap<>(); Stream.concat(matchStream(current), matchStream(previous)).distinct().forEach(aKey -> { String namespace = key.getNamespace(aKey); Setting concreteSetting = getConcreteSetting(namespace, aKey); AbstractScopedSettings.SettingUpdater updater = concreteSetting.newUpdater( (v) -> consumer.accept(namespace, v), logger, (v) -> validator.accept(namespace, v) ); if (updater.hasChanged(current, previous)) { // only the ones that have changed otherwise we might get too many updates // the hasChanged above checks only if there are any changes T value = updater.getValue(current, previous); result.put(updater, value); } }); return result; } @Override public void apply(Map, T> value, Settings current, Settings previous) { for (Map.Entry, T> entry : value.entrySet()) { entry.getKey().apply(entry.getValue(), current, previous); } } }; } AbstractScopedSettings.SettingUpdater> newAffixMapUpdater( Consumer> consumer, Logger logger, BiConsumer validator ) { return new AbstractScopedSettings.SettingUpdater>() { @Override public boolean hasChanged(Settings current, Settings previous) { return current.filter(k -> match(k)).equals(previous.filter(k -> match(k))) == false; } @Override public Map getValue(Settings current, Settings previous) { // we collect all concrete keys and then delegate to the actual setting for validation and settings extraction final Map result = new IdentityHashMap<>(); Stream.concat(matchStream(current), matchStream(previous)).distinct().forEach(aKey -> { String namespace = key.getNamespace(aKey); Setting concreteSetting = getConcreteSetting(namespace, aKey); AbstractScopedSettings.SettingUpdater updater = concreteSetting.newUpdater( (v) -> {}, logger, (v) -> validator.accept(namespace, v) ); if (updater.hasChanged(current, previous)) { // only the ones that have changed otherwise we might get too many updates // the hasChanged above checks only if there are any changes T value = updater.getValue(current, previous); result.put(namespace, value); } }); return result; } @Override public void apply(Map value, Settings current, Settings previous) { consumer.accept(value); } }; } @Override public T get(Settings settings) { throw new UnsupportedOperationException( "affix settings can't return values" + " use #getConcreteSetting to obtain a concrete setting" ); } @Override public String innerGetRaw(final Settings settings) { throw new UnsupportedOperationException( "affix settings can't return values" + " use #getConcreteSetting to obtain a concrete setting" ); } @Override public Setting getConcreteSetting(String key) { if (match(key)) { String namespace = this.key.getNamespace(key); return delegateFactory.apply(namespace, key); } else { throw new IllegalArgumentException("key [" + key + "] must match [" + getKey() + "] but didn't."); } } private Setting getConcreteSetting(String namespace, String key) { if (match(key)) { return delegateFactory.apply(namespace, key); } else { throw new IllegalArgumentException("key [" + key + "] must match [" + getKey() + "] but didn't."); } } /** * Get a setting with the given namespace filled in for prefix and suffix. */ public Setting getConcreteSettingForNamespace(String namespace) { if (namespace == null) { throw new IllegalArgumentException("Namespace should not be null"); } String fullKey = key.toConcreteKey(namespace).toString(); return getConcreteSetting(namespace, fullKey); } @Override public void diff(Settings.Builder builder, Settings source, Settings defaultSettings) { matchStream(defaultSettings).forEach((key) -> getConcreteSetting(key).diff(builder, source, defaultSettings)); } /** * Returns the namespace for a concrete setting. Ie. an affix setting with prefix: {@code search.} and suffix: {@code username} * will return {@code remote} as a namespace for the setting {@code cluster.remote.username} */ public String getNamespace(Setting concreteSetting) { return key.getNamespace(concreteSetting.getKey()); } /** * Returns a stream of all concrete setting instances for the given settings. AffixSetting is only a specification, concrete * settings depend on an actual set of setting keys. */ public Stream> getAllConcreteSettings(Settings settings) { return matchStream(settings).distinct().map(this::getConcreteSetting); } /** * Returns distinct namespaces for the given settings */ public Set getNamespaces(Settings settings) { return settings.keySet().stream().filter(this::match).map(key::getNamespace).collect(Collectors.toSet()); } /** * Returns a map of all namespaces to its values give the provided settings */ public Map getAsMap(Settings settings) { Map map = new HashMap<>(); matchStream(settings).distinct().forEach(key -> { String namespace = this.key.getNamespace(key); Setting concreteSetting = getConcreteSetting(namespace, key); map.put(namespace, concreteSetting.get(settings)); }); return Collections.unmodifiableMap(map); } } /** * Represents a validator for a setting. The {@link #validate(Object)} method is invoked early in the update setting process with the * value of this setting for a fail-fast validation. Later on, the {@link #validate(Object, Map)} and * {@link #validate(Object, Map, boolean)} methods are invoked with the value of this setting and a map from the settings specified by * {@link #settings()}} to their values. All these values come from the same {@link Settings} instance. * * @param the type of the {@link Setting} * * @opensearch.api */ @FunctionalInterface @PublicApi(since = "1.0.0") public interface Validator { /** * Validate this setting's value in isolation. * * @param value the value of this setting */ void validate(T value); /** * Validate this setting against its dependencies, specified by {@link #settings()}. The default implementation does nothing, * accepting any value as valid as long as it passes the validation in {@link #validate(Object)}. * * @param value the value of this setting * @param settings a map from the settings specified by {@link #settings()}} to their values */ default void validate(T value, Map, Object> settings) {} /** * Validate this setting against its dependencies, specified by {@link #settings()}. This method allows validation logic * to evaluate whether the setting will be present in the {@link Settings} after the update. The default implementation * does nothing, accepting any value as valid as long as it passes the validation in {@link #validate(Object)}. * * @param value the value of this setting * @param settings a map from the settings specified by {@link #settings()}} to their values * @param isPresent boolean indicating if this setting is present */ default void validate(T value, Map, Object> settings, boolean isPresent) {} /** * The settings on which the validity of this setting depends. The values of the specified settings are passed to * {@link #validate(Object, Map)}. By default this returns an empty iterator, indicating that this setting does not depend on any * other settings. * * @return the settings on which the validity of this setting depends. */ default Iterator> settings() { return Collections.emptyIterator(); } } /** * A group setting * * @opensearch.internal */ private static class GroupSetting extends Setting { private final String key; private final Consumer validator; private GroupSetting(String key, Consumer validator, Property... properties) { super(new GroupKey(key), (s) -> "", (s) -> null, properties); this.key = key; this.validator = validator; } private GroupSetting(String key, Setting fallback, Consumer validator, Property... properties) { super(new GroupKey(key), fallback, (s) -> null, properties); this.key = key; this.validator = validator; } @Override public boolean isGroupSetting() { return true; } @Override public String innerGetRaw(final Settings settings) { Settings subSettings = get(settings); try { XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); subSettings.toXContent(builder, EMPTY_PARAMS); builder.endObject(); return builder.toString(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public Settings get(Settings settings) { Settings byPrefix = settings.getByPrefix(getKey()); if (byPrefix.size() == 0 && this.fallbackSetting != null) { byPrefix = fallbackSetting.get(settings); } validator.accept(byPrefix); return byPrefix; } @Override public boolean exists(Settings settings) { for (String settingsKey : settings.keySet()) { if (settingsKey.startsWith(key)) { return true; } } if (this.fallbackSetting != null) { return fallbackSetting.exists(settings); } else { return false; } } @Override public void diff(Settings.Builder builder, Settings source, Settings defaultSettings) { Set leftGroup = get(source).keySet(); Settings defaultGroup = get(defaultSettings); builder.put( Settings.builder().put(defaultGroup.filter(k -> leftGroup.contains(k) == false), false).normalizePrefix(getKey()).build(), false ); } @Override public AbstractScopedSettings.SettingUpdater newUpdater( Consumer consumer, Logger logger, Consumer validator ) { if (isDynamic() == false) { throw new IllegalStateException("setting [" + getKey() + "] is not dynamic"); } final Setting setting = this; return new AbstractScopedSettings.SettingUpdater() { @Override public boolean hasChanged(Settings current, Settings previous) { Settings currentSettings = get(current); Settings previousSettings = get(previous); return currentSettings.equals(previousSettings) == false; } @Override public Settings getValue(Settings current, Settings previous) { Settings currentSettings = get(current); Settings previousSettings = get(previous); try { validator.accept(currentSettings); } catch (Exception | AssertionError e) { String err = "illegal value can't update [" + key + "]" + (isFiltered() ? "" : " from [" + previousSettings + "] to [" + currentSettings + "]"); throw new IllegalArgumentException(err, e); } return currentSettings; } @Override public void apply(Settings value, Settings current, Settings previous) { Setting.logSettingUpdate(GroupSetting.this, current, previous, logger); consumer.accept(value); } @Override public String toString() { return "Updater for: " + setting.toString(); } }; } } private final class Updater implements AbstractScopedSettings.SettingUpdater { private final Consumer consumer; private final Logger logger; private final Consumer accept; Updater(Consumer consumer, Logger logger, Consumer accept) { this.consumer = consumer; this.logger = logger; this.accept = accept; } @Override public String toString() { return "Updater for: " + Setting.this.toString(); } @Override public boolean hasChanged(Settings current, Settings previous) { final String newValue = getRaw(current); final String value = getRaw(previous); assert isGroupSetting() == false : "group settings must override this method"; assert value != null : "value was null but can't be unless default is null which is invalid"; return value.equals(newValue) == false; } @Override public T getValue(Settings current, Settings previous) { final String newValue = getRaw(current); final String value = getRaw(previous); try { T inst = get(current); accept.accept(inst); return inst; } catch (Exception | AssertionError e) { if (isFiltered()) { throw new IllegalArgumentException("illegal value can't update [" + key + "]"); } else { throw new IllegalArgumentException( "illegal value can't update [" + key + "] from [" + value + "] to [" + newValue + "]", e ); } } } @Override public void apply(T value, Settings current, Settings previous) { logSettingUpdate(Setting.this, current, previous, logger); consumer.accept(value); } } public static Setting versionSetting(final String key, final Version defaultValue, Property... properties) { return new Setting<>(key, s -> Integer.toString(defaultValue.id), s -> Version.fromId(Integer.parseInt(s)), properties); } private static boolean isFiltered(Property[] properties) { return properties != null && Arrays.asList(properties).contains(Property.Filtered); } /** * A writeable validator able to check the value of string type custom setting by using regular expression */ public static class RegexValidator implements Writeable, Validator { private Pattern pattern; private boolean isMatching; /** * @param regex A regular expression containing the only valid input for this setting. */ public RegexValidator(String regex) { this(regex, true); } /** * @param regex constructs a validator based on a regular expression. * @param isMatching If true, the setting must match the given regex. If false, the setting must not match the given regex. */ public RegexValidator(String regex, boolean isMatching) { this.pattern = Pattern.compile(regex); this.isMatching = isMatching; } public RegexValidator(StreamInput in) throws IOException { this.pattern = Pattern.compile(in.readString()); this.isMatching = in.readBoolean(); } Pattern getPattern() { return pattern; } @Override public void validate(String value) { if (isMatching && !pattern.matcher(value).find()) { throw new IllegalArgumentException("Setting [" + value + "] does not match regex [" + pattern.pattern() + "]"); } else if (!isMatching && pattern.matcher(value).find()) { throw new IllegalArgumentException("Setting [" + value + "] must match regex [" + pattern.pattern() + "]"); } } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(pattern.pattern()); out.writeBoolean(isMatching); } } // Float private static float parseFloat(String s, float minValue, float maxValue, String key, boolean isFiltered) { float value = Float.parseFloat(s); if (value < minValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be >= " + minValue; throw new IllegalArgumentException(err); } if (value > maxValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be <= " + maxValue; throw new IllegalArgumentException(err); } return value; } // Setting with defaultValue public static Setting floatSetting(String key, float defaultValue, Property... properties) { return floatSetting(key, defaultValue, Float.MIN_VALUE, Float.MAX_VALUE, properties); } public static Setting floatSetting(String key, float defaultValue, float minValue, Property... properties) { return floatSetting(key, defaultValue, minValue, Float.MAX_VALUE, properties); } public static Setting floatSetting(String key, float defaultValue, float minValue, float maxValue, Property... properties) { return floatSetting(key, defaultValue, minValue, maxValue, v -> {}, properties); } public static Setting floatSetting( String key, float defaultValue, float minValue, float maxValue, Validator validator, Property... properties ) { return new Setting<>( key, Float.toString(defaultValue), new FloatParser(minValue, maxValue, key, isFiltered(properties)), validator, properties ); } /** * A writeable parser for float * */ public static class FloatParser implements Function, Writeable { private float minValue; private float maxValue; private String key; private boolean isFiltered; public FloatParser(float minValue, float maxValue, String key, boolean isFiltered) { this.minValue = minValue; this.maxValue = maxValue; this.key = key; this.isFiltered = isFiltered; } public FloatParser(StreamInput in) throws IOException { minValue = in.readFloat(); maxValue = in.readFloat(); key = in.readString(); isFiltered = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeFloat(minValue); out.writeFloat(maxValue); out.writeString(key); out.writeBoolean(isFiltered); } public float getMin() { return minValue; } public float getMax() { return maxValue; } public String getKey() { return key; } public boolean getFilterStatus() { return isFiltered; } @Override public Float apply(String s) { return parseFloat(s, minValue, maxValue, key, isFiltered); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; FloatParser that = (FloatParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(maxValue, that.maxValue) && Objects.equals(isFiltered, that.isFiltered); } public int hashCode() { return Objects.hash(minValue, maxValue, key, isFiltered); } } // Setting with fallback public static Setting floatSetting(String key, Setting fallbackSetting, Property... properties) { return floatSetting(key, fallbackSetting, Float.MIN_VALUE, Float.MAX_VALUE, properties); } public static Setting floatSetting(String key, Setting fallbackSetting, float minValue, Property... properties) { return floatSetting(key, fallbackSetting, minValue, Float.MAX_VALUE, properties); } public static Setting floatSetting( String key, Setting fallbackSetting, float minValue, float maxValue, Property... properties ) { return floatSetting(key, fallbackSetting, minValue, maxValue, v -> {}, properties); } public static Setting floatSetting( String key, Setting fallbackSetting, float minValue, float maxValue, Validator validator, Property... properties ) { return new Setting<>( new SimpleKey(key), fallbackSetting, fallbackSetting::getRaw, (s) -> parseFloat(s, minValue, maxValue, key, isFiltered(properties)), validator, properties ); } // Integer public static int parseInt(String s, int minValue, String key) { return parseInt(s, minValue, Integer.MAX_VALUE, key); } public static int parseInt(String s, int minValue, int maxValue, String key) { return parseInt(s, minValue, maxValue, key, false); } public static int parseInt(String s, int minValue, int maxValue, String key, boolean isFiltered) { int value = Integer.parseInt(s); if (value < minValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be >= " + minValue; throw new IllegalArgumentException(err); } if (value > maxValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be <= " + maxValue; throw new IllegalArgumentException(err); } return value; } // Setting with defaultValue public static Setting intSetting(String key, int defaultValue, Property... properties) { return intSetting(key, defaultValue, Integer.MIN_VALUE, Integer.MAX_VALUE, properties); } public static Setting intSetting(String key, int defaultValue, int minValue, Property... properties) { return intSetting(key, defaultValue, minValue, Integer.MAX_VALUE, properties); } public static Setting intSetting(String key, int defaultValue, int minValue, int maxValue, Property... properties) { return intSetting(key, defaultValue, minValue, maxValue, v -> {}, properties); } public static Setting intSetting( String key, int defaultValue, int minValue, Validator validator, Property... properties ) { return intSetting(key, defaultValue, minValue, Integer.MAX_VALUE, validator, properties); } public static Setting intSetting( String key, int defaultValue, int minValue, int maxValue, Validator validator, Property... properties ) { return new Setting<>( key, Integer.toString(defaultValue), new IntegerParser(minValue, maxValue, key, isFiltered(properties)), validator, properties ); } /** * A writeable parser for integer * */ public static class IntegerParser implements Function, Writeable { private String key; private int minValue; private int maxValue; private boolean isFiltered; public IntegerParser(int minValue, int maxValue, String key, boolean isFiltered) { this.minValue = minValue; this.maxValue = maxValue; this.key = key; this.isFiltered = isFiltered; } public IntegerParser(StreamInput in) throws IOException { minValue = in.readInt(); maxValue = in.readInt(); key = in.readString(); isFiltered = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeInt(minValue); out.writeInt(maxValue); out.writeString(key); out.writeBoolean(isFiltered); } public int getMin() { return minValue; } public int getMax() { return maxValue; } public String getKey() { return key; } public boolean getFilterStatus() { return isFiltered; } @Override public Integer apply(String s) { return parseInt(s, minValue, maxValue, key, isFiltered); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; IntegerParser that = (IntegerParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(maxValue, that.maxValue) && Objects.equals(isFiltered, that.isFiltered); } public int hashCode() { return Objects.hash(minValue, maxValue, key, isFiltered); } } // Setting with fallback public static Setting intSetting(String key, Setting fallbackSetting, Property... properties) { return intSetting(key, fallbackSetting, Integer.MIN_VALUE, Integer.MAX_VALUE, properties); } public static Setting intSetting(String key, Setting fallbackSetting, int minValue, Property... properties) { return intSetting(key, fallbackSetting, minValue, Integer.MAX_VALUE, properties); } public static Setting intSetting( String key, Setting fallbackSetting, int minValue, int maxValue, Property... properties ) { return intSetting(key, fallbackSetting, minValue, maxValue, v -> {}, properties); } public static Setting intSetting( String key, Setting fallbackSetting, int minValue, Validator validator, Property... properties ) { return intSetting(key, fallbackSetting, minValue, Integer.MAX_VALUE, validator, properties); } public static Setting intSetting( String key, Setting fallbackSetting, int minValue, int maxValue, Validator validator, Property... properties ) { return new Setting<>( new SimpleKey(key), fallbackSetting, fallbackSetting::getRaw, (s) -> parseInt(s, minValue, maxValue, key, isFiltered(properties)), validator, properties ); } // Long private static long parseLong(String s, long minValue, long maxValue, String key, boolean isFiltered) { long value = Long.parseLong(s); if (value < minValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be >= " + minValue; throw new IllegalArgumentException(err); } if (value > maxValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be <= " + maxValue; throw new IllegalArgumentException(err); } return value; } // Setting with defaultValue public static Setting longSetting(String key, long defaultValue, Property... properties) { return longSetting(key, defaultValue, Long.MIN_VALUE, Long.MAX_VALUE, properties); } public static Setting longSetting(String key, long defaultValue, long minValue, Property... properties) { return longSetting(key, defaultValue, minValue, Long.MAX_VALUE, properties); } public static Setting longSetting(String key, long defaultValue, long minValue, long maxValue, Property... properties) { return longSetting(key, defaultValue, minValue, maxValue, v -> {}, properties); } public static Setting longSetting( String key, long defaultValue, long minValue, long maxValue, Validator validator, Property... properties ) { return new Setting<>( key, Long.toString(defaultValue), new LongParser(minValue, maxValue, key, isFiltered(properties)), validator, properties ); } /** * A writeable parser for long * */ public static class LongParser implements Function, Writeable { private String key; private long minValue; private long maxValue; private boolean isFiltered; public LongParser(long minValue, long maxValue, String key, boolean isFiltered) { this.minValue = minValue; this.maxValue = maxValue; this.key = key; this.isFiltered = isFiltered; } public LongParser(StreamInput in) throws IOException { minValue = in.readLong(); maxValue = in.readLong(); key = in.readString(); isFiltered = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeLong(minValue); out.writeLong(maxValue); out.writeString(key); out.writeBoolean(isFiltered); } public long getMin() { return minValue; } public long getMax() { return maxValue; } public String getKey() { return key; } public boolean getFilterStatus() { return isFiltered; } @Override public Long apply(String s) { return parseLong(s, minValue, maxValue, key, isFiltered); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; LongParser that = (LongParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(maxValue, that.maxValue) && Objects.equals(isFiltered, that.isFiltered); } public int hashCode() { return Objects.hash(minValue, maxValue, key, isFiltered); } } // Setting with fallback public static Setting longSetting(String key, Setting fallbackSetting, Property... properties) { return longSetting(key, fallbackSetting, Long.MIN_VALUE, Long.MAX_VALUE, properties); } public static Setting longSetting(String key, Setting fallbackSetting, long minValue, Property... properties) { return longSetting(key, fallbackSetting, minValue, Long.MAX_VALUE, properties); } public static Setting longSetting( String key, Setting fallbackSetting, long minValue, long maxValue, Property... properties ) { return longSetting(key, fallbackSetting, minValue, maxValue, v -> {}, properties); } public static Setting longSetting( String key, Setting fallbackSetting, long minValue, Validator validator, Property... properties ) { return longSetting(key, fallbackSetting, minValue, Long.MAX_VALUE, validator, properties); } public static Setting longSetting( String key, Setting fallbackSetting, long minValue, long maxValue, Validator validator, Property... properties ) { return new Setting<>( new SimpleKey(key), fallbackSetting, fallbackSetting::getRaw, (s) -> parseLong(s, minValue, maxValue, key, isFiltered(properties)), validator, properties ); } // Double private static double parseDouble(String s, double minValue, double maxValue, String key, boolean isFiltered) { double value = Double.parseDouble(s); if (value < minValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be >= " + minValue; throw new IllegalArgumentException(err); } if (value > maxValue) { String err = "Failed to parse value" + (isFiltered ? "" : " [" + s + "]") + " for setting [" + key + "] must be <= " + maxValue; throw new IllegalArgumentException(err); } return value; } // Setting with defaultValue public static Setting doubleSetting(String key, double defaultValue, Property... properties) { return doubleSetting(key, defaultValue, Double.MIN_VALUE, Double.MAX_VALUE, properties); } public static Setting doubleSetting(String key, double defaultValue, double minValue, Property... properties) { return doubleSetting(key, defaultValue, minValue, Double.MAX_VALUE, properties); } public static Setting doubleSetting(String key, double defaultValue, double minValue, double maxValue, Property... properties) { return doubleSetting(key, defaultValue, minValue, maxValue, v -> {}, properties); } public static Setting doubleSetting( String key, double defaultValue, double minValue, double maxValue, Validator validator, Property... properties ) { return new Setting<>( key, Double.toString(defaultValue), new DoubleParser(minValue, maxValue, key, isFiltered(properties)), validator, properties ); } /** * A writeable parser for double * */ public static class DoubleParser implements Function, Writeable { private double minValue; private double maxValue; private String key; private boolean isFiltered; public DoubleParser(double minValue, double maxValue, String key, boolean isFiltered) { this.minValue = minValue; this.maxValue = maxValue; this.key = key; this.isFiltered = isFiltered; } public DoubleParser(StreamInput in) throws IOException { minValue = in.readDouble(); maxValue = in.readDouble(); key = in.readString(); isFiltered = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeDouble(minValue); out.writeDouble(maxValue); out.writeString(key); out.writeBoolean(isFiltered); } public double getMin() { return minValue; } public double getMax() { return maxValue; } public String getKey() { return key; } public boolean getFilterStatus() { return isFiltered; } @Override public Double apply(String s) { return parseDouble(s, minValue, maxValue, key, isFiltered); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; DoubleParser that = (DoubleParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(maxValue, that.maxValue) && Objects.equals(isFiltered, that.isFiltered); } public int hashCode() { return Objects.hash(minValue, maxValue, key, isFiltered); } } // Setting with fallback public static Setting doubleSetting(String key, Setting fallbackSetting, Property... properties) { return doubleSetting(key, fallbackSetting, Double.MIN_VALUE, Double.MAX_VALUE, properties); } public static Setting doubleSetting(String key, Setting fallbackSetting, double minValue, Property... properties) { return doubleSetting(key, fallbackSetting, minValue, Double.MAX_VALUE, properties); } public static Setting doubleSetting( String key, Setting fallbackSetting, double minValue, double maxValue, Property... properties ) { return doubleSetting(key, fallbackSetting, minValue, maxValue, v -> {}, properties); } public static Setting doubleSetting( String key, Setting fallbackSetting, double minValue, double maxValue, Validator validator, Property... properties ) { return new Setting<>( new SimpleKey(key), fallbackSetting, fallbackSetting::getRaw, (s) -> parseDouble(s, minValue, maxValue, key, isFiltered(properties)), validator, properties ); } /// simpleString public static Setting simpleString(String key, Property... properties) { return new Setting<>(key, s -> "", Function.identity(), properties); } public static Setting simpleString(String key, Validator validator, Property... properties) { return new Setting<>(new SimpleKey(key), null, s -> "", Function.identity(), validator, properties); } public static Setting simpleString(String key, Validator validator, Setting fallback, Property... properties) { return new Setting<>(new SimpleKey(key), fallback, fallback::getRaw, Function.identity(), validator, properties); } public static Setting simpleString(String key, String defaultValue, Validator validator, Property... properties) { validator.validate(defaultValue); return new Setting<>(new SimpleKey(key), null, s -> defaultValue, Function.identity(), validator, properties); } public static Setting simpleString(String key, Setting fallback, Property... properties) { return simpleString(key, fallback, Function.identity(), properties); } public static Setting simpleString( final String key, final Setting fallback, final Function parser, final Property... properties ) { return new Setting<>(key, fallback, parser, properties); } /** * Creates a new Setting instance with a String value * * @param key the settings key for this setting. * @param defaultValue the default String value. * @param properties properties for this setting like scope, filtering... * @return the Setting Object */ public static Setting simpleString(String key, String defaultValue, Property... properties) { return new Setting<>(key, s -> defaultValue, Function.identity(), properties); } public static TimeValue parseTimeValue(String s, TimeValue minValue, String key) { TimeValue timeValue = TimeValue.parseTimeValue(s, null, key); if (timeValue.millis() < minValue.millis()) { throw new IllegalArgumentException("Failed to parse value [" + s + "] for setting [" + key + "] must be >= " + minValue); } return timeValue; } public static Setting boolSetting(String key, boolean defaultValue, Property... properties) { return new Setting<>(key, (s) -> Boolean.toString(defaultValue), b -> parseBoolean(b, key, isFiltered(properties)), properties); } public static Setting boolSetting(String key, Setting fallbackSetting, Property... properties) { return new Setting<>(key, fallbackSetting, b -> parseBoolean(b, key, isFiltered(properties)), properties); } public static Setting boolSetting( String key, Setting fallbackSetting, Validator validator, Property... properties ) { return new Setting<>( new SimpleKey(key), fallbackSetting, fallbackSetting::getRaw, b -> parseBoolean(b, key, isFiltered(properties)), validator, properties ); } public static Setting boolSetting(String key, boolean defaultValue, Validator validator, Property... properties) { return new Setting<>(key, Boolean.toString(defaultValue), b -> parseBoolean(b, key, isFiltered(properties)), validator, properties); } public static Setting boolSetting(String key, Function defaultValueFn, Property... properties) { return new Setting<>(key, defaultValueFn, b -> parseBoolean(b, key, isFiltered(properties)), properties); } static boolean parseBoolean(String b, String key, boolean isFiltered) { try { return Booleans.parseBoolean(b); } catch (IllegalArgumentException ex) { if (isFiltered) { throw new IllegalArgumentException("Failed to parse value for setting [" + key + "]"); } else { throw ex; } } } public static Setting byteSizeSetting(String key, ByteSizeValue value, Property... properties) { return byteSizeSetting(key, (s) -> value.getBytes() + ByteSizeUnit.BYTES.getSuffix(), properties); } public static Setting byteSizeSetting(String key, Setting fallbackSetting, Property... properties) { return new Setting<>(key, fallbackSetting, new ByteSizeValueParser(key), properties); } public static Setting byteSizeSetting(String key, Function defaultValue, Property... properties) { return new Setting<>(key, defaultValue, new ByteSizeValueParser(key), properties); } public static Setting byteSizeSetting( String key, ByteSizeValue defaultValue, ByteSizeValue minValue, ByteSizeValue maxValue, Property... properties ) { return byteSizeSetting(key, (s) -> defaultValue.getStringRep(), minValue, maxValue, properties); } public static Setting byteSizeSetting( String key, Function defaultValue, ByteSizeValue minValue, ByteSizeValue maxValue, Property... properties ) { return new Setting<>(key, defaultValue, new ByteSizeValueParser(minValue, maxValue, key), properties); } /** * A writeable parser for bytesizevalue * */ public static class ByteSizeValueParser implements Function, Writeable { private ByteSizeValue minValue; private ByteSizeValue maxValue; private String key; public ByteSizeValueParser(ByteSizeValue minValue, ByteSizeValue maxValue, String key) { this.minValue = minValue; this.maxValue = maxValue; this.key = key; } public ByteSizeValueParser(String key) { this(new ByteSizeValue(-1), new ByteSizeValue(Long.MAX_VALUE), key); } public ByteSizeValueParser(StreamInput in) throws IOException { minValue = new ByteSizeValue(in); maxValue = new ByteSizeValue(in); key = in.readString(); } @Override public void writeTo(StreamOutput out) throws IOException { ((ByteSizeValue) minValue).writeTo(out); ((ByteSizeValue) maxValue).writeTo(out); out.writeString(key); } public ByteSizeValue getMin() { return minValue; } public ByteSizeValue getMax() { return maxValue; } public String getKey() { return key; } @Override public ByteSizeValue apply(String s) { return parseByteSize(s, minValue, maxValue, key); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; ByteSizeValueParser that = (ByteSizeValueParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(maxValue, that.maxValue); } public int hashCode() { return Objects.hash(key, minValue, maxValue); } } public static ByteSizeValue parseByteSize(String s, ByteSizeValue minValue, ByteSizeValue maxValue, String key) { ByteSizeValue value = ByteSizeValue.parseBytesSizeValue(s, key); if (value.getBytes() < minValue.getBytes()) { final String message = String.format( Locale.ROOT, "failed to parse value [%s] for setting [%s], must be >= [%s]", s, key, minValue.getStringRep() ); throw new IllegalArgumentException(message); } if (value.getBytes() > maxValue.getBytes()) { final String message = String.format( Locale.ROOT, "failed to parse value [%s] for setting [%s], must be <= [%s]", s, key, maxValue.getStringRep() ); throw new IllegalArgumentException(message); } return value; } /** * Creates a setting which specifies a memory size. This can either be * specified as an absolute bytes value or as a percentage of the heap * memory. * * @param key the key for the setting * @param defaultValue the default value for this setting * @param properties properties for this setting like scope, filtering... * @return the setting object */ public static Setting memorySizeSetting(String key, ByteSizeValue defaultValue, Property... properties) { return memorySizeSetting(key, (s) -> defaultValue.toString(), properties); } /** * Creates a setting which specifies a memory size. This can either be * specified as an absolute bytes value or as a percentage of the heap * memory. * * @param key the key for the setting * @param defaultValue a function that supplies the default value for this setting * @param properties properties for this setting like scope, filtering... * @return the setting object */ public static Setting memorySizeSetting(String key, Function defaultValue, Property... properties) { return new Setting<>(key, defaultValue, new MemorySizeValueParser(key), properties); } /** * Creates a setting which specifies a memory size. This can either be * specified as an absolute bytes value or as a percentage of the heap * memory. * * @param key the key for the setting * @param defaultPercentage the default value of this setting as a percentage of the heap memory * @param properties properties for this setting like scope, filtering... * @return the setting object */ public static Setting memorySizeSetting(String key, String defaultPercentage, Property... properties) { return new Setting<>(key, (s) -> defaultPercentage, new MemorySizeValueParser(key), properties); } /** * A writeable parser for memory size value */ public static class MemorySizeValueParser implements Function, Writeable { private String key; public MemorySizeValueParser(String key) { this.key = key; } public MemorySizeValueParser(StreamInput in) throws IOException { key = in.readString(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(key); } public String getKey() { return key; } @Override public ByteSizeValue apply(String s) { return MemorySizeValue.parseBytesSizeValueOrHeapRatio(s, key); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; MemorySizeValueParser that = (MemorySizeValueParser) obj; return Objects.equals(key, that.key); } public int hashCode() { return Objects.hash(key); } } /** * Creates a setting which specifies a memory size. This can either be * specified as an absolute bytes value or as a percentage of the heap * memory. * * @param key the key for the setting * @param fallbackSetting a memory size setting to use as fallback * @param properties properties for this setting like scope, filtering... * @return the setting object */ public static Setting memorySizeSetting(String key, Setting fallbackSetting, Property... properties) { return new Setting<>(key, fallbackSetting, (s) -> MemorySizeValue.parseBytesSizeValueOrHeapRatio(s, key), properties); } public static Setting> listSetting( final String key, final List defaultStringValue, final Function singleValueParser, final Property... properties ) { return listSetting(key, null, singleValueParser, (s) -> defaultStringValue, properties); } public static Setting> listSetting( final String key, final List defaultStringValue, final Function singleValueParser, final Validator> validator, final Property... properties ) { return listSetting(key, null, singleValueParser, (s) -> defaultStringValue, validator, properties); } // TODO this one's two argument get is still broken public static Setting> listSetting( final String key, final Setting> fallbackSetting, final Function singleValueParser, final Property... properties ) { return listSetting(key, fallbackSetting, singleValueParser, (s) -> parseableStringToList(fallbackSetting.getRaw(s)), properties); } public static Setting> listSetting( final String key, final Function singleValueParser, final Function> defaultStringValue, final Property... properties ) { return listSetting(key, null, singleValueParser, defaultStringValue, properties); } public static Setting> listSetting( final String key, final Function singleValueParser, final Function> defaultStringValue, final Validator> validator, final Property... properties ) { return listSetting(key, null, singleValueParser, defaultStringValue, validator, properties); } public static Setting> listSetting( final String key, final @Nullable Setting> fallbackSetting, final Function singleValueParser, final Function> defaultStringValue, final Property... properties ) { return listSetting(key, fallbackSetting, singleValueParser, defaultStringValue, v -> {}, properties); } public static Setting> listSetting( final String key, final @Nullable Setting> fallbackSetting, final Function singleValueParser, final Function> defaultStringValue, final Validator> validator, final Property... properties ) { if (defaultStringValue.apply(Settings.EMPTY) == null) { throw new IllegalArgumentException("default value function must not return null"); } Function> parser = (s) -> parseableStringToList(s).stream().map(singleValueParser).collect(Collectors.toList()); return new ListSetting<>(key, fallbackSetting, defaultStringValue, parser, validator, properties); } private static List parseableStringToList(String parsableString) { // fromXContent doesn't use named xcontent or deprecation. try ( XContentParser xContentParser = MediaTypeRegistry.JSON.xContent() .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, parsableString) ) { XContentParser.Token token = xContentParser.nextToken(); if (token != XContentParser.Token.START_ARRAY) { throw new IllegalArgumentException("expected START_ARRAY but got " + token); } ArrayList list = new ArrayList<>(); while ((token = xContentParser.nextToken()) != XContentParser.Token.END_ARRAY) { if (token != XContentParser.Token.VALUE_STRING) { throw new IllegalArgumentException("expected VALUE_STRING but got " + token); } list.add(xContentParser.text()); } return list; } catch (IOException e) { throw new IllegalArgumentException("failed to parse array", e); } } private static String arrayToParsableString(List array) { try { XContentBuilder builder = XContentBuilder.builder(MediaTypeRegistry.JSON.xContent()); builder.startArray(); for (String element : array) { builder.value(element); } builder.endArray(); return builder.toString(); } catch (IOException ex) { throw new OpenSearchException(ex); } } /** * A list setting * * @opensearch.internal */ private static class ListSetting extends Setting> { private final Function> defaultStringValue; private ListSetting( final String key, final @Nullable Setting> fallbackSetting, final Function> defaultStringValue, final Function> parser, final Validator> validator, final Property... properties ) { super( new ListKey(key), fallbackSetting, s -> Setting.arrayToParsableString(defaultStringValue.apply(s)), parser, validator, properties ); this.defaultStringValue = defaultStringValue; } @Override String innerGetRaw(final Settings settings) { List array = settings.getAsList(getKey(), null); return array == null ? defaultValue.apply(settings) : arrayToParsableString(array); } @Override boolean hasComplexMatcher() { return true; } @Override public void diff(Settings.Builder builder, Settings source, Settings defaultSettings) { if (exists(source) == false) { List asList = defaultSettings.getAsList(getKey(), null); if (asList == null) { builder.putList(getKey(), defaultStringValue.apply(defaultSettings)); } else { builder.putList(getKey(), asList); } } } } static void logSettingUpdate(Setting setting, Settings current, Settings previous, Logger logger) { if (logger.isInfoEnabled()) { if (setting.isFiltered()) { logger.info("updating [{}]", setting.key); } else { logger.info("updating [{}] from [{}] to [{}]", setting.key, setting.getRaw(previous), setting.getRaw(current)); } } } /** * Creates a group of settings prefixed by a key. * * @param key the group key for the setting * @param properties properties for this setting like scope, filtering... * @return the group setting object */ public static Setting groupSetting(String key, Property... properties) { return groupSetting(key, (s) -> {}, properties); } /** * Creates a group of settings prefixed by a key. * * @param key the group key for the setting * @param validator a {@link Validator} for validating this setting * @param properties properties for this setting like scope, filtering... * @return the group setting object */ public static Setting groupSetting(String key, Consumer validator, Property... properties) { return new GroupSetting(key, validator, properties); } /** * Creates a group of settings prefixed by a key. * * @param key the group key for the setting * @param fallback a {@link GroupSetting} to use as fallback when no group key values exist * @param properties properties for this setting like scope, filtering... * @return the group setting object */ public static Setting groupSetting(String key, final Setting fallback, Property... properties) { return groupSetting(key, fallback, (s) -> {}, properties); } /** * Creates a group of settings prefixed by a key. * * @param key the group key for the setting * @param fallback a {@link GroupSetting} to use as fallback when no group key values exist * @param validator a {@link Validator} for validating this setting * @param properties properties for this setting like scope, filtering... * @return the group setting object */ public static Setting groupSetting( String key, final Setting fallback, Consumer validator, Property... properties ) { return new GroupSetting(key, fallback, validator, properties); } public static Setting timeSetting( final String key, final Setting fallbackSetting, final TimeValue minValue, final Property... properties ) { final SimpleKey simpleKey = new SimpleKey(key); return new Setting<>( simpleKey, fallbackSetting, fallbackSetting::getRaw, minTimeValueParser(key, minValue, isFiltered(properties)), v -> {}, properties ); } /** * A writeable parser for time value only has min value * */ public static class MinTimeValueParser implements Function, Writeable { private String key; private TimeValue minValue; private boolean isFiltered; public MinTimeValueParser(String key, TimeValue minValue, boolean isFiltered) { this.key = key; this.minValue = minValue; this.isFiltered = isFiltered; } public MinTimeValueParser(StreamInput in) throws IOException { key = in.readString(); minValue = in.readTimeValue(); isFiltered = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(key); out.writeTimeValue(minValue); out.writeBoolean(isFiltered); } public TimeValue getMin() { return minValue; } public String getKey() { return key; } public boolean getFilterStatus() { return isFiltered; } @Override public TimeValue apply(String s) { return minTimeValueParser(key, minValue, isFiltered).apply(s); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; MinTimeValueParser that = (MinTimeValueParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(isFiltered, that.isFiltered); } public int hashCode() { return Objects.hash(key, minValue, isFiltered); } } public static Setting timeSetting( final String key, Function defaultValue, final TimeValue minValue, final Property... properties ) { final SimpleKey simpleKey = new SimpleKey(key); return new Setting<>( simpleKey, s -> defaultValue.apply(s).getStringRep(), new MinTimeValueParser(key, minValue, isFiltered(properties)), properties ); } public static Setting timeSetting( final String key, TimeValue defaultValue, final TimeValue minValue, final TimeValue maxValue, final Property... properties ) { final SimpleKey simpleKey = new SimpleKey(key); return new Setting<>( simpleKey, s -> defaultValue.getStringRep(), new MinMaxTimeValueParser(key, minValue, maxValue, isFiltered(properties)), properties ); } /** * A writeable parser for time value have min and max value * */ public static class MinMaxTimeValueParser implements Function, Writeable { private String key; private TimeValue minValue; private TimeValue maxValue; private boolean isFiltered; public MinMaxTimeValueParser(String key, TimeValue minValue, TimeValue maxValue, boolean isFiltered) { this.key = key; this.minValue = minValue; this.maxValue = maxValue; this.isFiltered = isFiltered; } public MinMaxTimeValueParser(StreamInput in) throws IOException { key = in.readString(); minValue = in.readTimeValue(); maxValue = in.readTimeValue(); isFiltered = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(key); out.writeTimeValue(minValue); out.writeTimeValue(maxValue); out.writeBoolean(isFiltered); } public TimeValue getMin() { return minValue; } public TimeValue getMax() { return maxValue; } public String getKey() { return key; } public boolean getFilterStatus() { return isFiltered; } @Override public TimeValue apply(String s) { return minMaxTimeValueParser(key, minValue, maxValue, isFiltered).apply(s); } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; MinMaxTimeValueParser that = (MinMaxTimeValueParser) obj; return Objects.equals(key, that.key) && Objects.equals(minValue, that.minValue) && Objects.equals(maxValue, that.maxValue) && Objects.equals(isFiltered, that.isFiltered); } public int hashCode() { return Objects.hash(key, minValue, maxValue, isFiltered); } } private static Function minTimeValueParser(final String key, final TimeValue minValue, boolean isFiltered) { return s -> { TimeValue value; try { value = TimeValue.parseTimeValue(s, null, key); } catch (RuntimeException ex) { if (isFiltered) { throw new IllegalArgumentException("failed to parse value for setting [" + key + "] as a time value"); } else { throw ex; } } if (value.millis() < minValue.millis()) { final String message = String.format( Locale.ROOT, "failed to parse value%s for setting [%s], must be >= [%s]", isFiltered ? "" : " [" + s + "]", key, minValue.getStringRep() ); throw new IllegalArgumentException(message); } return value; }; } private static Function minMaxTimeValueParser( final String key, final TimeValue minValue, final TimeValue maxValue, boolean isFiltered ) { return s -> { TimeValue value; try { value = minTimeValueParser(key, minValue, isFiltered).apply(s); } catch (RuntimeException ex) { if (isFiltered) { throw new IllegalArgumentException("failed to parse value for setting [" + key + "] as a time value"); } else { throw ex; } } if (value.millis() > maxValue.millis()) { final String message = String.format( Locale.ROOT, "failed to parse value%s for setting [%s], must be <= [%s]", isFiltered ? "" : " [" + s + "]", key, maxValue.getStringRep() ); throw new IllegalArgumentException(message); } return value; }; } public static Setting timeSetting(String key, TimeValue defaultValue, TimeValue minValue, Property... properties) { return timeSetting(key, (s) -> defaultValue, minValue, properties); } public static Setting timeSetting(String key, TimeValue defaultValue, Property... properties) { return new Setting<>(key, (s) -> defaultValue.getStringRep(), (s) -> TimeValue.parseTimeValue(s, key), properties); } public static Setting timeSetting(String key, Setting fallbackSetting, Property... properties) { return new Setting<>(key, fallbackSetting, (s) -> TimeValue.parseTimeValue(s, key), properties); } public static Setting timeSetting( String key, TimeValue defaultValue, TimeValue minValue, Validator validator, Property... properties ) { final SimpleKey simpleKey = new SimpleKey(key); return new Setting<>( simpleKey, s -> defaultValue.getStringRep(), minTimeValueParser(key, minValue, isFiltered(properties)), validator, properties ); } public static Setting timeSetting( String key, Setting fallBackSetting, Validator validator, Property... properties ) { return new Setting<>( new SimpleKey(key), fallBackSetting, fallBackSetting::getRaw, (s) -> TimeValue.parseTimeValue(s, key), validator, properties ); } public static Setting positiveTimeSetting(String key, TimeValue defaultValue, Property... properties) { return timeSetting(key, defaultValue, TimeValue.timeValueMillis(0), properties); } public static Setting positiveTimeSetting( final String key, final Setting fallbackSetting, final Property... properties ) { return timeSetting(key, fallbackSetting, TimeValue.timeValueMillis(0), properties); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Setting setting = (Setting) o; return Objects.equals(key, setting.key); } @Override public int hashCode() { return Objects.hash(key); } /** * This setting type allows to validate settings that have the same type and a common prefix. For instance feature.${type}=[true|false] * can easily be added with this setting. Yet, prefix key settings don't support updaters out of the box unless * {@link #getConcreteSetting(String)} is used to pull the updater. */ public static AffixSetting prefixKeySetting(String prefix, Function> delegateFactory) { BiFunction> delegateFactoryWithNamespace = (ns, k) -> delegateFactory.apply(k); return affixKeySetting(new AffixKey(prefix), delegateFactoryWithNamespace); } public static AffixSetting suffixKeySetting(String suffix, Function> delegateFactory) { BiFunction> delegateFactoryWithNamespace = (ns, k) -> delegateFactory.apply(k); AffixKey affixKey = new AffixKey(null, suffix); return affixKeySetting(affixKey, delegateFactoryWithNamespace); } /** * This setting type allows to validate settings that have the same type and a common prefix and suffix. For instance * storage.${backend}.enable=[true|false] can easily be added with this setting. Yet, affix key settings don't support updaters * out of the box unless {@link #getConcreteSetting(String)} is used to pull the updater. */ public static AffixSetting affixKeySetting( String prefix, String suffix, Function> delegateFactory, AffixSettingDependency... dependencies ) { BiFunction> delegateFactoryWithNamespace = (ns, k) -> delegateFactory.apply(k); return affixKeySetting(new AffixKey(prefix, suffix), delegateFactoryWithNamespace, dependencies); } public static AffixSetting affixKeySetting( String prefix, String suffix, BiFunction> delegateFactory, AffixSettingDependency... dependencies ) { Setting delegate = delegateFactory.apply("_na_", "_na_"); return new AffixSetting<>(new AffixKey(prefix, suffix), delegate, delegateFactory, dependencies); } private static AffixSetting affixKeySetting( AffixKey key, BiFunction> delegateFactory, AffixSettingDependency... dependencies ) { Setting delegate = delegateFactory.apply("_na_", "_na_"); return new AffixSetting<>(key, delegate, delegateFactory, dependencies); } /** * Key for the setting * * @opensearch.api */ @PublicApi(since = "1.0.0") public interface Key { boolean match(String key); } /** * A simple key for a setting * * @opensearch.api */ @PublicApi(since = "1.0.0") public static class SimpleKey implements Key { protected final String key; public SimpleKey(String key) { this.key = key; } @Override public boolean match(String key) { return this.key.equals(key); } @Override public String toString() { return key; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SimpleKey simpleKey = (SimpleKey) o; return Objects.equals(key, simpleKey.key); } @Override public int hashCode() { return Objects.hash(key); } } /** * Settings Group keys * * @opensearch.internal */ public static final class GroupKey extends SimpleKey { public GroupKey(String key) { super(key); if (key.endsWith(".") == false) { throw new IllegalArgumentException("key must end with a '.'"); } } @Override public boolean match(String toTest) { return Regex.simpleMatch(key + "*", toTest); } } /** * List settings key * * @opensearch.internal */ public static final class ListKey extends SimpleKey { private final Pattern pattern; public ListKey(String key) { super(key); this.pattern = Pattern.compile(Pattern.quote(key) + "(\\.\\d+)?"); } @Override public boolean match(String toTest) { return pattern.matcher(toTest).matches(); } } /** * A key that allows for static pre and suffix. This is used for settings * that have dynamic namespaces like for different accounts etc. * * @opensearch.api */ @PublicApi(since = "1.0.0") public static final class AffixKey implements Key { private final Pattern pattern; private final String prefix; private final String suffix; AffixKey(String prefix) { this(prefix, null); } AffixKey(String prefix, String suffix) { assert prefix != null || suffix != null : "Either prefix or suffix must be non-null"; this.prefix = prefix; if (prefix != null && prefix.endsWith(".") == false) { throw new IllegalArgumentException("prefix must end with a '.'"); } this.suffix = suffix; if (suffix == null) { pattern = Pattern.compile("(" + Pattern.quote(prefix) + "((?:[-\\w]+[.])*[-\\w]+$))"); } else if (prefix == null) { pattern = Pattern.compile("((?:[-\\w]+[.])*[-\\w]+\\." + Pattern.quote(suffix) + ")"); } else { // the last part of this regexp is to support both list and group keys pattern = Pattern.compile("(" + Pattern.quote(prefix) + "([-\\w]+)\\." + Pattern.quote(suffix) + ")(?:\\..*)?"); } } @Override public boolean match(String key) { return pattern.matcher(key).matches(); } /** * Returns a string representation of the concrete setting key */ String getConcreteString(String key) { Matcher matcher = pattern.matcher(key); if (matcher.matches() == false) { throw new IllegalStateException("can't get concrete string for key " + key + " key doesn't match"); } return matcher.group(1); } /** * Returns a string representation of the concrete setting key */ String getNamespace(String key) { Matcher matcher = pattern.matcher(key); if (matcher.matches() == false) { throw new IllegalStateException("can't get concrete string for key " + key + " key doesn't match"); } return matcher.group(2); } public SimpleKey toConcreteKey(String missingPart) { StringBuilder key = new StringBuilder(); if (prefix != null) { key.append(prefix); } key.append(missingPart); if (suffix != null) { key.append("."); key.append(suffix); } return new SimpleKey(key.toString()); } @Override public String toString() { StringBuilder sb = new StringBuilder(); if (prefix != null) { sb.append(prefix); } if (suffix != null) { sb.append('*'); sb.append('.'); sb.append(suffix); } return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AffixKey that = (AffixKey) o; return Objects.equals(prefix, that.prefix) && Objects.equals(suffix, that.suffix); } @Override public int hashCode() { return Objects.hash(prefix, suffix); } } }