Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable structured types as config values #77

Merged
merged 19 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,22 @@ And the _@ConfigurationServiceScan_ annotation hints the Baigan registrar to loo
@BaiganConfig
public interface ExpressFeature {

public Boolean enabled();
Boolean enabled();

public String serviceUrl();
String serviceUrl();

SomeStructuredConfigClass complexConfiguration();

List<String> configList();

Map<UUID, List<SomeConfigObject>> nestedGenericConfiguration();
}
```

The individual methods may have arbitrary classes as return types, in particular complex structured types are supported, including Generics.

**Note**: Primitives are not supported as return types as they cannot be null and therefore cannot express a missing configuration value.

The above example code enables the application to inject _ExpressFeature_ spring bean into any other Spring bean:

```Java
Expand All @@ -84,13 +93,20 @@ This is done using the Spring Bean of type `RepositoryFactory`, which allows cre
types. The following example shows how to configure a filesystem based repository.

```Java
@Bean
public ConfigurationRepository configurationRepository(RepositoryFactory factory) {
return factory.fileSystemConfigurationRepository()
.fileName("configs.json");
@Configuration
public class ApplicationConfiguration {

@Bean
public ConfigurationRepository configurationRepository(RepositoryFactory factory){
return factory.fileSystemConfigurationRepository()
.fileName("configs.json");
}
}
```

Check the documentation of the builders for details on how to configure the repositories. In particular, all
repositories can be configured with a Jackson `ObjectMapper` used to deserialize the configuration.

### Creating configurations
Baigan configurations follow a specific schema and can be stored on any of the supported repositories.

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/zalando/baigan/model/In.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package org.zalando.baigan.model;

import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down Expand Up @@ -45,4 +47,23 @@ public boolean eval(final String forValue) {
return inValue.contains(forValue);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
In in = (In) o;
return Objects.equals(inValue, in.inValue);
}

@Override
public int hashCode() {
return Objects.hash(inValue);
}

@Override
public String toString() {
return new StringJoiner(", ", In.class.getSimpleName() + "[", "]")
.add("inValue=" + inValue)
.toString();
}
}
23 changes: 23 additions & 0 deletions src/main/java/org/zalando/baigan/proxy/BaiganConfigClasses.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.zalando.baigan.proxy;

import java.lang.reflect.Type;
import java.util.Map;

