/*
* Copyright 2002-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.integration.ip.tcp.connection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.core.serializer.Deserializer;
import org.springframework.core.serializer.Serializer;
import org.springframework.integration.ip.IpHeaders;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.util.SimplePool;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
/**
* Connection factory that caches connections from the underlying target factory. The underlying
* factory will be reconfigured to have {@code singleUse=true} in order for the connection to be
* returned to the cache after use. Users should not subsequently set the underlying property to
* false, or cache starvation will result.
*
* @author Gary Russell
* @since 2.2
*
*/
public class CachingClientConnectionFactory extends AbstractClientConnectionFactory {
private final AbstractClientConnectionFactory targetConnectionFactory;
private final SimplePool<TcpConnectionSupport> pool;
/**
* Construct a caching connection factory that delegates to the provided factory, with
* the provided pool size.
* @param target the target factory.
* @param poolSize the number of connections to allow.
*/
public CachingClientConnectionFactory(AbstractClientConnectionFactory target, int poolSize) {
super("", 0);
// override single-use to true so the target creates multiple connections
target.setSingleUse(true);
this.targetConnectionFactory = target;
this.pool = new SimplePool<TcpConnectionSupport>(poolSize,
new SimplePool.PoolItemCallback<TcpConnectionSupport>() {
@Override
public TcpConnectionSupport createForPool() {
try {
return CachingClientConnectionFactory.this.targetConnectionFactory.getConnection();
}
catch (Exception e) {
throw new MessagingException("Failed to obtain connection", e);
}
}
@Override
public boolean isStale(TcpConnectionSupport connection) {
return !connection.isOpen();
}
@Override
public void removedFromPool(TcpConnectionSupport connection) {
connection.close();
}
});
}
/**
* @param connectionWaitTimeout the new timeout.
* @see SimplePool#setWaitTimeout(long)
*/
public void setConnectionWaitTimeout(int connectionWaitTimeout) {
this.pool.setWaitTimeout(connectionWaitTimeout);
}
/**
* @param poolSize the new pool size.
* @see SimplePool#setPoolSize(int)
*/
public void setPoolSize(int poolSize) {
this.pool.setPoolSize(poolSize);
}
/**
* @see SimplePool#getPoolSize()
* @return the pool size.
*/
public int getPoolSize() {
return this.pool.getPoolSize();
}
/**
* @see SimplePool#getIdleCount()
* @return the idle count.
*/
public int getIdleCount() {
return this.pool.getIdleCount();
}
/**
* @see SimplePool#getActiveCount()
* @return the active count.
*/
public int getActiveCount() {
return this.pool.getActiveCount();
}
/**
* @see SimplePool#getAllocatedCount()
* @return the allocated count.
*/
public int getAllocatedCount() {
return this.pool.getAllocatedCount();
}
@Override
public TcpConnectionSupport obtainConnection() throws Exception {
return new CachedConnection(this.pool.getItem(), getListener());
}
///////////////// DELEGATE METHODS ///////////////////////
@Override
public boolean isRunning() {
return this.targetConnectionFactory.isRunning();
}
@Override
public int hashCode() {
return this.targetConnectionFactory.hashCode();
}
@Override
public void setComponentName(String componentName) {
this.targetConnectionFactory.setComponentName(componentName);
}
@Override
public String getComponentType() {
return this.targetConnectionFactory.getComponentType();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CachingClientConnectionFactory that = (CachingClientConnectionFactory) o;
return this.targetConnectionFactory.equals(that.targetConnectionFactory);
}
@Override
public int getSoTimeout() {
return this.targetConnectionFactory.getSoTimeout();
}
@Override
public void setSoTimeout(int soTimeout) {
this.targetConnectionFactory.setSoTimeout(soTimeout);
}
@Override
public int getSoReceiveBufferSize() {
return this.targetConnectionFactory.getSoReceiveBufferSize();
}
@Override
public void setSoReceiveBufferSize(int soReceiveBufferSize) {
this.targetConnectionFactory.setSoReceiveBufferSize(soReceiveBufferSize);
}
@Override
public int getSoSendBufferSize() {
return this.targetConnectionFactory.getSoSendBufferSize();
}
@Override
public void setSoSendBufferSize(int soSendBufferSize) {
this.targetConnectionFactory.setSoSendBufferSize(soSendBufferSize);
}
@Override
public boolean isSoTcpNoDelay() {
return this.targetConnectionFactory.isSoTcpNoDelay();
}
@Override
public void setSoTcpNoDelay(boolean soTcpNoDelay) {
this.targetConnectionFactory.setSoTcpNoDelay(soTcpNoDelay);
}
@Override
public int getSoLinger() {
return this.targetConnectionFactory.getSoLinger();
}
@Override
public void setSoLinger(int soLinger) {
this.targetConnectionFactory.setSoLinger(soLinger);
}
@Override
public boolean isSoKeepAlive() {
return this.targetConnectionFactory.isSoKeepAlive();
}
@Override
public void setSoKeepAlive(boolean soKeepAlive) {
this.targetConnectionFactory.setSoKeepAlive(soKeepAlive);
}
@Override
public int getSoTrafficClass() {
return this.targetConnectionFactory.getSoTrafficClass();
}
@Override
public void setSoTrafficClass(int soTrafficClass) {
this.targetConnectionFactory.setSoTrafficClass(soTrafficClass);
}
@Override
public String getHost() {
return this.targetConnectionFactory.getHost();
}
@Override
public int getPort() {
return this.targetConnectionFactory.getPort();
}
@Override
public TcpSender getSender() {
return this.targetConnectionFactory.getSender();
}
@Override
public Serializer<?> getSerializer() {
return this.targetConnectionFactory.getSerializer();
}
@Override
public Deserializer<?> getDeserializer() {
return this.targetConnectionFactory.getDeserializer();
}
@Override
public TcpMessageMapper getMapper() {
return this.targetConnectionFactory.getMapper();
}
/**
* Delegate TCP Client Connection factories that are used to receive
* data need a Listener to send the messages to.
* This applies to client factories used for outbound gateways
* or for a pair of collaborating channel adapters.
* <p>
* During initialization, if a factory detects it has no listener
* it's listening logic (active thread) is terminated.
* <p>
* The listener registered with a factory is provided to each
* connection it creates so it can call the onMessage() method.
* <p>
* This code satisfies the first requirement in that this
* listener signals to the factory that it needs to run
* its listening logic.
* <p>
* When we wrap actual connections with CachedConnections,
* the connection is given the wrapper as a listener, so it
* can enhance the headers in onMessage(); the wrapper then invokes
* the real listener supplied here, with the modified message.
*/
@Override
public void registerListener(TcpListener listener) {
super.registerListener(listener);
this.targetConnectionFactory.enableManualListenerRegistration();
}
@Override
public void registerSender(TcpSender sender) {
this.targetConnectionFactory.registerSender(sender);
}
@Override
public void setTaskExecutor(Executor taskExecutor) {
this.targetConnectionFactory.setTaskExecutor(taskExecutor);
}
@Override
public void setDeserializer(Deserializer<?> deserializer) {
this.targetConnectionFactory.setDeserializer(deserializer);
}
@Override
public void setSerializer(Serializer<?> serializer) {
this.targetConnectionFactory.setSerializer(serializer);
}
@Override
public void setMapper(TcpMessageMapper mapper) {
this.targetConnectionFactory.setMapper(mapper);
}
@Override
public boolean isSingleUse() {
return true;
}
/**
* Ignored on this factory; connections are always cached in the pool. The underlying
* connection factory will have its singleUse property coerced to true (causing the
* connection to be returned). Setting it to false on the underlying factory after initialization
* will cause cache starvation.
* @param singleUse the singleUse.
*/
@Override
public void setSingleUse(boolean singleUse) {
if (!singleUse && logger.isDebugEnabled()) {
logger.debug("singleUse=false is not supported; cached connections are never closed");
}
}
@Override
public void setInterceptorFactoryChain(TcpConnectionInterceptorFactoryChain interceptorFactoryChain) {
this.targetConnectionFactory.setInterceptorFactoryChain(interceptorFactoryChain);
}
@Override
public void setLookupHost(boolean lookupHost) {
this.targetConnectionFactory.setLookupHost(lookupHost);
}
@Override
public boolean isLookupHost() {
return this.targetConnectionFactory.isLookupHost();
}
@Override
public void forceClose(TcpConnection connection) {
if (connection instanceof CachedConnection) {
((CachedConnection) connection).physicallyClose();
}
// will be returned to pool but stale, so will be re-established
super.forceClose(connection);
}
@Override
public void enableManualListenerRegistration() {
super.enableManualListenerRegistration();
this.targetConnectionFactory.enableManualListenerRegistration();
}
@Override
public void start() {
setActive(true);
this.targetConnectionFactory.start();
super.start();
}
@Override
public synchronized void stop() {
this.targetConnectionFactory.stop();
this.pool.removeAllIdleItems();
}
private final class CachedConnection extends TcpConnectionInterceptorSupport {
private final AtomicBoolean released = new AtomicBoolean();
private CachedConnection(TcpConnectionSupport connection, TcpListener tcpListener) {
super.setTheConnection(connection);
registerListener(tcpListener);
}
@Override
public void close() {
if (!this.released.compareAndSet(false, true)) {
if (logger.isDebugEnabled()) {
logger.debug("Connection " + getConnectionId() + " has already been released");
}
}
else {
/*
* If the delegate is stopped, actually close the connection, but still release
* it to the pool, it will be discarded/renewed the next time it is retrieved.
*/
if (!isRunning()) {
if (logger.isDebugEnabled()) {
logger.debug("Factory not running - closing " + getConnectionId());
}
super.close();
}
CachingClientConnectionFactory.this.pool.releaseItem(getTheConnection());
}
}
@Override
public String getConnectionId() {
return "Cached:" + super.getConnectionId();
}
@Override
public String toString() {
return getConnectionId();
}
/**
* We have to intercept the message to replace the connectionId header with
* ours so the listener can correlate a response with a request. We supply
* the actual connectionId in another header for convenience and tracing
* purposes.
*/
@Override
public boolean onMessage(Message<?> message) {
Message<?> modifiedMessage;
if (message instanceof ErrorMessage) {
Map<String, Object> headers = new HashMap<String, Object>(message.getHeaders());
headers.put(IpHeaders.CONNECTION_ID, getConnectionId());
if (headers.get(IpHeaders.ACTUAL_CONNECTION_ID) == null) {
headers.put(IpHeaders.ACTUAL_CONNECTION_ID,
message.getHeaders().get(IpHeaders.CONNECTION_ID));
}
modifiedMessage = new ErrorMessage((Throwable) message.getPayload(), headers);
}
else {
AbstractIntegrationMessageBuilder<?> messageBuilder =
CachingClientConnectionFactory.this.getMessageBuilderFactory()
.fromMessage(message)
.setHeader(IpHeaders.CONNECTION_ID, getConnectionId());
if (message.getHeaders().get(IpHeaders.ACTUAL_CONNECTION_ID) == null) {
messageBuilder.setHeader(IpHeaders.ACTUAL_CONNECTION_ID,
message.getHeaders().get(IpHeaders.CONNECTION_ID));
}
modifiedMessage = messageBuilder.build();
}
TcpListener listener = getListener();
if (listener != null) {
listener.onMessage(modifiedMessage);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Message discarded; no listener: " + message);
}
}
return true;
}
private void physicallyClose() {
getTheConnection().close();
}
}
}