/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.
*/
package org.apache.kafka.common.config;
import org.apache.kafka.common.Configurable;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.config.types.Password;
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* A convenient base class for configurations to extend.
* <p>
* This class holds both the original configuration that was provided as well as the parsed
*/
public class AbstractConfig {
private final Logger log = LoggerFactory.getLogger(getClass());
/* configs for which values have been requested, used to detect unused configs */
private final Set<String> used;
/* the original values passed in by the user */
private final Map<String, ?> originals;
/* the parsed values */
private final Map<String, Object> values;
private final ConfigDef definition;
@SuppressWarnings("unchecked")
public AbstractConfig(ConfigDef definition, Map<?, ?> originals, boolean doLog) {
/* check that all the keys are really strings */
for (Map.Entry<?, ?> entry : originals.entrySet())
if (!(entry.getKey() instanceof String))
throw new ConfigException(entry.getKey().toString(), entry.getValue(), "Key must be a string.");
this.originals = (Map<String, ?>) originals;
this.values = definition.parse(this.originals);
Map<String, Object> configUpdates = postProcessParsedConfig(Collections.unmodifiableMap(this.values));
for (Map.Entry<String, Object> update : configUpdates.entrySet()) {
this.values.put(update.getKey(), update.getValue());
}
definition.parse(this.values);
this.used = Collections.synchronizedSet(new HashSet<String>());
this.definition = definition;
if (doLog)
logAll();
}
public AbstractConfig(ConfigDef definition, Map<?, ?> originals) {
this(definition, originals, true);
}
/**
* Called directly after user configs got parsed (and thus default values got set).
* This allows to change default values for "secondary defaults" if required.
*
* @param parsedValues unmodifiable map of current configuration
* @return a map of updates that should be applied to the configuration (will be validated to prevent bad updates)
*/
protected Map<String, Object> postProcessParsedConfig(Map<String, Object> parsedValues) {
return Collections.emptyMap();
}
protected Object get(String key) {
if (!values.containsKey(key))
throw new ConfigException(String.format("Unknown configuration '%s'", key));
used.add(key);
return values.get(key);
}
public void ignore(String key) {
used.add(key);
}
public Short getShort(String key) {
return (Short) get(key);
}
public Integer getInt(String key) {
return (Integer) get(key);
}
public Long getLong(String key) {
return (Long) get(key);
}
public Double getDouble(String key) {
return (Double) get(key);
}
@SuppressWarnings("unchecked")
public List<String> getList(String key) {
return (List<String>) get(key);
}
public Boolean getBoolean(String key) {
return (Boolean) get(key);
}
public String getString(String key) {
return (String) get(key);
}
public ConfigDef.Type typeOf(String key) {
ConfigDef.ConfigKey configKey = definition.configKeys().get(key);
if (configKey == null)
return null;
return configKey.type;
}
public Password getPassword(String key) {
return (Password) get(key);
}
public Class<?> getClass(String key) {
return (Class<?>) get(key);
}
public Set<String> unused() {
Set<String> keys = new HashSet<>(originals.keySet());
keys.removeAll(used);
return keys;
}
public Map<String, Object> originals() {
Map<String, Object> copy = new RecordingMap<>();
copy.putAll(originals);
return copy;
}
/**
* Get all the original settings, ensuring that all values are of type String.
* @return the original settings
* @throws ClassCastException if any of the values are not strings
*/
public Map<String, String> originalsStrings() {
Map<String, String> copy = new RecordingMap<>();
for (Map.Entry<String, ?> entry : originals.entrySet()) {
if (!(entry.getValue() instanceof String))
throw new ClassCastException("Non-string value found in original settings for key " + entry.getKey() +
": " + (entry.getValue() == null ? null : entry.getValue().getClass().getName()));
copy.put(entry.getKey(), (String) entry.getValue());
}
return copy;
}
/**
* Gets all original settings with the given prefix, stripping the prefix before adding it to the output.
*
* @param prefix the prefix to use as a filter
* @return a Map containing the settings with the prefix
*/
public Map<String, Object> originalsWithPrefix(String prefix) {
Map<String, Object> result = new RecordingMap<>(prefix, false);
for (Map.Entry<String, ?> entry : originals.entrySet()) {
if (entry.getKey().startsWith(prefix) && entry.getKey().length() > prefix.length())
result.put(entry.getKey().substring(prefix.length()), entry.getValue());
}
return result;
}
/**
* Put all keys that do not start with {@code prefix} and their parsed values in the result map and then
* put all the remaining keys with the prefix stripped and their parsed values in the result map.
*
* This is useful if one wants to allow prefixed configs to override default ones.
*/
public Map<String, Object> valuesWithPrefixOverride(String prefix) {
Map<String, Object> result = new RecordingMap<>(values(), prefix, true);
for (Map.Entry<String, ?> entry : originals.entrySet()) {
if (entry.getKey().startsWith(prefix) && entry.getKey().length() > prefix.length()) {
String keyWithNoPrefix = entry.getKey().substring(prefix.length());
ConfigDef.ConfigKey configKey = definition.configKeys().get(keyWithNoPrefix);
if (configKey != null)
result.put(keyWithNoPrefix, definition.parseValue(configKey, entry.getValue(), true));
}
}
return result;
}
public Map<String, ?> values() {
return new RecordingMap<>(values);
}
private void logAll() {
StringBuilder b = new StringBuilder();
b.append(getClass().getSimpleName());
b.append(" values: ");
b.append(Utils.NL);
for (Map.Entry<String, Object> entry : new TreeMap<>(this.values).entrySet()) {
b.append('\t');
b.append(entry.getKey());
b.append(" = ");
b.append(entry.getValue());
b.append(Utils.NL);
}
log.info(b.toString());
}
/**
* Log warnings for any unused configurations
*/
public void logUnused() {
for (String key : unused())
log.warn("The configuration '{}' was supplied but isn't a known config.", key);
}
/**
* Get a configured instance of the give class specified by the given configuration key. If the object implements
* Configurable configure it using the configuration.
*
* @param key The configuration key for the class
* @param t The interface the class should implement
* @return A configured instance of the class
*/
public <T> T getConfiguredInstance(String key, Class<T> t) {
Class<?> c = getClass(key);
if (c == null)
return null;
Object o = Utils.newInstance(c);
if (!t.isInstance(o))
throw new KafkaException(c.getName() + " is not an instance of " + t.getName());
if (o instanceof Configurable)
((Configurable) o).configure(originals());
return t.cast(o);
}
/**
* Get a list of configured instances of the given class specified by the given configuration key. The configuration
* may specify either null or an empty string to indicate no configured instances. In both cases, this method
* returns an empty list to indicate no configured instances.
* @param key The configuration key for the class
* @param t The interface the class should implement
* @return The list of configured instances
*/
public <T> List<T> getConfiguredInstances(String key, Class<T> t) {
return getConfiguredInstances(key, t, Collections.EMPTY_MAP);
}
/**
* Get a list of configured instances of the given class specified by the given configuration key. The configuration
* may specify either null or an empty string to indicate no configured instances. In both cases, this method
* returns an empty list to indicate no configured instances.
* @param key The configuration key for the class
* @param t The interface the class should implement
* @param configOverrides Configuration overrides to use.
* @return The list of configured instances
*/
public <T> List<T> getConfiguredInstances(String key, Class<T> t, Map<String, Object> configOverrides) {
List<String> klasses = getList(key);
List<T> objects = new ArrayList<T>();
if (klasses == null)
return objects;
Map<String, Object> configPairs = originals();
configPairs.putAll(configOverrides);
for (Object klass : klasses) {
Object o;
if (klass instanceof String) {
try {
o = Utils.newInstance((String) klass, t);
} catch (ClassNotFoundException e) {
throw new KafkaException(klass + " ClassNotFoundException exception occurred", e);
}
} else if (klass instanceof Class<?>) {
o = Utils.newInstance((Class<?>) klass);
} else
throw new KafkaException("List contains element of type " + klass.getClass().getName() + ", expected String or Class");
if (!t.isInstance(o))
throw new KafkaException(klass + " is not an instance of " + t.getName());
if (o instanceof Configurable)
((Configurable) o).configure(configPairs);
objects.add(t.cast(o));
}
return objects;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AbstractConfig that = (AbstractConfig) o;
return originals.equals(that.originals);
}
@Override
public int hashCode() {
return originals.hashCode();
}
/**
* Marks keys retrieved via `get` as used. This is needed because `Configurable.configure` takes a `Map` instead
* of an `AbstractConfig` and we can't change that without breaking public API like `Partitioner`.
*/
private class RecordingMap<V> extends HashMap<String, V> {
private final String prefix;
private final boolean withIgnoreFallback;
RecordingMap() {
this("", false);
}
RecordingMap(String prefix, boolean withIgnoreFallback) {
this.prefix = prefix;
this.withIgnoreFallback = withIgnoreFallback;
}
RecordingMap(Map<String, ? extends V> m) {
this(m, "", false);
}
RecordingMap(Map<String, ? extends V> m, String prefix, boolean withIgnoreFallback) {
super(m);
this.prefix = prefix;
this.withIgnoreFallback = withIgnoreFallback;
}
@Override
public V get(Object key) {
if (key instanceof String) {
String stringKey = (String) key;
String keyWithPrefix;
if (prefix.isEmpty()) {
keyWithPrefix = stringKey;
} else {
keyWithPrefix = prefix + stringKey;
}
ignore(keyWithPrefix);
if (withIgnoreFallback)
ignore(stringKey);
}
return super.get(key);
}
}
}