/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.component.mina2; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import org.apache.camel.CamelExchangeException; import org.apache.camel.Exchange; import org.apache.camel.ExchangeTimedOutException; import org.apache.camel.ServicePoolAware; import org.apache.camel.converter.IOConverter; import org.apache.camel.impl.DefaultProducer; import org.apache.camel.util.CamelLogger; import org.apache.camel.util.ExchangeHelper; import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder; import org.apache.mina.core.filterchain.IoFilter; import org.apache.mina.core.future.CloseFuture; import org.apache.mina.core.future.ConnectFuture; import org.apache.mina.core.service.IoConnector; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.service.IoService; import org.apache.mina.core.session.IoSession; import org.apache.mina.core.session.IoSessionConfig; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory; import org.apache.mina.filter.codec.textline.LineDelimiter; import org.apache.mina.filter.executor.ExecutorFilter; import org.apache.mina.filter.executor.OrderedThreadPoolExecutor; import org.apache.mina.filter.executor.UnorderedThreadPoolExecutor; import org.apache.mina.filter.logging.LoggingFilter; import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.transport.socket.nio.NioDatagramConnector; import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.apache.mina.transport.vmpipe.VmPipeAddress; import org.apache.mina.transport.vmpipe.VmPipeConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A {@link org.apache.camel.Producer} implementation for MINA * * @version */ public class Mina2Producer extends DefaultProducer implements ServicePoolAware { private static final Logger LOG = LoggerFactory.getLogger(Mina2Producer.class); private final ResponseHandler handler; private IoSession session; private CountDownLatch responseLatch; private CountDownLatch closeLatch; private boolean lazySessionCreation; private long timeout; private SocketAddress address; private IoConnector connector; private boolean sync; private CamelLogger noReplyLogger; private Mina2Configuration configuration; private IoSessionConfig connectorConfig; private ExecutorService workerPool; public Mina2Producer(Mina2Endpoint endpoint) throws Exception { super(endpoint); this.configuration = endpoint.getConfiguration(); this.lazySessionCreation = configuration.isLazySessionCreation(); this.timeout = configuration.getTimeout(); this.sync = configuration.isSync(); this.noReplyLogger = new CamelLogger(LOG, configuration.getNoReplyLogLevel()); String protocol = configuration.getProtocol(); if (protocol.equals("tcp")) { setupSocketProtocol(protocol); } else if (configuration.isDatagramProtocol()) { setupDatagramProtocol(protocol); } else if (protocol.equals("vm")) { setupVmProtocol(protocol); } handler = new ResponseHandler(); connector.setHandler(handler); } @Override public Mina2Endpoint getEndpoint() { return (Mina2Endpoint) super.getEndpoint(); } @Override public boolean isSingleton() { // the producer should not be singleton otherwise cannot use concurrent producers and safely // use request/reply with correct correlation return false; } @Override public void process(Exchange exchange) throws Exception { try { doProcess(exchange); } finally { // ensure we always disconnect if configured maybeDisconnectOnDone(exchange); } } @SuppressWarnings("deprecation") protected void doProcess(Exchange exchange) throws Exception { if (session == null && !lazySessionCreation) { throw new IllegalStateException("Not started yet!"); } if (session == null || !session.isConnected()) { openConnection(); } // set the exchange encoding property if (getEndpoint().getConfiguration().getCharsetName() != null) { exchange.setProperty(Exchange.CHARSET_NAME, IOConverter.normalizeCharset(getEndpoint().getConfiguration().getCharsetName())); } Object body = Mina2PayloadHelper.getIn(getEndpoint(), exchange); if (body == null) { noReplyLogger.log("No payload to send for exchange: " + exchange); return; // exit early since nothing to write } // if textline enabled then covert to a String which must be used for textline if (getEndpoint().getConfiguration().isTextline()) { body = getEndpoint().getCamelContext().getTypeConverter().mandatoryConvertTo(String.class, exchange, body); } // if sync is true then we should also wait for a response (synchronous mode) if (sync) { // only initialize responseLatch if we should get a response responseLatch = new CountDownLatch(1); // reset handler if we expect a response handler.reset(); } // log what we are writing if (LOG.isDebugEnabled()) { Object out = body; if (body instanceof byte[]) { // byte arrays is not readable so convert to string out = exchange.getContext().getTypeConverter().convertTo(String.class, body); } LOG.debug("Writing body: {}", out); } // write the body Mina2Helper.writeBody(session, body, exchange); if (sync) { // wait for response, consider timeout LOG.debug("Waiting for response using timeout {} millis.", timeout); boolean done = responseLatch.await(timeout, TimeUnit.MILLISECONDS); if (!done) { throw new ExchangeTimedOutException(exchange, timeout); } // did we get a response if (handler.getCause() != null) { throw new CamelExchangeException("Error occurred in ResponseHandler", exchange, handler.getCause()); } else if (!handler.isMessageReceived()) { // no message received throw new ExchangeTimedOutException(exchange, timeout); } else { // set the result on either IN or OUT on the original exchange depending on its pattern if (ExchangeHelper.isOutCapable(exchange)) { Mina2PayloadHelper.setOut(exchange, handler.getMessage()); } else { Mina2PayloadHelper.setIn(exchange, handler.getMessage()); } } } } protected void maybeDisconnectOnDone(Exchange exchange) throws InterruptedException { if (session == null) { return; } // should session be closed after complete? Boolean close; if (ExchangeHelper.isOutCapable(exchange)) { close = exchange.getOut().getHeader(Mina2Constants.MINA_CLOSE_SESSION_WHEN_COMPLETE, Boolean.class); } else { close = exchange.getIn().getHeader(Mina2Constants.MINA_CLOSE_SESSION_WHEN_COMPLETE, Boolean.class); } // should we disconnect, the header can override the configuration boolean disconnect = getEndpoint().getConfiguration().isDisconnect(); if (close != null) { disconnect = close; } if (disconnect) { LOG.debug("Closing session when complete at address: {}", address); closeSessionIfNeededAndAwaitCloseInHandler(session); } } private void closeSessionIfNeededAndAwaitCloseInHandler(IoSession sessionToBeClosed) throws InterruptedException { closeLatch = new CountDownLatch(1); if (!sessionToBeClosed.isClosing()) { CloseFuture closeFuture = sessionToBeClosed.closeNow(); closeFuture.await(timeout, TimeUnit.MILLISECONDS); closeLatch.await(timeout, TimeUnit.MILLISECONDS); } } public DefaultIoFilterChainBuilder getFilterChain() { return connector.getFilterChain(); } @Override protected void doStart() throws Exception { super.doStart(); if (!lazySessionCreation) { openConnection(); } } @Override protected void doStop() throws Exception { if (LOG.isDebugEnabled()) { LOG.debug("Stopping connector: {} at address: {}", connector, address); } closeConnection(); super.doStop(); } @Override protected void doShutdown() throws Exception { if (workerPool != null) { workerPool.shutdown(); } super.doShutdown(); } private void closeConnection() throws InterruptedException { if (session != null) { closeSessionIfNeededAndAwaitCloseInHandler(session); } connector.dispose(true); } private void openConnection() { if (this.address == null || !this.configuration.isCachedAddress()) { setSocketAddress(this.configuration.getProtocol()); } if (LOG.isDebugEnabled()) { LOG.debug("Creating connector to address: {} using connector: {} timeout: {} millis.", address, connector, timeout); } // connect and wait until the connection is established if (connectorConfig != null) { connector.getSessionConfig().setAll(connectorConfig); } ConnectFuture future = connector.connect(address); future.awaitUninterruptibly(); session = future.getSession(); } // Implementation methods //------------------------------------------------------------------------- protected void setupVmProtocol(String uri) { boolean minaLogger = configuration.isMinaLogger(); List<IoFilter> filters = configuration.getFilters(); address = new VmPipeAddress(configuration.getPort()); connector = new VmPipeConnector(); // connector config if (minaLogger) { connector.getFilterChain().addLast("logger", new LoggingFilter()); } appendIoFiltersToChain(filters, connector.getFilterChain()); if (configuration.getSslContextParameters() != null) { LOG.warn("Using vm protocol" + ", but an SSLContextParameters instance was provided. SSLContextParameters is only supported on the TCP protocol."); } configureCodecFactory("Mina2Producer", connector); } protected void setupSocketProtocol(String uri) throws Exception { boolean minaLogger = configuration.isMinaLogger(); long timeout = configuration.getTimeout(); List<IoFilter> filters = configuration.getFilters(); address = new InetSocketAddress(configuration.getHost(), configuration.getPort()); final int processorCount = Runtime.getRuntime().availableProcessors() + 1; connector = new NioSocketConnector(processorCount); // connector config connectorConfig = connector.getSessionConfig(); if (configuration.isOrderedThreadPoolExecutor()) { workerPool = new OrderedThreadPoolExecutor(configuration.getMaximumPoolSize()); } else { workerPool = new UnorderedThreadPoolExecutor(configuration.getMaximumPoolSize()); } connector.getFilterChain().addLast("threadPool", new ExecutorFilter(workerPool)); if (minaLogger) { connector.getFilterChain().addLast("logger", new LoggingFilter()); } appendIoFiltersToChain(filters, connector.getFilterChain()); if (configuration.getSslContextParameters() != null) { SslFilter filter = new SslFilter(configuration.getSslContextParameters().createSSLContext(getEndpoint().getCamelContext()), configuration.isAutoStartTls()); filter.setUseClientMode(true); connector.getFilterChain().addFirst("sslFilter", filter); } configureCodecFactory("Mina2Producer", connector); connector.setConnectTimeoutMillis(timeout); } protected void configureCodecFactory(String type, IoService service) { if (configuration.getCodec() != null) { addCodecFactory(service, configuration.getCodec()); } else if (configuration.isAllowDefaultCodec()) { configureDefaultCodecFactory(type, service); } } protected void configureDefaultCodecFactory(String type, IoService service) { if (configuration.isTextline()) { Charset charset = getEncodingParameter(type, configuration); LineDelimiter delimiter = getLineDelimiterParameter(configuration.getTextlineDelimiter()); Mina2TextLineCodecFactory codecFactory = new Mina2TextLineCodecFactory(charset, delimiter); if (configuration.getEncoderMaxLineLength() > 0) { codecFactory.setEncoderMaxLineLength(configuration.getEncoderMaxLineLength()); } if (configuration.getDecoderMaxLineLength() > 0) { codecFactory.setDecoderMaxLineLength(configuration.getDecoderMaxLineLength()); } addCodecFactory(service, codecFactory); LOG.debug("{}: Using TextLineCodecFactory: {} using encoding: {} line delimiter: {}({})", type, codecFactory, charset, configuration.getTextlineDelimiter(), delimiter); LOG.debug("Encoder maximum line length: {}. Decoder maximum line length: {}", codecFactory.getEncoderMaxLineLength(), codecFactory.getDecoderMaxLineLength()); } else { ObjectSerializationCodecFactory codecFactory = new ObjectSerializationCodecFactory(); addCodecFactory(service, codecFactory); LOG.debug("{}: Using ObjectSerializationCodecFactory: {}", type, codecFactory); } } protected void setupDatagramProtocol(String uri) { boolean minaLogger = configuration.isMinaLogger(); boolean transferExchange = configuration.isTransferExchange(); List<IoFilter> filters = configuration.getFilters(); if (transferExchange) { throw new IllegalArgumentException("transferExchange=true is not supported for datagram protocol"); } address = new InetSocketAddress(configuration.getHost(), configuration.getPort()); final int processorCount = Runtime.getRuntime().availableProcessors() + 1; connector = new NioDatagramConnector(processorCount); if (configuration.isOrderedThreadPoolExecutor()) { workerPool = new OrderedThreadPoolExecutor(configuration.getMaximumPoolSize()); } else { workerPool = new UnorderedThreadPoolExecutor(configuration.getMaximumPoolSize()); } connectorConfig = connector.getSessionConfig(); connector.getFilterChain().addLast("threadPool", new ExecutorFilter(workerPool)); if (minaLogger) { connector.getFilterChain().addLast("logger", new LoggingFilter()); } appendIoFiltersToChain(filters, connector.getFilterChain()); if (configuration.getSslContextParameters() != null) { LOG.warn("Using datagram protocol, " + configuration.getProtocol() + ", but an SSLContextParameters instance was provided. SSLContextParameters is only supported on the TCP protocol."); } configureDataGramCodecFactory("Mina2Producer", connector, configuration); // set connect timeout to mina in seconds connector.setConnectTimeoutMillis(timeout); } /** * For datagrams the entire message is available as a single IoBuffer so lets just pass those around by default * and try converting whatever they payload is into IoBuffer unless some custom converter is specified */ protected void configureDataGramCodecFactory(final String type, final IoService service, final Mina2Configuration configuration) { ProtocolCodecFactory codecFactory = configuration.getCodec(); if (codecFactory == null) { codecFactory = new Mina2UdpProtocolCodecFactory(this.getEndpoint().getCamelContext()); if (LOG.isDebugEnabled()) { LOG.debug("{}: Using CodecFactory: {}", new Object[]{type, codecFactory}); } } addCodecFactory(service, codecFactory); } private void addCodecFactory(IoService service, ProtocolCodecFactory codecFactory) { LOG.debug("addCodecFactory name: {}", codecFactory.getClass().getName()); service.getFilterChain().addLast("codec", new ProtocolCodecFilter(codecFactory)); } private static LineDelimiter getLineDelimiterParameter(Mina2TextLineDelimiter delimiter) { if (delimiter == null) { return LineDelimiter.DEFAULT; } return delimiter.getLineDelimiter(); } private Charset getEncodingParameter(String type, Mina2Configuration configuration) { String encoding = configuration.getEncoding(); if (encoding == null) { encoding = Charset.defaultCharset().name(); // set in on configuration so its updated configuration.setEncoding(encoding); LOG.debug("{}: No encoding parameter using default charset: {}", type, encoding); } if (!Charset.isSupported(encoding)) { throw new IllegalArgumentException("The encoding: " + encoding + " is not supported"); } return Charset.forName(encoding); } private void appendIoFiltersToChain(List<IoFilter> filters, DefaultIoFilterChainBuilder filterChain) { if (filters != null && filters.size() > 0) { for (IoFilter ioFilter : filters) { filterChain.addLast(ioFilter.getClass().getCanonicalName(), ioFilter); } } } private void setSocketAddress(String protocol) { if (protocol.equals("tcp")) { this.address = new InetSocketAddress(configuration.getHost(), configuration.getPort()); } else if (configuration.isDatagramProtocol()) { this.address = new InetSocketAddress(configuration.getHost(), configuration.getPort()); } else if (protocol.equals("vm")) { this.address = new VmPipeAddress(configuration.getPort()); } } /** * Handles response from session writes */ private final class ResponseHandler extends IoHandlerAdapter { private Object message; private Throwable cause; private boolean messageReceived; public void reset() { this.message = null; this.cause = null; this.messageReceived = false; } @Override public void messageReceived(IoSession ioSession, Object message) throws Exception { LOG.debug("Message received: {}", message); this.message = message; messageReceived = true; cause = null; notifyResultAvailable(); } protected void notifyResultAvailable() { CountDownLatch downLatch = responseLatch; if (downLatch != null) { downLatch.countDown(); } } @Override public void sessionClosed(IoSession session) throws Exception { if (sync && !messageReceived) { // sync=true (InOut mode) so we expected a message as reply but did not get one before the session is closed LOG.debug("Session closed but no message received from address: {}", address); // session was closed but no message received. This could be because the remote server had an internal error // and could not return a response. We should count down to stop waiting for a response notifyResultAvailable(); } notifySessionClosed(); } private void notifySessionClosed() { if (closeLatch != null) { closeLatch.countDown(); } } @Override public void exceptionCaught(IoSession ioSession, Throwable cause) { this.message = null; this.messageReceived = false; this.cause = cause; if (ioSession != null && !closedByMina(cause)) { CloseFuture closeFuture = ioSession.closeNow(); closeFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS); } } private boolean closedByMina(Throwable cause) { return cause instanceof IOException; } public Throwable getCause() { return this.cause; } public Object getMessage() { return this.message; } public boolean isMessageReceived() { return messageReceived; } } }