Skip to content

Instantly share code, notes, and snippets.

@xdcrafts
Last active December 16, 2016 04:01
Show Gist options
  • Save xdcrafts/c17c81bb2351604137d272699a4efae2 to your computer and use it in GitHub Desktop.
Save xdcrafts/c17c81bb2351604137d272699a4efae2 to your computer and use it in GitHub Desktop.
{:actions {:test/first net.thumbtack.flower.spring.TestAction/staticApply
:test/second {:method testAction/apply
:middleware [counter debug]}}
:flows {:test-flows/simple-flow {:factory basicSyncFlowFactory
:flow [:test/first :test/second]}
:test-flows/complex-flow {:factory basicAsyncFlowFactory
:flow [:test-flows/simple-flow :test/second]}}}
package net.thumtack.flower.spring;
import net.thumbtack.flower.AsFunction;
import net.thumbtack.flower.Namespaced;
import net.thumbtack.flower.Action;
import net.thumbtack.flower.Middleware;
import net.thumbtack.flower.utils.MapApi;
import net.thumbtack.flower.impl.DefaultAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.context.ApplicationContext;
import us.bpsm.edn.Keyword;
import us.bpsm.edn.Symbol;
import us.bpsm.edn.parser.Parseable;
import us.bpsm.edn.parser.Parser;
import us.bpsm.edn.parser.Parsers;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Flower spring-based configurer.
*/
@SuppressWarnings("unchecked")
public class FlowerConfigurator implements BeanDefinitionRegistryPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(FlowerConfigurator.class);
public static final String FLOWER_CONFIGURATION = "flower.configuration";
public static final String FLOWER_CONFIGURATION_DEFAULT = "/flower.edn";
public static final String ACTIONS_CONFIGURATION_KEY = "actions";
public static final String FLOWS_CONFIGURATION_KEY = "flows";
private final Map configuration;
private final ApplicationContext applicationContext;
public FlowerConfigurator(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
try {
final String configurationPath = System.getProperty(FLOWER_CONFIGURATION);
final Path path = configurationPath != null
? Paths.get(configurationPath)
: Paths.get(getClass().getResource(FLOWER_CONFIGURATION_DEFAULT).toURI());
final String configurationAsString = new String(Files.readAllBytes(path), "UTF-8");
final Parseable parseable = Parsers.newParseable(configurationAsString);
final Parser parser = Parsers.newParser(Parsers.defaultConfiguration());
this.configuration = (Map) parser.nextValue(parseable);
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
}
private Map getConfiguration(String keyword) {
return MapApi.getMap(this.configuration, Keyword.newKeyword(keyword))
.orElseThrow(() -> new IllegalStateException(":" + keyword + " configuration is required."));
}
private static Map safeVirtualInvoke(MethodHandle methodHandle, Object bean, Map context) {
try {
return (Map) methodHandle.invoke(bean, context);
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
}
private static Map safeStaticInvoke(MethodHandle methodHandle, Map context) {
try {
return (Map) methodHandle.invoke(context);
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
}
private Function<Map, Map> buildActionFunction(
String classOrBeanName, List<String> middlewareNames, String methodName
) {
try {
final Function<Function<Map, Map>, Function<Map, Map>> middleware = middlewareNames
.stream()
.map(name -> this.applicationContext.getBean(name, Middleware.class))
.map(AsFunction::asFunction)
.reduce(Function.identity(), Function::andThen);
final Object bean = this.applicationContext.containsBean(classOrBeanName)
? this.applicationContext.getBean(classOrBeanName)
: null;
final boolean isVirtual = bean != null;
final Class clazz = isVirtual ? bean.getClass() : Class.forName(classOrBeanName);
final MethodHandles.Lookup lookup = MethodHandles.lookup();
final MethodType methodType = MethodType.methodType(Map.class, Map.class);
final MethodHandle methodHandle = isVirtual
? lookup.findVirtual(clazz, methodName, methodType)
: lookup.findStatic(clazz, methodName, methodType);
final Function<Map, Map> pureFunction = isVirtual
? ctx -> safeVirtualInvoke(methodHandle, bean, ctx)
: ctx -> safeStaticInvoke(methodHandle, ctx);
return middleware.apply(pureFunction);
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
}
private void processActions(
Map<Object, Object> actionsConfiguration,
BeanDefinitionRegistry registry
) {
for (Map.Entry entry : actionsConfiguration.entrySet()) {
final Keyword actionNameKeyword = (Keyword) entry.getKey();
final String actionNamespace = actionNameKeyword.getPrefix();
final String actionName = actionNameKeyword.getName();
final String beanName = Namespaced.fullName(actionNamespace, actionName);
final Object symbolOrMap = entry.getValue();
final Symbol methodSymbol;
final List<Symbol> middleware;
if (symbolOrMap instanceof Symbol) {
methodSymbol = (Symbol) entry.getValue();
middleware = new ArrayList<>();
} else if (symbolOrMap instanceof Map) {
final Map actionConfiguration = (Map) symbolOrMap;
methodSymbol = (Symbol) actionConfiguration.get(Keyword.newKeyword("method"));
middleware = actionConfiguration.containsKey(Keyword.newKeyword("middleware"))
? (List<Symbol>) actionConfiguration.get(Keyword.newKeyword("middleware"))
: new ArrayList<>();
} else {
throw new IllegalStateException("Symbol or map expected: " + symbolOrMap);
}
final String classOrBeanName = methodSymbol.getPrefix();
final String methodName = methodSymbol.getName();
final List<String> middlewareNames = middleware
.stream()
.map(kwd -> Namespaced.fullName(kwd.getPrefix(), kwd.getName()))
.collect(Collectors.toList());
final ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(actionNamespace);
constructorArgumentValues.addGenericArgumentValue(actionName);
constructorArgumentValues.addGenericArgumentValue(
buildActionFunction(classOrBeanName, middlewareNames, methodName)
);
final GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DefaultAction.class);
beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
registry.registerBeanDefinition(beanName, beanDefinition);
}
}
private void processFlows(Map<Object, Object> flowsConfiguration, BeanDefinitionRegistry registry) {
for (Map.Entry entry : flowsConfiguration.entrySet()) {
try {
final Keyword flowNameKeyword = (Keyword) entry.getKey();
final String flowNamespace = flowNameKeyword.getPrefix();
final String flowName = flowNameKeyword.getName();
final String beanName = Namespaced.fullName(flowNamespace, flowName);
final Map<Object, Object> flowConfiguration = (Map<Object, Object>) entry.getValue();
final Symbol factoryBeanSymbol = (Symbol) flowConfiguration.get(Keyword.newKeyword("factory"));
final String factoryBeanName = Namespaced.fullName(
factoryBeanSymbol.getPrefix(), factoryBeanSymbol.getName()
);
final List<Keyword> flowItemNames = (List<Keyword>) flowConfiguration.get(Keyword.newKeyword("flow"));
final ManagedList beanReferences = flowItemNames
.stream()
.map(keyword -> Namespaced.fullName(keyword.getPrefix(), keyword.getName()))
.map(RuntimeBeanReference::new)
.collect(Collectors.toCollection(ManagedList::new));
final ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(flowNamespace);
constructorArgumentValues.addGenericArgumentValue(flowName);
constructorArgumentValues.addGenericArgumentValue(beanReferences);
final GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setFactoryBeanName(factoryBeanName);
beanDefinition.setFactoryMethodName("build");
beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
registry.registerBeanDefinition(beanName, beanDefinition);
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
}
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
processActions(getConfiguration(ACTIONS_CONFIGURATION_KEY), registry);
processFlows(getConfiguration(FLOWS_CONFIGURATION_KEY), registry);
LOGGER.info("Configuration: {}", this.configuration);
LOGGER.info("Actions:");
this.applicationContext.getBeansOfType(Action.class)
.values()
.stream()
.collect(Collectors.groupingBy(Object::getClass))
.forEach((actionClass, actionList) ->
LOGGER.info(
"{}: {}",
actionClass.getSimpleName(),
actionList.stream().map(Namespaced::getFullName).collect(Collectors.joining(", "))
)
);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// Nothing
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment