org.codehaus.stomp.jms.ProtocolConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.stomp.jms.ProtocolConverter.java

Source

/**
 *
 * 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.codehaus.stomp.jms;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.XAConnection;
import javax.jms.XAConnectionFactory;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.xa.Xid;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.stomp.ProtocolException;
import org.codehaus.stomp.Stomp;
import org.codehaus.stomp.StompFrame;
import org.codehaus.stomp.StompFrameError;
import org.codehaus.stomp.tcp.TcpTransport;
import org.jboss.narayana.rest.bridge.inbound.InboundBridge;
import org.jboss.narayana.rest.bridge.inbound.InboundBridgeManager;

/**
 * A protocol switch between JMS and Stomp
 *
 * @author <a href="http://people.apache.org/~jstrachan/">James Strachan</a>
 * @author <a href="http://hiramchirino.com">chirino</a>
 */
public class ProtocolConverter {
    private static final transient Log log = LogFactory.getLog(ProtocolConverter.class);
    private final TcpTransport tcpTransport;
    private ConnectionFactory noneXAConnectionFactory;
    private XAConnectionFactory xaConnectionFactory;
    private StompSession noneXaSession;
    private Map<Xid, StompSession> xaSessions = new ConcurrentHashMap<Xid, StompSession>();

    private TransactionManager tm;
    private String login;
    private String passcode;
    private String clientId;
    private InitialContext initialContext;
    private boolean closed;

    public ProtocolConverter(InitialContext initialContext, ConnectionFactory connectionFactory,
            XAConnectionFactory xaConnectionFactory, TcpTransport outputHandler) throws NamingException {
        this.noneXAConnectionFactory = connectionFactory;
        this.xaConnectionFactory = xaConnectionFactory;
        this.tcpTransport = outputHandler;
        this.initialContext = initialContext;
        tm = (TransactionManager) initialContext.lookup("java:/TransactionManager");
        outputHandler.setProtocolConverter(this);
    }

    public void close() {
        if (!closed) {
            try {
                // First close the XA sessions
                Iterator<StompSession> iterator = xaSessions.values().iterator();
                while (iterator.hasNext()) {
                    StompSession xaSession = iterator.next();
                    try {
                        xaSession.close();
                    } catch (JMSException e) {
                        log.error("Could not close XASession: " + e);
                    }
                }
                xaSessions.clear();
                xaSessions = null;
            } finally {
                if (noneXaSession != null) {
                    try {
                        noneXaSession.close();
                    } catch (JMSException e) {
                        log.error("Could not close none XASession: " + e);
                    }
                    noneXaSession = null;
                }
            }
            closed = true;
        }
    }

    /**
     * Process a Stomp Frame
     *
     * @throws IOException
     */
    public void onStompFrame(StompFrame command) throws IOException {
        try {
            if (log.isDebugEnabled()) {
                log.debug(">>>> " + command.getAction() + " headers: " + command.getHeaders());
            }

            if (command.getClass() == StompFrameError.class) {
                throw ((StompFrameError) command).getException();
            }

            if (closed) {
                log.error("Connection is closed: " + this);
                throw new ProtocolException("Connection is closed: " + this);
            }

            String action = command.getAction();
            if (action.startsWith(Stomp.Commands.SEND)) {
                onStompSend(command);
            } else if (action.startsWith(Stomp.Commands.RECEIVE)) {
                onStompReceive(command);
            } else if (action.startsWith(Stomp.Commands.ACK)) {
                onStompAck(command);
            } else if (action.startsWith(Stomp.Commands.SUBSCRIBE)) {
                onStompSubscribe(command);
            } else if (action.startsWith(Stomp.Commands.UNSUBSCRIBE)) {
                onStompUnsubscribe(command);
            } else if (action.startsWith(Stomp.Commands.CONNECT)) {
                onStompConnect(command);
            } else if (action.startsWith(Stomp.Commands.DISCONNECT)) {
                onStompDisconnect(command);
            } else {
                throw new ProtocolException("Unknown STOMP action: " + action);
            }

            if (log.isDebugEnabled()) {
                log.debug(">>>> " + command.getAction() + " headers: " + command.getHeaders() + " >> done");
            }
        } catch (Exception e) {
            log.debug("Caught an exception: ", e);
            // Let the stomp client know about any protocol errors.
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PrintWriter stream = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8));
            e.printStackTrace(stream);
            stream.close();

            Map<String, Object> headers = new HashMap<String, Object>();
            headers.put(Stomp.Headers.Error.MESSAGE, e.getMessage());

            final String receiptId = (String) command.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
            if (receiptId != null) {
                headers.put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
            }

            StompFrame errorMessage = new StompFrame(Stomp.Responses.ERROR, headers, baos.toByteArray());
            sendToStomp(errorMessage);