public class BaiganConfigClasses {
private Map<String, Type> configTypesByKey;

public BaiganConfigClasses() {}

public void setConfigTypesByKey(Map<String, Type> configTypesByKey) {
configTypesByKey.forEach((key, value) -> {
if (value.getClass().isPrimitive()) {
throw new IllegalArgumentException("Config " + key + " has an illegal return type " + value + ". Primitives are not supported as return type.");
}
});
this.configTypesByKey = configTypesByKey;
}

public Map<String, Type> getConfigTypesByKey() {
return configTypesByKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,25 @@
import org.zalando.baigan.annotation.BaiganConfig;
import org.zalando.baigan.annotation.ConfigurationServiceScan;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toMap;
import static org.zalando.baigan.proxy.ProxyUtils.createKey;

/**
* ImportBeanDefinitionRegistrar implementation that finds the
* {@link ConfigurationServiceScan} annotations, delegates the scanning of
* packages and proxy bean creation further down to the corresponding
* implementations.
*
* @see ConfigurationServiceBeanFactory
*
* @author mchand
*
* @see ConfigurationServiceBeanFactory
*/
public class ConfigurationBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar {
Expand Down Expand Up @@ -83,38 +87,49 @@ private void createAndRegisterBeanDefinitions(final Set<String> packages,
final ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateInterfaceProvider();
scanner.addIncludeFilter(new AnnotationTypeFilter(BaiganConfig.class));

List<Class<?>> baiganConfigClasses = new ArrayList<>();

for (final String singlePackage : packages) {
final Set<BeanDefinition> candidates = scanner.findCandidateComponents(singlePackage);
for (BeanDefinition definition : candidates) {
if (definition instanceof GenericBeanDefinition) {
final GenericBeanDefinition genericDefinition = (GenericBeanDefinition) definition;
registerAsBean(registry, genericDefinition);
final Class<?> baiganConfigClass = registerAsBean(registry, genericDefinition);
baiganConfigClasses.add(baiganConfigClass);
} else {
throw new IllegalStateException(
String.format(
"Unable to read required metadata of configuration candidate [%s]",
definition
)
String.format(
"Unable to read required metadata of configuration candidate [%s]",
definition
)
);
}
}
}
Map<String, Type> configTypesByKey = baiganConfigClasses.stream().flatMap(clazz ->
Arrays.stream(clazz.getMethods()).map(method -> new ConfigType(createKey(clazz, method), method.getGenericReturnType()))
).collect(toMap(c -> c.key, c -> c.type));
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BaiganConfigClasses.class);
beanDefinition.getPropertyValues().add("configTypesByKey", configTypesByKey);
registry.registerBeanDefinition("baiganConfigClasses", beanDefinition);
}

private void registerAsBean(final BeanDefinitionRegistry registry, final GenericBeanDefinition genericDefinition) {
private Class<?> registerAsBean(final BeanDefinitionRegistry registry, final GenericBeanDefinition genericDefinition) {
try {
final Class<?> interfaceToImplement = genericDefinition.resolveBeanClass(
registry.getClass().getClassLoader()
registry.getClass().getClassLoader()
);
registerAsBean(registry, interfaceToImplement);
return interfaceToImplement;
} catch (final ClassNotFoundException e) {
throw new IllegalStateException("Unable to register annotated interface as configuration bean", e);
}
}

private void registerAsBean(final BeanDefinitionRegistry registry, final Class<?> interfaceToImplement) {
final BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
ConfigurationServiceBeanFactory.class
ConfigurationServiceBeanFactory.class
);
builder.addPropertyValue("candidateInterface", interfaceToImplement);

Expand All @@ -123,7 +138,7 @@ private void registerAsBean(final BeanDefinitionRegistry registry, final Class<?
}

private static class ClassPathScanningCandidateInterfaceProvider
extends ClassPathScanningCandidateComponentProvider {
extends ClassPathScanningCandidateComponentProvider {

public ClassPathScanningCandidateInterfaceProvider() {
super(false);
Expand All @@ -136,4 +151,13 @@ protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
}
}

private static class ConfigType {
private final String key;
private final Type type;

public ConfigType(String key, Type type) {
this.key = key;
this.type = type;
}
}
}
12 changes: 11 additions & 1 deletion src/main/java/org/zalando/baigan/proxy/ProxyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;

import java.lang.reflect.Method;

/**
* The class to contain utility methods used in proxying configuration beans.
*
Expand All @@ -28,7 +30,15 @@
public class ProxyUtils {
private static final String NAMESPACE_SEPARATOR = ".";

public static String dottify(final String text) {
public static String createKey(final Class<?> clazz, Method method) {
final String methodName = method.getName();
final String nameSpace = clazz.getSimpleName();

return ProxyUtils.dottify(nameSpace) + "."
+ ProxyUtils.dottify(methodName);
}

private static String dottify(final String text) {

if (Strings.isNullOrEmpty(text)) {
return NAMESPACE_SEPARATOR;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.zalando.baigan.proxy.handler;

import com.google.common.base.Supplier;
import com.google.common.primitives.Primitives;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
Expand All @@ -12,19 +11,17 @@
import org.zalando.baigan.context.ContextProviderRetriever;
import org.zalando.baigan.model.Configuration;
import org.zalando.baigan.context.ContextProvider;
import org.zalando.baigan.proxy.ProxyUtils;
import org.zalando.baigan.repository.ConfigurationRepository;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Suppliers.memoize;
import static org.zalando.baigan.proxy.ProxyUtils.createKey;

/**
* This class provides a concrete implementation for the Method invocation
Expand Down Expand Up @@ -58,58 +55,25 @@ public void setBeanFactory(final BeanFactory beanFactory) throws BeansException
}

@Override
protected Object handleInvocation(Object proxy, Method method,
Object[] args) throws Throwable {
final String methodName = method.getName();
final String nameSpace = getNamespace(proxy);

final String key = ProxyUtils.dottify(nameSpace) + "."
+ ProxyUtils.dottify(methodName);
protected Object handleInvocation(Object proxy, Method method, Object[] args) {
final String key = createKey(getClass(proxy), method);
final Object result = getConfig(key);
if (result == null) {
LOG.warn("Configuration not found for key: {}", key);
return null;
}

final Class<?> declaredReturnType = method.getReturnType();

try {

Constructor<?> constructor;
if (declaredReturnType.isInstance(result)) {
return result;
} else if (declaredReturnType.isPrimitive()) {
final Class<?> resultClass = result.getClass();
constructor = Primitives.wrap(declaredReturnType)
.getDeclaredConstructor(resultClass);
} else if (declaredReturnType.isEnum()) {
for (Object t : Arrays
.asList(declaredReturnType.getEnumConstants())) {
if (result.toString().equalsIgnoreCase(t.toString())) {
return t;
}
}
LOG.warn("Unable to map [{}] to enum type [{}].", result, declaredReturnType.getName());
return null;
} else {
constructor = declaredReturnType
.getDeclaredConstructor(result.getClass());
}
return constructor.newInstance(result);
lukas-c-wilhelm marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception exception) {
LOG.warn(
"Wrong or Incompatible configuration. Cannot find a constructor to create object of type "
+ declaredReturnType
+ " for value of the configuration key " + key,
exception);
if (!method.getReturnType().isInstance(result)) {
LOG.error("Configuration repository returned object of wrong type. Expected: {}, actual: {}", method.getReturnType(), result.getClass());
return null;
}
return null;

return result;
}

private String getNamespace(final Object proxy) {
private Class<?> getClass(final Object proxy) {
final Class<?>[] interfaces = proxy.getClass().getInterfaces();
checkState(interfaces.length == 1, "Expected exactly one interface on proxy object.");
return interfaces[0].getSimpleName();
return interfaces[0];
}

private Object getConfig(final String key) {
Expand Down

This file was deleted.

Loading