/* * Copyright 2002-2017 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.integration.router; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.springframework.integration.support.management.MappingMessageRouterManagement; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.DestinationResolutionException; import org.springframework.messaging.core.DestinationResolver; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Base class for all Message Routers that support mapping from arbitrary String values * to Message Channel names. * * @author Mark Fisher * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell * @author Artem Bilan * @since 2.1 */ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter implements MappingMessageRouterManagement { private static final int DEFAULT_DYNAMIC_CHANNEL_LIMIT = 100; private int dynamicChannelLimit = DEFAULT_DYNAMIC_CHANNEL_LIMIT; @SuppressWarnings("serial") private final Map<String, MessageChannel> dynamicChannels = Collections.<String, MessageChannel>synchronizedMap( new LinkedHashMap<String, MessageChannel>(DEFAULT_DYNAMIC_CHANNEL_LIMIT, 0.75f, true) { @Override protected boolean removeEldestEntry(Entry<String, MessageChannel> eldest) { return this.size() > AbstractMappingMessageRouter.this.dynamicChannelLimit; } }); protected volatile Map<String, String> channelMappings = new ConcurrentHashMap<String, String>(); private volatile String prefix; private volatile String suffix; private volatile boolean resolutionRequired = true; /** * Provide mappings from channel keys to channel names. * Channel names will be resolved by the {@link DestinationResolver}. * @param channelMappings The channel mappings. */ @Override @ManagedAttribute public void setChannelMappings(Map<String, String> channelMappings) { Assert.notNull(channelMappings, "'channelMappings' must not be null"); Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(channelMappings); doSetChannelMappings(newChannelMappings); } /** * Specify a prefix to be added to each channel name prior to resolution. * @param prefix The prefix. */ public void setPrefix(String prefix) { this.prefix = prefix; } /** * Specify a suffix to be added to each channel name prior to resolution. * @param suffix The suffix. */ public void setSuffix(String suffix) { this.suffix = suffix; } /** * Specify whether this router should ignore any failure to resolve a channel name to * an actual MessageChannel instance when delegating to the ChannelResolver strategy. * @param resolutionRequired true if resolution is required. */ public void setResolutionRequired(boolean resolutionRequired) { this.resolutionRequired = resolutionRequired; } /** * Set a limit for how many dynamic channels are retained (for reporting purposes). * When the limit is exceeded, the oldest channel is discarded. * <p><b>NOTE: this does not affect routing, just the reporting which dynamically * resolved channels have been routed to.</b> Default {@code 100}. * @param dynamicChannelLimit the limit. * @see #getDynamicChannelNames() */ public void setDynamicChannelLimit(int dynamicChannelLimit) { this.dynamicChannelLimit = dynamicChannelLimit; } /** * Returns an unmodifiable version of the channel mappings. * This is intended for use by subclasses only. * @return The channel mappings. */ @Override @ManagedAttribute public Map<String, String> getChannelMappings() { return new HashMap<String, String>(this.channelMappings); } /** * Add a channel mapping from the provided key to channel name. * @param key The key. * @param channelName The channel name. */ @Override @ManagedOperation public void setChannelMapping(String key, String channelName) { Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(this.channelMappings); newChannelMappings.put(key, channelName); this.channelMappings = newChannelMappings; } /** * Remove a channel mapping for the given key if present. * @param key The key. */ @Override @ManagedOperation public void removeChannelMapping(String key) { Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(this.channelMappings); newChannelMappings.remove(key); this.channelMappings = newChannelMappings; } @Override @ManagedAttribute public Collection<String> getDynamicChannelNames() { return Collections.unmodifiableSet(this.dynamicChannels.keySet()); } /** * Subclasses must implement this method to return the channel keys. * A "key" might be present in this router's "channelMappings", or it * could be the channel's name or even the Message Channel instance itself. * @param message The message. * @return The channel keys. */ protected abstract List<Object> getChannelKeys(Message<?> message); @Override protected Collection<MessageChannel> determineTargetChannels(Message<?> message) { Collection<MessageChannel> channels = new ArrayList<MessageChannel>(); Collection<Object> channelKeys = this.getChannelKeys(message); addToCollection(channels, channelKeys, message); return channels; } /** * Convenience method allowing conversion of a list * of mappings in a control-bus message. * <p>This is intended to be called via a control-bus; keys and values that are not * Strings will be ignored. * <p>Mappings must be delimited with newlines, for example: * <p>{@code "@'myRouter.handler'.replaceChannelMappings('foo=qux \n baz=bar')"}. * @param channelMappings The channel mappings. * @since 4.0 */ @Override @ManagedOperation public void replaceChannelMappings(Properties channelMappings) { Assert.notNull(channelMappings, "'channelMappings' must not be null"); Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(); Set<String> keys = channelMappings.stringPropertyNames(); for (String key : keys) { newChannelMappings.put(key.trim(), channelMappings.getProperty(key).trim()); } this.doSetChannelMappings(newChannelMappings); } private void doSetChannelMappings(Map<String, String> newChannelMappings) { Map<String, String> oldChannelMappings = this.channelMappings; this.channelMappings = newChannelMappings; if (logger.isDebugEnabled()) { logger.debug("Channel mappings: " + oldChannelMappings + " replaced with: " + newChannelMappings); } } private MessageChannel resolveChannelForName(String channelName, Message<?> message) { MessageChannel channel = null; try { channel = getChannelResolver().resolveDestination(channelName); } catch (DestinationResolutionException e) { if (this.resolutionRequired) { throw new MessagingException(message, "failed to resolve channel name '" + channelName + "'", e); } } if (channel == null && this.resolutionRequired) { throw new MessagingException(message, "failed to resolve channel name '" + channelName + "'"); } return channel; } private void addChannelFromString(Collection<MessageChannel> channels, String channelKey, Message<?> message) { if (channelKey.indexOf(',') != -1) { for (String name : StringUtils.tokenizeToStringArray(channelKey, ",")) { addChannelFromString(channels, name, message); } return; } // if the channelMappings contains a mapping, we'll use the mapped value // otherwise, the String-based channelKey itself will be used as the channel name String channelName = channelKey; boolean mapped = false; if (this.channelMappings.containsKey(channelKey)) { channelName = this.channelMappings.get(channelKey); mapped = true; } if (this.prefix != null) { channelName = this.prefix + channelName; } if (this.suffix != null) { channelName = channelName + this.suffix; } MessageChannel channel = resolveChannelForName(channelName, message); if (channel != null) { channels.add(channel); if (!mapped && !(this.dynamicChannels.get(channelName) != null)) { this.dynamicChannels.put(channelName, channel); } } } private void addToCollection(Collection<MessageChannel> channels, Collection<?> channelKeys, Message<?> message) { if (channelKeys == null) { return; } for (Object channelKey : channelKeys) { if (channelKey == null) { continue; } else if (channelKey instanceof MessageChannel) { channels.add((MessageChannel) channelKey); } else if (channelKey instanceof MessageChannel[]) { channels.addAll(Arrays.asList((MessageChannel[]) channelKey)); } else if (channelKey instanceof String) { addChannelFromString(channels, (String) channelKey, message); } else if (channelKey instanceof Class) { addChannelFromString(channels, ((Class<?>) channelKey).getName(), message); } else if (channelKey instanceof String[]) { for (String indicatorName : (String[]) channelKey) { addChannelFromString(channels, indicatorName, message); } } else if (channelKey instanceof Collection) { addToCollection(channels, (Collection<?>) channelKey, message); } else if (getRequiredConversionService().canConvert(channelKey.getClass(), String.class)) { addChannelFromString(channels, getConversionService().convert(channelKey, String.class), message); } else { throw new MessagingException("unsupported return type for router [" + channelKey.getClass() + "]"); } } } }