            // TODO need to do anything else? Should we close the connection?
        }
    }

    // Implemenation methods
    // -------------------------------------------------------------------------
    protected void onStompConnect(StompFrame command) throws IOException, JMSException {
        if (noneXaSession != null) {
            throw new ProtocolException("Already connected.");
        }

        Map<String, Object> headers = command.getHeaders();
        login = (String) headers.get(Stomp.Headers.Connect.LOGIN);
        passcode = (String) headers.get(Stomp.Headers.Connect.PASSCODE);
        clientId = (String) headers.get(Stomp.Headers.Connect.CLIENT_ID);

        Connection noneXaConnection;
        if (login != null) {
            noneXaConnection = noneXAConnectionFactory.createConnection(login, passcode);
        } else {
            noneXaConnection = noneXAConnectionFactory.createConnection();
        }
        if (clientId != null) {
            noneXaConnection.setClientID(clientId);
        }

        noneXaConnection.start();

        Session session = noneXaConnection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
        if (log.isDebugEnabled()) {
            log.debug("Created session with ack mode: " + session.getAcknowledgeMode());
        }
        this.noneXaSession = new StompSession(initialContext, this, session, noneXaConnection);

        Map<String, Object> responseHeaders = new HashMap<String, Object>();

        responseHeaders.put(Stomp.Headers.Connected.SESSION, clientId);
        String requestId = (String) headers.get(Stomp.Headers.Connect.REQUEST_ID);
        if (requestId == null) {
            // TODO legacy
            requestId = (String) headers.get(Stomp.Headers.RECEIPT_REQUESTED);
        }
        if (requestId != null) {
            // TODO legacy
            responseHeaders.put(Stomp.Headers.Connected.RESPONSE_ID, requestId);
            responseHeaders.put(Stomp.Headers.Response.RECEIPT_ID, requestId);
        }

        StompFrame sc = new StompFrame();
        sc.setAction(Stomp.Responses.CONNECTED);
        sc.setHeaders(responseHeaders);
        sendToStomp(sc);
    }

    protected void onStompDisconnect(StompFrame command)
            throws JMSException, InterruptedException, IOException, URISyntaxException {
        checkConnected();
        close();
        sendResponse(command);
    }

    protected void onStompSend(StompFrame command)
            throws IllegalStateException, SystemException, JMSException, NamingException, IOException {
        checkConnected();

        Map<String, Object> headers = command.getHeaders();
        String xid = (String) headers.get("messagexid");

        if (xid != null) {
            if (xid.startsWith("IOR")) {
                log.error("OTS Transaction Not Supported");
            } else if (xid.startsWith("http")) {
                log.trace("RTS Transaction was propagated: " + xid);

                String enlistmentUrl = xid;
                final InboundBridge inboundBridge = InboundBridgeManager.getInstance()
                        .createInboundBridge(enlistmentUrl);
                log.trace("Start inboundBridge");
                inboundBridge.start();
                StompSession session = getXASession(inboundBridge.getXid());
                session.sendToJms(command);
                inboundBridge.stop();
                log.trace("Stop inboundBridge");
            } else {
                log.error(xid + " is not OTS or RTS transaction");
            }
        } else {
            String destinationName = (String) headers.get(Stomp.Headers.Send.DESTINATION);
            log.trace("WAS NULL XID: " + destinationName);
            noneXaSession.sendToJms(command);
            log.trace("Sent to JMS: " + destinationName);
        }
        sendResponse(command);
        log.trace("Sent Response");
    }

    protected void onStompReceive(StompFrame command)
            throws IllegalStateException, SystemException, JMSException, NamingException, IOException {
        checkConnected();

        Map<String, Object> headers = command.getHeaders();
        String destinationName = (String) headers.remove(Stomp.Headers.Send.DESTINATION);
        String xid = (String) headers.get("messagexid");
        Message msg = null;
        StompSession session = null;
        if (xid != null) {
            if (xid.startsWith("IOR")) {
                log.error("OTS Transaction Not Supported");
            } else if (xid.startsWith("http")) {
                log.trace("RTS Transaction was propagated: " + xid);

                String enlistmentUrl = xid;
                final InboundBridge inboundBridge = InboundBridgeManager.getInstance()
                        .createInboundBridge(enlistmentUrl);
                log.trace("Start inboundBridge");
                inboundBridge.start();
                session = getXASession(inboundBridge.getXid());
                msg = session.receiveFromJms(destinationName, headers);
                inboundBridge.stop();
                log.trace("Stop inboundBridge");

            } else {
                log.error(xid + " is not OTS or RTS transaction");
            }
        } else {
            log.trace("WAS NULL XID");
            session = noneXaSession;
            msg = session.receiveFromJms(destinationName, headers);

            log.trace("Received from JMS");
        }

        StompFrame sf;
        if (msg == null) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PrintWriter stream = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8));
            stream.print("No messages available");
            stream.close();

            Map<String, Object> eheaders = new HashMap<String, Object>();
            eheaders.put(Stomp.Headers.Error.MESSAGE, "timeout");

            sf = new StompFrame(Stomp.Responses.ERROR, eheaders, baos.toByteArray());
        } else {
            // Don't use sendResponse since it uses Stomp.Responses.RECEIPT as the action
            // which only allows zero length message bodies, Stomp.Responses.MESSAGE is correct:
            sf = session.convertMessage(msg);
        }

        if (headers.containsKey(Stomp.Headers.RECEIPT_REQUESTED))
            sf.getHeaders().put(Stomp.Headers.Response.RECEIPT_ID, headers.get(Stomp.Headers.RECEIPT_REQUESTED));

        sendToStomp(sf);
    }

    protected void onStompSubscribe(StompFrame command) throws JMSException, NamingException, IOException {
        checkConnected();

        Map<String, Object> headers = command.getHeaders();

        String subscriptionId = (String) headers.get(Stomp.Headers.Subscribe.ID);
        if (subscriptionId == null) {
            subscriptionId = createSubscriptionId(headers);
        }

        // We know this is going to be none-XA as the XA receive is handled in onStompReceive
        noneXaSession.subscribe(subscriptionId, command);
        sendResponse(command);
    }

    protected void onStompUnsubscribe(StompFrame command) throws JMSException, IOException {
        checkConnected();
        Map<String, Object> headers = command.getHeaders();

        String destinationName = (String) headers.get(Stomp.Headers.Unsubscribe.DESTINATION);
        String subscriptionId = (String) headers.get(Stomp.Headers.Unsubscribe.ID);

        if (subscriptionId == null) {
            if (destinationName == null) {
                throw new ProtocolException(
                        "Must specify the subscriptionId or the destination you are unsubscribing from");
            }
            subscriptionId = createSubscriptionId(headers);
        }

        noneXaSession.unsubscribe(subscriptionId);
        sendResponse(command);
    }

    protected void onStompAck(StompFrame command) throws JMSException, IOException {
        checkConnected();

        // We know this is none XA
        StompSession session = noneXaSession;
        if (session == null) {
            throw new ProtocolException("None XA session was not stopped");
        }

        log.debug("Locking session to restart it");
        // Dont allow the session to deliver any more messages until after we have acked the clients ack
        synchronized (session) {
            // Allow another message to be consumed
            session.resume();
            sendResponse(command);
        }
        log.debug("Session started");
    }

    protected void checkConnected() throws ProtocolException {
        if (noneXaSession == null) {
            throw new ProtocolException("Not connected.");
        }
    }

    /**
     * Auto-create a subscription ID using the destination
     */
    protected String createSubscriptionId(Map<String, Object> headers) {
        return "/subscription-to/" + headers.get(Stomp.Headers.Subscribe.DESTINATION);
    }

    protected StompSession getXASession(Xid xid) throws JMSException {
        StompSession xaSession = xaSessions.get(xid);
        if (xaSession == null) {

            XAConnection xaConnection;
            if (login != null) {
                xaConnection = xaConnectionFactory.createXAConnection(login, passcode);
            } else {
                xaConnection = xaConnectionFactory.createXAConnection();
            }
            if (clientId != null) {
                xaConnection.setClientID(clientId);
            }
            xaConnection.start();
            Session session = xaConnection.createXASession();
            if (log.isDebugEnabled()) {
                log.debug("Created XA session");
            }
            xaSession = new StompSession(initialContext, this, session, xaConnection);
            log.trace("Created XA Session");
            xaSessions.put(xid, xaSession);
        } else {
            log.trace("Returned existing XA session");
        }
        return xaSession;
    }

    protected void sendResponse(StompFrame command) throws IOException {
        final String receiptId = (String) command.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
        // A response may not be needed.
        if (receiptId != null) {
            StompFrame sc = new StompFrame();
            sc.setAction(Stomp.Responses.RECEIPT);
            sc.setHeaders(new HashMap<String, Object>(1));
            sc.getHeaders().put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
            sendToStomp(sc);
        } else {
            log.trace("No receipt required");
        }
    }

    protected void sendToStomp(StompFrame frame) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("<<<< " + frame.getAction() + " headers: " + frame.getHeaders());
        }
        log.debug("Locking output handler to ensure that we don't mux signals");
        synchronized (tcpTransport) {
            tcpTransport.onStompFrame(frame);
        }
        if (log.isDebugEnabled()) {
            log.debug("<<<< " + frame.getAction() + " headers: " + frame.getHeaders() + " << done");
        }
    }

}