Last active
December 16, 2016 04:01
-
-
Save xdcrafts/c17c81bb2351604137d272699a4efae2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{: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]}}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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