/* * Copyright 2015-2016 the original author or authors. * * 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 org.springframework.cloud.stream.aggregate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; import org.springframework.boot.bind.PropertySourcesPropertyValues; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.bind.RelaxedNames; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.binding.BindableProxyFactory; import org.springframework.cloud.stream.config.ChannelBindingAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.env.PropertySources; import org.springframework.integration.monitor.IntegrationMBeanExporter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Application builder for {@link AggregateApplication}. * * @author Dave Syer * @author Ilayaperumal Gopinathan * @author Marius Bogoevici * @author Venil Noronha */ @EnableBinding public class AggregateApplicationBuilder implements AggregateApplication, ApplicationContextAware, SmartInitializingSingleton { private static final String CHILD_CONTEXT_SUFFIX = ".spring.cloud.stream.context"; private SourceConfigurer sourceConfigurer; private SinkConfigurer sinkConfigurer; private List<ProcessorConfigurer> processorConfigurers = new ArrayList<>(); private AggregateApplicationBuilder applicationBuilder = this; private ConfigurableApplicationContext parentContext; private List<Object> parentSources = new ArrayList<>(); private List<String> parentArgs = new ArrayList<>(); private boolean headless = true; private boolean webEnvironment = true; public AggregateApplicationBuilder(String... args) { this(new Object[]{ParentConfiguration.class}, args); } public AggregateApplicationBuilder(Object source, String... args) { this(new Object[]{source}, args); } public AggregateApplicationBuilder(Object[] sources, String[] args) { addParentSources(sources); this.parentArgs.addAll(Arrays.asList(args)); } /** * Adding auto configuration classes to parent sources excluding the configuration * classes related to binder/binding. */ private void addParentSources(Object[] sources) { if (!this.parentSources.contains(ParentConfiguration.class)) { this.parentSources.add(ParentConfiguration.class); } this.parentSources.addAll(Arrays.asList(sources)); } public AggregateApplicationBuilder parent(Object source, String... args) { return parent(new Object[]{source}, args); } public AggregateApplicationBuilder parent(Object[] sources, String[] args) { addParentSources(sources); this.parentArgs.addAll(Arrays.asList(args)); return this; } /** * Flag to explicitly request a web or non-web environment. * * @param webEnvironment true if the application has a web environment * @return the AggregateApplicationBuilder being constructed * @see SpringApplicationBuilder#web(boolean) */ public AggregateApplicationBuilder web(boolean webEnvironment) { this.webEnvironment = webEnvironment; return this; } /** * Configures the headless attribute of the build application. * * @param headless true if the application is headless * @return the AggregateApplicationBuilder being constructed * @see SpringApplicationBuilder#headless(boolean) */ public AggregateApplicationBuilder headless(boolean headless) { this.headless = headless; return this; } @Override public void afterSingletonsInstantiated() { this.run(); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.parentContext = (ConfigurableApplicationContext) applicationContext; } @Override public <T> T getBinding(Class<T> bindableType, String namespace) { if (parentContext == null) { throw new IllegalStateException("The aggregate application has not been started yet"); } try { ChildContextHolder contextHolder = parentContext.getBean(namespace + CHILD_CONTEXT_SUFFIX, ChildContextHolder.class); return contextHolder.getChildContext().getBean(bindableType); } catch (BeansException e) { throw new IllegalStateException("Binding not found for '" + bindableType.getName() + "' into namespace " + namespace); } } public SourceConfigurer from(Class<?> app) { SourceConfigurer sourceConfigurer = new SourceConfigurer(app); this.sourceConfigurer = sourceConfigurer; return sourceConfigurer; } public ConfigurableApplicationContext run(String... parentArgs) { this.parentArgs.addAll(Arrays.asList(parentArgs)); List<AppConfigurer<?>> apps = new ArrayList<>(); if (this.sourceConfigurer != null) { apps.add(sourceConfigurer); } if (!processorConfigurers.isEmpty()) { for (ProcessorConfigurer processorConfigurer : processorConfigurers) { apps.add(processorConfigurer); } } if (this.sinkConfigurer != null) { apps.add(sinkConfigurer); } LinkedHashMap<Class<?>, String> appsToEmbed = new LinkedHashMap<>(); LinkedHashMap<AppConfigurer, String> appConfigurers = new LinkedHashMap<>(); for (int i = 0; i < apps.size(); i++) { AppConfigurer<?> appConfigurer = apps.get(i); Class<?> appToEmbed = appConfigurer.getApp(); // Always update namespace before preparing SharedChannelRegistry if (appConfigurer.namespace == null) { appConfigurer.namespace = AggregateApplicationUtils.getDefaultNamespace(appConfigurer.getApp().getName(), i); } appsToEmbed.put(appToEmbed, appConfigurer.namespace); appConfigurers.put(appConfigurer, appConfigurer.namespace); } if (this.parentContext == null) { if (Boolean.TRUE.equals(this.webEnvironment)) { this.addParentSources(new Object[]{EmbeddedServletContainerAutoConfiguration.class}); } this.parentContext = AggregateApplicationUtils.createParentContext(this.parentSources.toArray(new Object[0]), this.parentArgs.toArray(new String[0]), selfContained(), this.webEnvironment, this.headless); } else { if (BeanFactoryUtils.beansOfTypeIncludingAncestors(this.parentContext, SharedBindingTargetRegistry.class) .size() == 0) { SharedBindingTargetRegistry sharedBindingTargetRegistry = new SharedBindingTargetRegistry(); this.parentContext.getBeanFactory().registerSingleton("sharedBindingTargetRegistry", sharedBindingTargetRegistry); this.parentContext.getBeanFactory().registerSingleton("sharedChannelRegistry", new SharedChannelRegistry(sharedBindingTargetRegistry)); } } SharedBindingTargetRegistry sharedBindingTargetRegistry = this.parentContext.getBean(SharedBindingTargetRegistry.class); AggregateApplicationUtils.prepareSharedBindingTargetRegistry(sharedBindingTargetRegistry, appsToEmbed); PropertySources propertySources = this.parentContext.getEnvironment() .getPropertySources(); for (Map.Entry<AppConfigurer, String> appConfigurerEntry : appConfigurers.entrySet()) { AppConfigurer appConfigurer = appConfigurerEntry.getKey(); String namespace = appConfigurerEntry.getValue().toLowerCase(); Set<String> argsToUpdate = new LinkedHashSet<>(); Set<String> argKeys = new LinkedHashSet<>(); final HashMap<String, String> target = new HashMap<>(); RelaxedDataBinder relaxedDataBinder = new RelaxedDataBinder(target, namespace); relaxedDataBinder.bind(new PropertySourcesPropertyValues(propertySources)); if (!target.isEmpty()) { for (Map.Entry<String, String> entry : target.entrySet()) { // only update the values with the highest precedence level. if (!relaxedNameKeyExists(entry.getKey(), argKeys)) { String key = entry.getKey(); // in case of environment variables pass the lower-case property // key // as we pass the properties as command line properties if (key.contains("_")) { key = key.replace("_", "-").toLowerCase(); } argKeys.add(key); argsToUpdate.add("--" + key + "=" + entry.getValue()); } } } // Add the args that are set at the application level if they weren't // overridden above from other property sources. if (appConfigurer.getArgs() != null) { for (String arg : appConfigurer.getArgs()) { // use the key part left to the assignment and trimming the prefix // `--` String key = arg.substring(0, arg.indexOf("=")).substring(2); if (!relaxedNameKeyExists(key, argKeys)) { argsToUpdate.add(arg); } } } if (!argsToUpdate.isEmpty()) { appConfigurer.args(argsToUpdate.toArray(new String[0])); } } for (int i = apps.size() - 1; i >= 0; i--) { AppConfigurer<?> appConfigurer = apps.get(i); appConfigurer.embed(); } if (BeanFactoryUtils.beansOfTypeIncludingAncestors(this.parentContext, AggregateApplication.class) .size() == 0) { this.parentContext.getBeanFactory().registerSingleton("aggregateApplicationAccessor", this); } return this.parentContext; } private boolean selfContained() { return (this.sourceConfigurer != null) && (this.sinkConfigurer != null); } private boolean relaxedNameKeyExists(String key, Collection<String> collection) { RelaxedNames relaxedNames = new RelaxedNames(key); for (String name : relaxedNames) { if (collection.contains(name)) { return true; } } return false; } private ChildContextBuilder childContext(Class<?> app, ConfigurableApplicationContext parentContext, String namespace) { return new ChildContextBuilder(AggregateApplicationUtils.embedApp(parentContext, namespace, app)); } public class SourceConfigurer extends AppConfigurer<SourceConfigurer> { public SourceConfigurer(Class<?> app) { this.app = app; sourceConfigurer = this; } public SinkConfigurer to(Class<?> sink) { return new SinkConfigurer(sink); } public ProcessorConfigurer via(Class<?> processor) { return new ProcessorConfigurer(processor); } } public class SinkConfigurer extends AppConfigurer<SinkConfigurer> { public SinkConfigurer(Class<?> app) { this.app = app; sinkConfigurer = this; } } public class ProcessorConfigurer extends AppConfigurer<ProcessorConfigurer> { public ProcessorConfigurer(Class<?> app) { this.app = app; processorConfigurers.add(this); } public SinkConfigurer to(Class<?> sink) { return new SinkConfigurer(sink); } public ProcessorConfigurer via(Class<?> processor) { return new ProcessorConfigurer(processor); } } public abstract class AppConfigurer<T extends AppConfigurer<T>> { Class<?> app; String[] args; String[] names; String[] profiles; String namespace; Class<?> getApp() { return this.app; } public T as(String... names) { this.names = names; return getConfigurer(); } public T args(String... args) { this.args = args; return getConfigurer(); } public T profiles(String... profiles) { this.profiles = profiles; return getConfigurer(); } @SuppressWarnings("unchecked") private T getConfigurer() { return (T) this; } public T namespace(String namespace) { this.namespace = namespace; return getConfigurer(); } public ConfigurableApplicationContext run(String... args) { return applicationBuilder.run(args); } void embed() { final ConfigurableApplicationContext childContext = childContext(this.app, AggregateApplicationBuilder.this.parentContext, this.namespace).args(this.args).config(this.names) .profiles(this.profiles).run(); // Register bindable proxies as beans so they can be queried for later Map<String, BindableProxyFactory> bindableProxies = BeanFactoryUtils .beansOfTypeIncludingAncestors(childContext.getBeanFactory(), BindableProxyFactory.class); for (String bindableProxyName : bindableProxies.keySet()) { try { AggregateApplicationBuilder.this.parentContext.getBeanFactory().registerSingleton( this.getNamespace() + CHILD_CONTEXT_SUFFIX, new ChildContextHolder(childContext)); } catch (Exception e) { throw new IllegalStateException( "Error while trying to register the aggregate bound interface '" + bindableProxyName + "' into namespace '" + this.getNamespace() + "'", e); } } // Register metrics if JMX enabled and exporter avalable if (BeanFactoryUtils.beansOfTypeIncludingAncestors(AggregateApplicationBuilder.this.parentContext, IntegrationMBeanExporter.class).size() > 0) { BeanFactoryUtils .beanOfTypeIncludingAncestors(AggregateApplicationBuilder.this.parentContext, MetricsEndpoint.class) .registerPublicMetrics( new MetricReaderPublicMetrics(new NamespaceAwareSpringIntegrationMetricReader( this.namespace, childContext.getBean(IntegrationMBeanExporter.class)))); } } public AggregateApplication build() { return applicationBuilder; } public String[] getArgs() { return this.args; } public String getNamespace() { return this.namespace; } } private final class ChildContextBuilder { private SpringApplicationBuilder builder; private String configName; private String[] args; private ChildContextBuilder(SpringApplicationBuilder builder) { this.builder = builder; } public ChildContextBuilder profiles(String... profiles) { if (profiles != null) { this.builder.profiles(profiles); } return this; } public ChildContextBuilder config(String... configs) { if (configs != null) { this.configName = StringUtils.arrayToCommaDelimitedString(configs); } return this; } public ChildContextBuilder args(String... args) { this.args = args; return this; } public ConfigurableApplicationContext run() { List<String> args = new ArrayList<String>(); if (this.args != null) { args.addAll(Arrays.asList(this.args)); } if (this.configName != null) { args.add("--spring.config.name=" + this.configName); } return this.builder.run(args.toArray(new String[0])); } } private static class ChildContextHolder { private final ConfigurableApplicationContext childContext; ChildContextHolder(ConfigurableApplicationContext childContext) { Assert.notNull(childContext, "cannot be null"); this.childContext = childContext; } public ConfigurableApplicationContext getChildContext() { return childContext; } } @ImportAutoConfiguration({ChannelBindingAutoConfiguration.class, EndpointAutoConfiguration.class}) @EnableBinding public static class ParentConfiguration { @Bean @ConditionalOnMissingBean(SharedBindingTargetRegistry.class) public SharedBindingTargetRegistry sharedBindingTargetRegistry() { return new SharedBindingTargetRegistry(); } @Bean @ConditionalOnMissingBean(SharedChannelRegistry.class) public SharedChannelRegistry sharedChannelRegistry(SharedBindingTargetRegistry sharedBindingTargetRegistry) { return new SharedChannelRegistry(sharedBindingTargetRegistry); } } }