/** * 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.mina; import java.net.SocketAddress; import java.util.concurrent.CountDownLatch; 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.impl.DefaultProducer; import org.apache.camel.util.CamelLogger; import org.apache.camel.util.ExchangeHelper; import org.apache.camel.util.IOHelper; import org.apache.mina.common.ConnectFuture; import org.apache.mina.common.IoConnector; import org.apache.mina.common.IoHandler; import org.apache.mina.common.IoHandlerAdapter; import org.apache.mina.common.IoSession; import org.apache.mina.transport.socket.nio.SocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A {@link org.apache.camel.Producer} implementation for MINA * * @version */ public class MinaProducer extends DefaultProducer implements ServicePoolAware { private static final Logger LOG = LoggerFactory.getLogger(MinaProducer.class); private IoSession session; private CountDownLatch latch; private boolean lazySessionCreation; private long timeout; private IoConnector connector; private boolean sync; private CamelLogger noReplyLogger; public MinaProducer(MinaEndpoint endpoint) { super(endpoint); this.lazySessionCreation = endpoint.getConfiguration().isLazySessionCreation(); this.timeout = endpoint.getConfiguration().getTimeout(); this.sync = endpoint.getConfiguration().isSync(); this.noReplyLogger = new CamelLogger(LOG, endpoint.getConfiguration().getNoReplyLogLevel()); } @Override public MinaEndpoint getEndpoint() { return (MinaEndpoint) 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; } public void process(Exchange exchange) throws Exception { try { doProcess(exchange); } finally { // ensure we always disconnect if configured maybeDisconnectOnDone(exchange); } } 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, IOHelper.normalizeCharset(getEndpoint().getConfiguration().getCharsetName())); } Object body = MinaPayloadHelper.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 latch if we should get a response latch = new CountDownLatch(1); // reset handler if we expect a response ResponseHandler handler = (ResponseHandler) session.getHandler(); 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 MinaHelper.writeBody(session, body, exchange); if (sync) { // wait for response, consider timeout LOG.debug("Waiting for response using timeout {} millis.", timeout); boolean done = latch.await(timeout, TimeUnit.MILLISECONDS); if (!done) { throw new ExchangeTimedOutException(exchange, timeout); } // did we get a response ResponseHandler handler = (ResponseHandler) session.getHandler(); if (handler.getCause() != null) { throw new CamelExchangeException("Error occurred in ResponseHandler", exchange, handler.getCause()); } else if (!handler.isMessageReceived()) { // no message received throw new CamelExchangeException("No response received from remote server: " + getEndpoint().getEndpointUri(), exchange); } else { // set the result on either IN or OUT on the original exchange depending on its pattern if (ExchangeHelper.isOutCapable(exchange)) { MinaPayloadHelper.setOut(exchange, handler.getMessage()); } else { MinaPayloadHelper.setIn(exchange, handler.getMessage()); } } } } protected void maybeDisconnectOnDone(Exchange exchange) { if (session == null) { return; } // should session be closed after complete? Boolean close; if (ExchangeHelper.isOutCapable(exchange)) { close = exchange.getOut().getHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, Boolean.class); } else { close = exchange.getIn().getHeader(MinaConstants.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) { if (LOG.isDebugEnabled()) { LOG.debug("Closing session when complete at address: {}", getEndpoint().getAddress()); } session.close(); } } @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, getEndpoint().getAddress()); } closeConnection(); super.doStop(); } private void closeConnection() { if (connector instanceof SocketConnector) { // Change the worker timeout to 0 second to make the I/O thread quit soon when there's no connection to manage. // Default worker timeout is 60 sec and therefore the client using MinaProducer cannot terminate the JVM // asap but must wait for the timeout to happen, so to speed this up we set the timeout to 0. LOG.trace("Setting SocketConnector WorkerTimeout=0 to force MINA stopping its resources faster"); ((SocketConnector) connector).setWorkerTimeout(0); } if (session != null) { session.close(); } } private void openConnection() { SocketAddress address = getEndpoint().getAddress(); connector = getEndpoint().getConnector(); if (LOG.isDebugEnabled()) { LOG.debug("Creating connector to address: {} using connector: {} timeout: {} millis.", new Object[]{address, connector, timeout}); } IoHandler ioHandler = new ResponseHandler(getEndpoint()); // connect and wait until the connection is established ConnectFuture future = connector.connect(address, ioHandler, getEndpoint().getConnectorConfig()); future.join(); session = future.getSession(); } /** * Handles response from session writes */ private final class ResponseHandler extends IoHandlerAdapter { private MinaEndpoint endpoint; private Object message; private Throwable cause; private boolean messageReceived; private ResponseHandler(MinaEndpoint endpoint) { this.endpoint = endpoint; } 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; countDown(); } protected void countDown() { CountDownLatch downLatch = latch; 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 if (LOG.isDebugEnabled()) { LOG.debug("Session closed but no message received from address: {}", this.endpoint.getAddress()); } // 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 countDown(); } } @Override public void exceptionCaught(IoSession ioSession, Throwable cause) { LOG.error("Exception on receiving message from address: " + this.endpoint.getAddress() + " using connector: " + this.endpoint.getConnector(), cause); this.message = null; this.messageReceived = false; this.cause = cause; if (ioSession != null) { ioSession.close(); } } public Throwable getCause() { return this.cause; } public Object getMessage() { return this.message; } public boolean isMessageReceived() { return messageReceived; } } }