/** * 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 io.hawtjms.provider.stomp; import static io.hawtjms.provider.stomp.StompConstants.DISCONNECT; import io.hawtjms.jms.JmsDestination; import io.hawtjms.jms.message.JmsInboundMessageDispatch; import io.hawtjms.jms.message.JmsMessageFactory; import io.hawtjms.jms.message.JmsOutboundMessageDispatch; import io.hawtjms.jms.meta.JmsConnectionInfo; import io.hawtjms.jms.meta.JmsConsumerId; import io.hawtjms.jms.meta.JmsConsumerInfo; import io.hawtjms.jms.meta.JmsDefaultResourceVisitor; import io.hawtjms.jms.meta.JmsProducerId; import io.hawtjms.jms.meta.JmsProducerInfo; import io.hawtjms.jms.meta.JmsResource; import io.hawtjms.jms.meta.JmsSessionId; import io.hawtjms.jms.meta.JmsSessionInfo; import io.hawtjms.jms.meta.JmsTransactionInfo; import io.hawtjms.provider.AbstractAsyncProvider; import io.hawtjms.provider.AsyncResult; import io.hawtjms.provider.ProviderConstants.ACK_TYPE; import io.hawtjms.provider.ProviderRequest; import io.hawtjms.transports.TcpTransport; import io.hawtjms.transports.Transport; import io.hawtjms.transports.TransportListener; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; import javax.jms.JMSException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.vertx.java.core.buffer.Buffer; /** * Async Provider implementation for the STOMP protocol. */ public class StompProvider extends AbstractAsyncProvider implements TransportListener { private static final Logger LOG = LoggerFactory.getLogger(StompProvider.class); private final StompCodec codec = new StompCodec(); private Transport transport; private StompConnection connection; private long closeTimeout = JmsConnectionInfo.DEFAULT_CLOSE_TIMEOUT; public StompProvider(URI remoteURI) { super(remoteURI); } @Override public void connect() throws IOException { checkClosed(); transport = createTransport(getRemoteURI()); transport.connect(); } @Override public void close() { if (closed.compareAndSet(false, true)) { final ProviderRequest<Void> request = new ProviderRequest<Void>(); serializer.execute(new Runnable() { @Override public void run() { try { // TODO - We should wait, but for now lets just do it async. StompFrame disconnect = new StompFrame(DISCONNECT); transport.send(codec.encode(disconnect)); } catch (Exception e) { LOG.debug("Caught exception while closing proton connection"); } finally { if (transport != null) { try { transport.close(); } catch (Exception e) { LOG.debug("Cuaght exception while closing down Transport: {}", e.getMessage()); } } request.onSuccess(); } } }); try { if (closeTimeout < 0) { request.getResponse(); } else { request.getResponse(closeTimeout, TimeUnit.MILLISECONDS); } } catch (IOException e) { LOG.warn("Error caught while closing Provider: ", e.getMessage()); } finally { if (serializer != null) { serializer.shutdown(); } } } } @Override public void create(final JmsResource resource, final AsyncResult<Void> request) throws IOException, JMSException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); resource.visit(new JmsDefaultResourceVisitor() { @Override public void processConsumerInfo(JmsConsumerInfo consumerInfo) throws Exception { StompSession session = connection.getSession(consumerInfo.getParentId()); session.createConsumer(consumerInfo, request); } @Override public void processProducerInfo(JmsProducerInfo producerInfo) throws Exception { StompSession session = connection.getSession(producerInfo.getParentId()); session.createProducer(producerInfo, request); } @Override public void processSessionInfo(JmsSessionInfo sessionInfo) throws Exception { connection.createSession(sessionInfo, request); } @Override public void processConnectionInfo(JmsConnectionInfo connectionInfo) throws Exception { closeTimeout = connectionInfo.getCloseTimeout(); connection = new StompConnection(StompProvider.this, connectionInfo); connection.connect(request); } @Override public void processDestination(JmsDestination destination) throws Exception { // The generated names from the JMS framework are valid so we // just use those and apply the correct prefix on send etc. request.onSuccess(); } @Override public void processTransactionInfo(JmsTransactionInfo transactionInfo) throws Exception { request.onFailure(new JMSException("Not implemented")); } }); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void start(final JmsResource resource, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); resource.visit(new JmsDefaultResourceVisitor() { @Override public void processConsumerInfo(JmsConsumerInfo consumerInfo) throws Exception { StompSession session = connection.getSession(consumerInfo.getParentId()); StompConsumer consumer = session.getConsumer(consumerInfo.getConsumerId()); consumer.start(); request.onSuccess(); } }); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void destroy(final JmsResource resource, final AsyncResult<Void> request) throws IOException, JMSException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); resource.visit(new JmsDefaultResourceVisitor() { @Override public void processConsumerInfo(JmsConsumerInfo consumerInfo) throws Exception { StompSession session = connection.getSession(consumerInfo.getParentId()); StompConsumer consumer = session.getConsumer(consumerInfo.getConsumerId()); consumer.close(request); } @Override public void processProducerInfo(JmsProducerInfo producerInfo) throws Exception { StompSession session = connection.getSession(producerInfo.getParentId()); StompProducer producer = session.getProducer(producerInfo.getProducerId()); producer.close(request); } @Override public void processSessionInfo(JmsSessionInfo sessionInfo) throws Exception { StompSession session = connection.getSession(sessionInfo.getSessionId()); session.close(request); } @Override public void processConnectionInfo(JmsConnectionInfo connectionInfo) throws Exception { // TODO - Instruct Connection to close. // TODO - If we send the disconnect frame here we need to wait for a timeout // period an cancel since broker might close socket early. // probably better not to do that here. request.onSuccess(); } @Override public void processDestination(JmsDestination destination) throws Exception { request.onFailure(new JMSException("STOMP does not support destination remove.")); } @Override public void processTransactionInfo(JmsTransactionInfo transactionInfo) throws Exception { request.onFailure(new JMSException("Not implemented")); } }); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void send(final JmsOutboundMessageDispatch envelope, final AsyncResult<Void> request) throws IOException, JMSException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); JmsProducerId producerId = envelope.getProducerId(); StompProducer producer = connection.getProducer(producerId); producer.send(envelope, request); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void acknowledge(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); StompSession amqpSession = connection.getSession(sessionId); amqpSession.acknowledge(request); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void acknowledge(final JmsInboundMessageDispatch envelope, final ACK_TYPE ackType, final AsyncResult<Void> request) throws IOException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); JmsConsumerId consumerId = envelope.getConsumerId(); StompConsumer consumer = connection.getConsumer(consumerId); consumer.acknowledge(envelope, ackType, request); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void commit(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException, JMSException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); StompSession session = connection.getSession(sessionId); session.commit(request); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void rollback(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException, JMSException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); StompSession session = connection.getSession(sessionId); session.rollback(request); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void recover(final JmsSessionId sessionId, final AsyncResult<Void> request) throws IOException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); StompSession session = connection.getSession(sessionId); session.recover(); request.onSuccess(); } catch (Exception error) { request.onFailure(error); } } }); } @Override public void pull(final JmsConsumerId consumerId, long timeout, final AsyncResult<Void> request) throws IOException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); StompSession session = connection.getSession(consumerId.getParentId()); StompConsumer consumer = session.getConsumer(consumerId); /* * Client will trying to perform a pull in order to allow a browser to * check it's state and retrieve any pending messages. We don't error * in this case as we know in STOMP if we are done or not by an end of * browse but other protocols might need the kick. */ if (consumer.isBrowser()) { request.onSuccess(); } else { request.onFailure(new UnsupportedOperationException("STOMP consumer cannot pull messages.")); } } catch (Exception error) { request.onFailure(error); } } }); } @Override public void unsubscribe(final String subscription, final AsyncResult<Void> request) throws IOException, JMSException, UnsupportedOperationException { checkClosed(); serializer.execute(new Runnable() { @Override public void run() { try { checkClosed(); // TODO - Can we do this with STOMP with just this info? request.onSuccess(); } catch (Exception error) { request.onFailure(error); } } }); } /** * Provides an extension point for subclasses to insert other types of transports such * as SSL etc. * * @param remoteLocation * The remote location where the transport should attempt to connect. * * @return the newly created transport instance. */ protected Transport createTransport(URI remoteLocation) { return new TcpTransport(this, remoteLocation); } /** * Encodes and sends the given STOMP frame using the provider's Transport. * This method must be called from an job running on the serializer thread. * * @param frame * the STOMP frame instance to send. * * @throws IOException if an error occurs while encoding or sending the frame. */ protected void send(StompFrame frame) throws IOException { ByteBuffer connect = codec.encode(frame); transport.send(connect); } @Override public void onData(Buffer incoming) { // Create our own copy since we will process later. final ByteBuffer source = ByteBuffer.wrap(incoming.getBytes()); serializer.execute(new Runnable() { @Override public void run() { LOG.trace("Received from Broker {} bytes:", source.remaining()); try { do { StompFrame frame = codec.decode(source); if (frame != null) { connection.processFrame(frame); } } while (source.hasRemaining()); } catch (Exception e) { LOG.warn("Caught exception while processing new data: {}", e.getMessage()); LOG.trace("Exception detail: ", e); fireProviderException(e); } } }); } /** * Callback method for the Transport to report connection errors. When called * the method will queue a new task to fire the failure error back to the listener. * * @param error * the error that causes the transport to fail. */ @Override public void onTransportError(final Throwable error) { if (!closed.get()) { serializer.execute(new Runnable() { @Override public void run() { LOG.info("Transport failed: {}", error.getMessage()); if (!closed.get()) { fireProviderException(error); } } }); } } /** * Callback method for the Transport to report that the underlying connection * has closed. When called this method will queue a new task that will check for * the closed state on this transport and if not closed then an exception is raied * to the registered ProviderListener to indicate connection loss. */ @Override public void onTransportClosed() { if (!closed.get()) { serializer.execute(new Runnable() { @Override public void run() { LOG.info("Transport connection remotely closed:"); if (!closed.get()) { fireProviderException(new IOException("Connection remotely closed.")); } } }); } } //------------- Property Getters / Setters -------------------------------// @Override public JmsMessageFactory getMessageFactory() { if (connection == null) { throw new RuntimeException("Message Factory is not accessible when not connected."); } return connection.getMessageFactory(); } public long getCloseTimeout() { return this.closeTimeout; } public void setCloseTimeout(long closeTimeout) { this.closeTimeout = closeTimeout; } }