com.couchbase.connect.kafka.util.config.KafkaConfigProxyFactory Maven / Gradle / Ivy
Show all versions of kafka-connect-couchbase Show documentation
/*
* Copyright 2020 Couchbase, Inc.
*
* Licensed 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 com.couchbase.connect.kafka.util.config;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.connect.kafka.util.config.annotation.Default;
import com.couchbase.connect.kafka.util.config.annotation.Dependents;
import com.couchbase.connect.kafka.util.config.annotation.DisplayName;
import com.couchbase.connect.kafka.util.config.annotation.EnvironmentVariable;
import com.couchbase.connect.kafka.util.config.annotation.Group;
import com.couchbase.connect.kafka.util.config.annotation.Importance;
import com.couchbase.connect.kafka.util.config.annotation.Width;
import com.github.therapi.runtimejavadoc.ClassJavadoc;
import com.github.therapi.runtimejavadoc.MethodJavadoc;
import com.github.therapi.runtimejavadoc.OtherJavadoc;
import com.github.therapi.runtimejavadoc.RuntimeJavadoc;
import org.apache.kafka.common.config.AbstractConfig;
import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.common.config.types.Password;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import static com.couchbase.connect.kafka.util.config.HtmlRenderer.htmlToPlaintext;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
/**
* Given a config interface, generates a matching Kafka ConfigDef.
*
* Given a config interface and a set of config properties, returns an
* implementation of the interface that can be used to access the
* config properties in a type-safe way.
*
* A "config interface" is any interface containing only zero-arg methods
* whose return type is one of:
*
* - String
*
- boolean
*
- int
*
- short
*
- long
*
- double
*
- Class
*
- List<String>
*
- {@link Password}
*
- {@link Duration}
*
- {@link DataSize}
*
- any enum
*
* Support for additional types can be added by calling {@link #register(Class, CustomTypeHandler)}.
*
* Each interface method corresponds to a Kafka config key. The return type of the method
* determines the type of the config key. Other config key attributes are inferred
* from the method, or can be made explicit by annotating the method
* with one of the annotations in {@link com.couchbase.connect.kafka.util.config.annotation}.
*/
public class KafkaConfigProxyFactory {
private static final Logger log = LoggerFactory.getLogger(KafkaConfigProxyFactory.class);
protected final String prefix;
protected final Map, CustomTypeHandler>> customTypeMap = new HashMap<>();
protected final Map, ConfigDef.Type> javaClassToKafkaType = new HashMap<>();
// visible for testing
Function environmentVariableAccessor = System::getenv;
public interface CustomTypeHandler {
T valueOf(String value);
default ConfigDef.Validator validator() {
return null;
}
default ConfigDef.Recommender recommender() {
return null;
}
}
/**
* @param prefix The string to prepend to all generated config property names.
*/
public KafkaConfigProxyFactory(String prefix) {
// make sure prefix is either empty, or ends with dot.
this.prefix = prefix.isEmpty()
? ""
: (prefix.endsWith(".") ? prefix : prefix + ".");
initTypeMap();
register(Duration.class, new CustomTypeHandler() {
@Override
public Duration valueOf(String value) {
return DurationParser.parseDuration(value);
}
@Override
public ConfigDef.Validator validator() {
return new DurationValidator();
}
});
register(DataSize.class, new CustomTypeHandler() {
@Override
public DataSize valueOf(String value) {
return DataSizeParser.parseDataSize(value);
}
@Override
public ConfigDef.Validator validator() {
return new DataSizeValidator();
}
});
}
public KafkaConfigProxyFactory register(Class customType, CustomTypeHandler handler) {
customTypeMap.put(customType, handler);
javaClassToKafkaType.put(customType, ConfigDef.Type.STRING);
return this;
}
/**
* Returns a Kafka ConfigDef whose config keys match the methods of the
* given interface.
*/
public ConfigDef define(Class configInterface) {
return define(configInterface, new ConfigDef());
}
/**
* Returns the given Kafka ConfigDef augmented with config keys from
* the given interface.
*/
public ConfigDef define(Class configInterface, ConfigDef def) {
for (Method method : configInterface.getMethods()) {
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
validateReturnType(method);
def.define(new ConfigDef.ConfigKey(
getConfigKeyName(method),
getKafkaType(method),
getDefaultValue(method),
getValidator(method),
getImportance(method),
getDocumentation(method),
getGroup(method),
getOrderInGroup(method),
getWidth(method),
getDisplayName(method),
getDependents(method),
getRecommender(method),
false));
}
return def;
}
/**
* Returns in implementation of the given config interface
* backed by the given properties.
*
* Logs the config.
*/
public T newProxy(Class configInterface, Map properties) {
return newProxy(configInterface, properties, true);
}
/**
* Returns in implementation of the given config interface
* backed by the given properties.
*
* @param doLog whether to log the config.
*/
public T newProxy(Class configInterface, Map properties, boolean doLog) {
ConfigDef configDef = define(configInterface, new ConfigDef());
ConcreteKafkaConfig kafkaConfig = new ConcreteKafkaConfig(configDef, properties, doLog);
return configInterface.cast(
Proxy.newProxyInstance(
configInterface.getClassLoader(),
new Class[]{configInterface},
new AbstractInvocationHandler(configInterface.getName()) {
@Override
protected Object doInvoke(Object proxy, Method method, Object[] args) {
String configKeyName = getConfigKeyName(method);
Object result = getValueFromEnvironmentVariable(configKeyName, method)
.orElse(kafkaConfig.get(configKeyName));
return postProcessValue(method, result);
}
}));
}
/**
* Returns the name of the config key associated with the method invoked
* by the given consumer.
*
* Example usage:
*
* String name = proxyFactory.keyName(MyConfig.class, MyConfig::myProperty);
*
*
* @param configInterface the config interface to inspect
* @param methodInvoker accepts an implementation of the specified interface
* and calls the method whose name you want to know
*/
public String keyName(Class configInterface, Consumer methodInvoker) {
try {
T instance = newProxyForKeyNames(configInterface);
methodInvoker.accept(instance);
throw new IllegalArgumentException("Consumer should have invoked a method of the config interface.");
} catch (KeyNameHolderException e) {
return e.name;
}
}
/**
* Returns an implementation whose methods all throw an exception
* that holds the name of the config key associated with the method.
*/
protected T newProxyForKeyNames(Class configInterface) {
return configInterface.cast(
Proxy.newProxyInstance(
configInterface.getClassLoader(),
new Class[]{configInterface},
new AbstractInvocationHandler(configInterface.getName()) {
@Override
protected Object doInvoke(Object proxy, Method method, Object[] args) {
throw new KeyNameHolderException(getConfigKeyName(method));
}
}));
}
protected static class KeyNameHolderException extends RuntimeException {
private final String name;
public KeyNameHolderException(String name) {
super(name);
this.name = requireNonNull(name);
}
}
protected Object postProcessValue(Method method, Object value) {
Class> javaType = method.getReturnType();
CustomTypeHandler> customTypeHandler = customTypeMap.get(javaType);
if (customTypeHandler != null) {
return customTypeHandler.valueOf((String) value);
}
if (javaType.isEnum()) {
return parseEnum(javaType, (String) value);
}
return value;
}
protected String getEnv(String environmentVariableName) {
return environmentVariableAccessor.apply(environmentVariableName);
}
protected Optional