/*
* Copyright 2013-2015 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.xd.dirt.plugins;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.utils.ThreadUtils;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.interceptor.WireTap;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.util.Assert;
import org.springframework.xd.dirt.integration.bus.BusUtils;
import org.springframework.xd.dirt.integration.bus.MessageBus;
import org.springframework.xd.dirt.integration.bus.MessageBus.Capability;
import org.springframework.xd.dirt.integration.bus.XdHeaders;
import org.springframework.xd.dirt.zookeeper.Paths;
import org.springframework.xd.dirt.zookeeper.ZooKeeperConnection;
import org.springframework.xd.dirt.zookeeper.ZooKeeperConnectionListener;
import org.springframework.xd.dirt.zookeeper.ZooKeeperUtils;
import org.springframework.xd.module.ModuleType;
import org.springframework.xd.module.core.Module;
import org.springframework.xd.module.core.Plugin;
import org.springframework.xd.module.options.spi.ModulePlaceholders;
/**
* Abstract {@link Plugin} that has common implementation methods to bind/unbind {@link Module}'s message producers and
* consumers to/from {@link MessageBus}.
*
* @author Mark Fisher
* @author Gary Russell
* @author David Turanski
* @author Jennifer Hickey
* @author Glenn Renfro
* @author Ilayaperumal Gopinathan
*/
public abstract class AbstractMessageBusBinderPlugin extends AbstractPlugin {
protected static final String MODULE_INPUT_CHANNEL = "input";
protected static final String MODULE_OUTPUT_CHANNEL = "output";
protected static final String JOB_CHANNEL_PREFIX = "job:";
protected final MessageBus messageBus;
/**
* Cache of children under the taps path.
*/
private volatile PathChildrenCache taps;
/**
* A {@link PathChildrenCacheListener} implementation that monitors tap additions and removals.
*/
private final TapListener tapListener = new TapListener();
/**
* Map of channels that can be tapped. The keys are the tap channel names (e.g. tap:stream:ticktock.time.0),
* and the values are the output channels from modules where the actual WireTap interceptors would be added.
*/
private final Map<String, MessageChannel> tappableChannels = new HashMap<String, MessageChannel>();
public AbstractMessageBusBinderPlugin(MessageBus messageBus) {
this(messageBus, null);
}
public AbstractMessageBusBinderPlugin(MessageBus messageBus, ZooKeeperConnection zkConnection) {
Assert.notNull(messageBus, "MessageBus must not be null.");
this.messageBus = messageBus;
if (zkConnection != null) {
if (zkConnection.isConnected()) {
startTapListener(zkConnection.getClient());
}
zkConnection.addListener(new TapLifecycleConnectionListener());
}
}
private void startTapListener(CuratorFramework client) {
String tapPath = Paths.build(Paths.TAPS);
Paths.ensurePath(client, tapPath);
taps = new PathChildrenCache(client, tapPath, true,
ThreadUtils.newThreadFactory("TapsPathChildrenCache"));
taps.getListenable().addListener(tapListener);
try {
taps.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
}
catch (Exception e) {
throw ZooKeeperUtils.wrapThrowable(e, "failed to start TapListener");
}
}
/**
* Bind input/output channel of the module's message consumer/producers to {@link MessageBus}'s message
* source/target entities. The producer must be bound first so that messages can immediately
* start flowing from the consumer.
*
* @param module the module whose consumer and producers to bind to the {@link MessageBus}.
*/
protected final void bindConsumerAndProducers(final Module module) {
boolean trackHistory = module.getDeploymentProperties() != null
? module.getDeploymentProperties().getTrackHistory()
: false;
Properties[] properties = extractConsumerProducerProperties(module);
Map<String, Object> historyProperties = null;
if (trackHistory) {
historyProperties = extractHistoryProperties(module);
addHistoryTag(module, historyProperties);
}
MessageChannel outputChannel = module.getComponent(MODULE_OUTPUT_CHANNEL, MessageChannel.class);
if (outputChannel != null) {
bindMessageProducer(outputChannel, getOutputChannelName(module), properties[1]);
String tapChannelName = buildTapChannelName(module);
tappableChannels.put(tapChannelName, outputChannel);
if (isTapActive(tapChannelName)) {
createAndBindTapChannel(tapChannelName, outputChannel);
}
if (trackHistory) {
track(module, outputChannel, historyProperties);
}
}
MessageChannel inputChannel = module.getComponent(MODULE_INPUT_CHANNEL, MessageChannel.class);
if (inputChannel != null) {
bindMessageConsumer(inputChannel, getInputChannelName(module), module.getDescriptor().getGroup(),
properties[0]);
if (trackHistory && module.getType().equals(ModuleType.sink)) {
track(module, inputChannel, historyProperties);
}
}
}
private void addHistoryTag(Module module, Map<String, Object> historyProperties) {
String historyTag = module.getDescriptor().getModuleLabel();
if (module.getDescriptor().getSinkChannelName() != null) {
historyTag += ">" + module.getDescriptor().getSinkChannelName();
}
if (module.getDescriptor().getSourceChannelName() != null) {
historyTag = module.getDescriptor().getSourceChannelName() + ">" + historyTag;
}
historyProperties.put("module", historyTag);
}
private void track(final Module module, MessageChannel channel, final Map<String, Object> historyProps) {
final MessageBuilderFactory messageBuilderFactory = module.getComponent(
IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
MessageBuilderFactory.class) == null
? new DefaultMessageBuilderFactory()
: module.getComponent(
IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
MessageBuilderFactory.class);
if (channel instanceof ChannelInterceptorAware) {
((ChannelInterceptorAware) channel).addInterceptor(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
@SuppressWarnings("unchecked")
Collection<Map<String, Object>> history =
(Collection<Map<String, Object>>) message.getHeaders().get(XdHeaders.XD_HISTORY);
if (history == null) {
history = new ArrayList<Map<String, Object>>(1);
}
else {
history = new ArrayList<Map<String, Object>>(history);
}
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.putAll(historyProps);
map.put("thread", Thread.currentThread().getName());
history.add(map);
Message<?> out = messageBuilderFactory
.fromMessage(message)
.setHeader(XdHeaders.XD_HISTORY, history)
.build();
map.put("timestamp", out.getHeaders().getTimestamp());
return out;
}
});
}
}
protected final Properties[] extractConsumerProducerProperties(Module module) {
Properties consumerProperties = new Properties();
Properties producerProperties = new Properties();
String consumerKeyPrefix = "consumer.";
String producerKeyPrefix = "producer.";
if (module.getDeploymentProperties() != null) {
for (Map.Entry<String, String> entry : module.getDeploymentProperties().entrySet()) {
if (entry.getKey().startsWith(consumerKeyPrefix)) {
consumerProperties.put(entry.getKey().substring(consumerKeyPrefix.length()), entry.getValue());
}
else if (entry.getKey().startsWith(producerKeyPrefix)) {
producerProperties.put(entry.getKey().substring(producerKeyPrefix.length()), entry.getValue());
}
}
}
return new Properties[] { consumerProperties, producerProperties };
}
protected final Map<String, Object> extractHistoryProperties(Module module) {
Map<String, Object> properties = new LinkedHashMap<String, Object>();
if (module.getProperties() != null) {
for (Map.Entry<Object, Object> entry : module.getProperties().entrySet()) {
if (entry.getKey() instanceof String) {
String key = (String) entry.getKey();
if (key.startsWith(ModulePlaceholders.XD_CONTAINER_KEY_PREFIX)) {
key = key.substring(ModulePlaceholders.XD_CONTAINER_KEY_PREFIX.length());
if (key.equals("id")) {
key = "container.id";
}
properties.put(key, entry.getValue());
}
else if (key.equals(ModulePlaceholders.XD_STREAM_NAME_KEY)) {
properties.put(key.substring(3), entry.getValue());
}
}
}
}
return properties;
}
@Override
public void beforeShutdown(Module module) {
unbindConsumer(module);
}
@Override
public void removeModule(Module module) {
super.removeModule(module);
unbindProducers(module);
}
protected abstract String getInputChannelName(Module module);
protected abstract String getOutputChannelName(Module module);
protected abstract String buildTapChannelName(Module module);
private void bindMessageConsumer(MessageChannel inputChannel, String inputChannelName,
String group, Properties consumerProperties) {
if (BusUtils.isChannelPubSub(inputChannelName)) {
String channelToBind = inputChannelName;
if (this.messageBus.isCapable(Capability.DURABLE_PUBSUB)) {
channelToBind = BusUtils.addGroupToPubSub(group, inputChannelName);
}
messageBus.bindPubSubConsumer(channelToBind, inputChannel, consumerProperties);
}
else {
messageBus.bindConsumer(inputChannelName, inputChannel, consumerProperties);
}
}
private void bindMessageProducer(MessageChannel outputChannel, String outputChannelName,
Properties producerProperties) {
if (BusUtils.isChannelPubSub(outputChannelName)) {
messageBus.bindPubSubProducer(outputChannelName, outputChannel, producerProperties);
}
else {
messageBus.bindProducer(outputChannelName, outputChannel, producerProperties);
}
}
/**
* Creates a wiretap on the output channel of the {@link Module} and binds the tap channel to {@link MessageBus}'s
* message target.
*
* @param tapChannelName the name of the tap channel
* @param outputChannel the channel to tap
*/
private void createAndBindTapChannel(String tapChannelName, MessageChannel outputChannel) {
logger.info("creating and binding tap channel for {}", tapChannelName);
if (outputChannel instanceof ChannelInterceptorAware) {
DirectChannel tapChannel = new DirectChannel();
tapChannel.setBeanName(tapChannelName + ".tap.bridge");
messageBus.bindPubSubProducer(tapChannelName, tapChannel, null); // TODO tap producer props
tapOutputChannel(tapChannel, (ChannelInterceptorAware) outputChannel);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("output channel is not interceptor aware. Tap will not be created.");
}
}
}
private MessageChannel tapOutputChannel(MessageChannel tapChannel, ChannelInterceptorAware outputChannel) {
outputChannel.addInterceptor(new WireTap(tapChannel));
return tapChannel;
}
/**
* Unbind the input channel of the module from the {@link MessageBus}
* (stop sending new messages to the module so it can be stopped).
*
* @param module the module for which the consumer is to be unbound from the {@link MessageBus}.
*/
protected void unbindConsumer(Module module) {
MessageChannel inputChannel = module.getComponent(MODULE_INPUT_CHANNEL, MessageChannel.class);
if (inputChannel != null) {
String channelToUnbind = getInputChannelName(module);
if (this.messageBus.isCapable(Capability.DURABLE_PUBSUB)) {
channelToUnbind = BusUtils.addGroupToPubSub(module.getDescriptor().getGroup(), channelToUnbind);
}
messageBus.unbindConsumer(channelToUnbind, inputChannel);
if (logger.isDebugEnabled()) {
logger.debug("Unbound consumer for " + module.toString());
}
}
}
/**
* Unbind the output channel of the module (and tap if present) from the {@link MessageBus}
* (after it has been stopped).
*
* @param module the module for which producers are to be unbound from the {@link MessageBus}.
*/
protected void unbindProducers(Module module) {
MessageChannel outputChannel = module.getComponent(MODULE_OUTPUT_CHANNEL, MessageChannel.class);
if (outputChannel != null) {
messageBus.unbindProducer(getOutputChannelName(module), outputChannel);
String tapChannelName = buildTapChannelName(module);
unbindTapChannel(tapChannelName);
tappableChannels.remove(tapChannelName);
if (logger.isDebugEnabled()) {
logger.debug("Unbound producer(s) for " + module.toString());
}
}
}
private void unbindTapChannel(String tapChannelName) {
// Should this be unbindProducer() as there won't be multiple producers on the tap channel.
MessageChannel tappedChannel = tappableChannels.get(tapChannelName);
if (tappedChannel instanceof ChannelInterceptorAware) {
ChannelInterceptorAware interceptorAware = ((ChannelInterceptorAware) tappedChannel);
List<ChannelInterceptor> interceptors = new ArrayList<ChannelInterceptor>();
for (ChannelInterceptor interceptor : interceptorAware.getChannelInterceptors()) {
if (interceptor instanceof WireTap) {
((WireTap) interceptor).stop();
}
else {
interceptors.add(interceptor);
}
}
interceptorAware.setInterceptors(interceptors);
messageBus.unbindProducers(tapChannelName);
}
}
@Override
public int getOrder() {
return 0;
}
/**
* Event handler for tap additions.
*
* @param data module data
*/
private void onTapAdded(ChildData data) {
String tapChannelName = buildTapChannelNameFromPath(data.getPath());
MessageChannel outputChannel = tappableChannels.get(tapChannelName);
if (outputChannel != null) {
createAndBindTapChannel(tapChannelName, outputChannel);
}
}
/**
* Event handler for tap removals.
*
* @param data module data
*/
private void onTapRemoved(ChildData data) {
unbindTapChannel(buildTapChannelNameFromPath(data.getPath()));
}
/**
* Checks whether the provided tap channel name has one or more active subscribers.
*
* @param tapChannelName the tap channel to check
*
* @return {@code true} if the tap does have one or more active subscribers
*/
private boolean isTapActive(String tapChannelName) {
Assert.state(taps != null, "tap cache not started");
List<ChildData> currentTaps = taps.getCurrentData();
for (ChildData data : currentTaps) {
// example path: /taps/stream:ticktock.time.0
if (buildTapChannelNameFromPath(data.getPath()).equals(tapChannelName)) {
return true;
}
}
return false;
}
/**
* Generates the name of a tap channel given a ZooKeeper tap path.
*
* @param path the ZooKeeper path under {@link Paths#TAPS}.
*
* @return the tap channel name
*/
private String buildTapChannelNameFromPath(String path) {
return BusUtils.TAP_CHANNEL_PREFIX + Paths.stripPath(path);
}
/**
* Listener for tap additions and removals under {@link Paths#TAPS}.
*/
class TapListener implements PathChildrenCacheListener {
/**
* {@inheritDoc}
*/
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
ZooKeeperUtils.logCacheEvent(logger, event);
switch (event.getType()) {
case INITIALIZED:
break;
case CHILD_ADDED:
onTapAdded(event.getData());
break;
case CHILD_REMOVED:
onTapRemoved(event.getData());
break;
default:
break;
}
}
}
/**
* A {@link ZooKeeperConnectionListener} that manages the lifecycle of the taps cache listener.
*/
class TapLifecycleConnectionListener implements ZooKeeperConnectionListener {
@Override
public void onDisconnect(CuratorFramework client) {
taps.getListenable().removeListener(tapListener);
try {
taps.close();
}
catch (Exception e) {
throw ZooKeeperUtils.wrapThrowable(e);
}
}
@Override
public void onSuspend(CuratorFramework client) {
}
@Override
public void onConnect(CuratorFramework client) {
startTapListener(client);
}
@Override
public void onResume(CuratorFramework client) {
}
}
}