001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.component.mina;
018    
019    import java.net.SocketAddress;
020    import java.util.concurrent.CountDownLatch;
021    import java.util.concurrent.TimeUnit;
022    
023    import org.apache.camel.CamelExchangeException;
024    import org.apache.camel.Exchange;
025    import org.apache.camel.ExchangeTimedOutException;
026    import org.apache.camel.ServicePoolAware;
027    import org.apache.camel.impl.DefaultProducer;
028    import org.apache.camel.processor.Logger;
029    import org.apache.camel.util.ExchangeHelper;
030    import org.apache.commons.logging.Log;
031    import org.apache.commons.logging.LogFactory;
032    import org.apache.mina.common.ConnectFuture;
033    import org.apache.mina.common.IoConnector;
034    import org.apache.mina.common.IoHandler;
035    import org.apache.mina.common.IoHandlerAdapter;
036    import org.apache.mina.common.IoSession;
037    import org.apache.mina.transport.socket.nio.SocketConnector;
038    
039    /**
040     * A {@link org.apache.camel.Producer} implementation for MINA
041     *
042     * @version $Revision: 20195 $
043     */
044    public class MinaProducer extends DefaultProducer implements ServicePoolAware {
045        private static final transient Log LOG = LogFactory.getLog(MinaProducer.class);
046        private IoSession session;
047        private MinaEndpoint endpoint;
048        private CountDownLatch latch;
049        private boolean lazySessionCreation;
050        private long timeout;
051        private IoConnector connector;
052        private boolean sync;
053        private Logger noReplyLogger;
054    
055        public MinaProducer(MinaEndpoint endpoint) {
056            super(endpoint);
057            this.endpoint = endpoint;
058            this.lazySessionCreation = endpoint.getConfiguration().isLazySessionCreation();
059            this.timeout = endpoint.getConfiguration().getTimeout();
060            this.sync = endpoint.getConfiguration().isSync();
061            this.noReplyLogger = new Logger(LOG, endpoint.getConfiguration().getNoReplyLogLevel());
062        }
063    
064        @Override
065        public boolean isSingleton() {
066            // the producer should not be singleton otherwise cannot use concurrent producers and safely
067            // use request/reply with correct correlation
068            return false;
069        }
070    
071        public void process(Exchange exchange) throws Exception {
072            if (session == null && !lazySessionCreation) {
073                throw new IllegalStateException("Not started yet!");
074            }
075            if (session == null || !session.isConnected()) {
076                openConnection();
077            }
078    
079            // set the exchange encoding property
080            if (endpoint.getConfiguration().getCharsetName() != null) {
081                exchange.setProperty(Exchange.CHARSET_NAME, endpoint.getConfiguration().getCharsetName());
082            }
083    
084            Object body = MinaPayloadHelper.getIn(endpoint, exchange);
085            if (body == null) {
086                noReplyLogger.log("No payload to send for exchange: " + exchange);
087                return; // exit early since nothing to write
088            }
089    
090            // if textline enabled then covert to a String which must be used for textline
091            if (endpoint.getConfiguration().isTextline()) {
092                body = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo(String.class, exchange, body);
093            }
094    
095            // if sync is true then we should also wait for a response (synchronous mode)
096            if (sync) {
097                // only initialize latch if we should get a response
098                latch = new CountDownLatch(1);
099                // reset handler if we expect a response
100                ResponseHandler handler = (ResponseHandler) session.getHandler();
101                handler.reset();
102            }
103    
104            // log what we are writing
105            if (LOG.isDebugEnabled()) {
106                Object out = body;
107                if (body instanceof byte[]) {
108                    // byte arrays is not readable so convert to string
109                    out = exchange.getContext().getTypeConverter().convertTo(String.class, body);
110                }
111                LOG.debug("Writing body : " + out);
112            }
113            // write the body
114            MinaHelper.writeBody(session, body, exchange);
115    
116            if (sync) {
117                // wait for response, consider timeout
118                if (LOG.isDebugEnabled()) {
119                    LOG.debug("Waiting for response using timeout " + timeout + " millis.");
120                }
121                boolean done = latch.await(timeout, TimeUnit.MILLISECONDS);
122                if (!done) {
123                    throw new ExchangeTimedOutException(exchange, timeout);
124                }
125    
126                // did we get a response
127                ResponseHandler handler = (ResponseHandler) session.getHandler();
128                if (handler.getCause() != null) {
129                    throw new CamelExchangeException("Error occurred in ResponseHandler", exchange, handler.getCause());
130                } else if (!handler.isMessageReceived()) {
131                    // no message received
132                    throw new CamelExchangeException("No response received from remote server: " + endpoint.getEndpointUri(), exchange);
133                } else {
134                    // set the result on either IN or OUT on the original exchange depending on its pattern
135                    if (ExchangeHelper.isOutCapable(exchange)) {
136                        MinaPayloadHelper.setOut(exchange, handler.getMessage());
137                    } else {
138                        MinaPayloadHelper.setIn(exchange, handler.getMessage());
139                    }
140                }
141            }
142    
143            // should session be closed after complete?
144            Boolean close;
145            if (ExchangeHelper.isOutCapable(exchange)) {
146                close = exchange.getOut().getHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, Boolean.class);
147            } else {
148                close = exchange.getIn().getHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, Boolean.class);
149            }
150    
151            // should we disconnect, the header can override the configuration
152            boolean disconnect = endpoint.getConfiguration().isDisconnect();
153            if (close != null) {
154                disconnect = close;
155            }
156            if (disconnect) {
157                if (LOG.isDebugEnabled()) {
158                    LOG.debug("Closing session when complete at address: " + endpoint.getAddress());
159                }
160                session.close();
161            }
162        }
163    
164        @Override
165        protected void doStart() throws Exception {
166            super.doStart();
167            if (!lazySessionCreation) {
168                openConnection();
169            }
170        }
171    
172        @Override
173        protected void doStop() throws Exception {
174            if (LOG.isDebugEnabled()) {
175                LOG.debug("Stopping connector: " + connector + " at address: " + endpoint.getAddress());
176            }
177            closeConnection();
178            super.doStop();
179        }
180    
181        private void closeConnection() {
182            if (connector instanceof SocketConnector) {
183                // Change the worker timeout to 0 second to make the I/O thread quit soon when there's no connection to manage.
184                // Default worker timeout is 60 sec and therefore the client using MinaProducer cannot terminate the JVM
185                // asap but must wait for the timeout to happen, so to speed this up we set the timeout to 0.
186                if (LOG.isTraceEnabled()) {
187                    LOG.trace("Setting SocketConnector WorkerTimeout=0 to force MINA stopping its resources faster");
188                }
189                ((SocketConnector) connector).setWorkerTimeout(0);
190            }
191    
192            if (session != null) {
193                session.close();
194            }
195        }
196    
197        private void openConnection() {
198            SocketAddress address = endpoint.getAddress();
199            connector = endpoint.getConnector();
200            if (LOG.isDebugEnabled()) {
201                LOG.debug("Creating connector to address: " + address + " using connector: " + connector + " timeout: " + timeout + " millis.");
202            }
203            IoHandler ioHandler = new ResponseHandler(endpoint);
204            // connect and wait until the connection is established
205            ConnectFuture future = connector.connect(address, ioHandler, endpoint.getConnectorConfig());
206            future.join();
207            session = future.getSession();
208        }
209    
210        /**
211         * Handles response from session writes
212         */
213        private final class ResponseHandler extends IoHandlerAdapter {
214            private MinaEndpoint endpoint;
215            private Object message;
216            private Throwable cause;
217            private boolean messageReceived;
218    
219            private ResponseHandler(MinaEndpoint endpoint) {
220                this.endpoint = endpoint;
221            }
222    
223            public void reset() {
224                this.message = null;
225                this.cause = null;
226                this.messageReceived = false;
227            }
228    
229            @Override
230            public void messageReceived(IoSession ioSession, Object message) throws Exception {
231                if (LOG.isDebugEnabled()) {
232                    LOG.debug("Message received: " + message);
233                }
234                this.message = message;
235                messageReceived = true;
236                cause = null;
237                countDown();
238            }
239    
240            protected void countDown() {
241                CountDownLatch downLatch = latch;
242                if (downLatch != null) {
243                    downLatch.countDown();
244                }
245            }
246    
247            @Override
248            public void sessionClosed(IoSession session) throws Exception {
249                if (sync && !messageReceived) {
250                    // sync=true (InOut mode) so we expected a message as reply but did not get one before the session is closed
251                    if (LOG.isDebugEnabled()) {
252                        LOG.debug("Session closed but no message received from address: " + this.endpoint.getAddress());
253                    }
254                    // session was closed but no message received. This could be because the remote server had an internal error
255                    // and could not return a response. We should count down to stop waiting for a response
256                    countDown();
257                }
258            }
259    
260            @Override
261            public void exceptionCaught(IoSession ioSession, Throwable cause) {
262                LOG.error("Exception on receiving message from address: " + this.endpoint.getAddress()
263                        + " using connector: " + this.endpoint.getConnector(), cause);
264                this.message = null;
265                this.messageReceived = false;
266                this.cause = cause;
267                if (ioSession != null) {
268                    ioSession.close();
269                }
270            }
271    
272            public Throwable getCause() {
273                return this.cause;
274            }
275    
276            public Object getMessage() {
277                return this.message;
278            }
279    
280            public boolean isMessageReceived() {
281                return messageReceived;
282            }
283        }
284    
285    }