de.fun2code.google.cloudprint.push.PushReceiver.java Source code

Java tutorial

Introduction

Here is the source code for de.fun2code.google.cloudprint.push.PushReceiver.java

Source

package de.fun2code.google.cloudprint.push;

/*
Copyright 2011 Jochen Ruehl
    
Licensed 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.
*/

/*
 * This receiver does not use a XML SAX parser when reading the XMPP XML stream.
 * Therefore the code might not be applicable within a productive environment.
 * The main purpose is to demonstrate how Cloud Print XMPP push notifications are working.
 * 
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.apache.commons.codec.binary.Base64;

import de.fun2code.google.cloudprint.CloudPrintConnection;

/**
 * Receiver that handles the connection to Google Talk.
 * 
 * @see <a
 *      href="http://code.google.com/intl/de-DE/apis/cloudprint/docs/rawxmpp.html">Google
 *      Cloud Print - XMPP Handshake Flow</a> 
 * @author Jochen Ruehl
 *
 */
public class PushReceiver {
    private String username;

    private String authToken;

    private CountDownLatch latch;

    private List<PushListener> listeners;

    private PushNotificatinThread pushThread;

    private String resourceName;

    public static String SERVICE_NAME = "chromiumsync";

    public static String GTALK_SERVER = "talk.google.com";

    public static int GTALK_PORT = 5222;

    /*
     * Stanzas taken from
     * http://code.google.com/intl/de-DE/apis/cloudprint/docs/rawxmpp.html.
     * Names as in documentation.
     */
    private static String STREAM_START = "<stream:stream to=\"gmail.com\" xml:lang=\"en\" version=\"1.0\" xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\">";

    private static String STREAM_START_RESULT = "</stream:features>";

    private static String STREAM_END = "</stream:stream> ";

    private static String STANZA_C = "<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>";

    private static String STANZA_C_RESULT = "xmpp-tls\"/>";

    private static String STANZA_H = "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"X-GOOGLE-TOKEN\" auth:service=\"chromiumsync\" auth:allow-generated-jid=\"true\" auth:client-uses-full-bind-result=\"true\" xmlns:auth=\"http://www.google.com/talk/protocol/auth\">[credential]</auth>";

    private static String STANZA_H_RESULT = "xmpp-sasl\"/>";

    private static String STANZA_L = "<iq type=\"set\" id=\"0\"><bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"><resource>[resource name]</resource></bind></iq>";

    private static String STANZA_L_RESULT = "</iq>";

    private static String STANZA_N = "<iq type=\"set\" id=\"1\"><session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/></iq>";

    private static String STANZA_N_RESULT = "/>";

    private static String STANZA_P = "<iq type=\"set\" to=\"[bare jid]\" id=\"3\"><subscribe xmlns=\"google:push\"><item channel=\"cloudprint.google.com\" from=\"cloudprint.google.com\"/></subscribe></iq>";

    private static String STANZA_P_RESULT = "type=\"result\"/>";

    private static String PUSH_MESSAGE_RESULT = "</message>";

    /**
     * Constructor
     * 
     * @param resourceName   Descriptive name of the ressource.
     */
    public PushReceiver(String resourceName) {
        this.resourceName = resourceName != null ? resourceName : "";
        latch = new CountDownLatch(1);
        listeners = new ArrayList<PushListener>();
    }

    /**
     * Connects to Google Talk.
     * 
     * @param username   The username (Google Mail Address).
     * @param passwd   The Google Account passord.
     * @return   <tt>true</tt> on success, othewise <tt>false</tt>.
     */
    public boolean connectPlain(String username, String passwd) {
        if (pushThread != null) {
            return pushThread.isRunning();
        }

        this.username = username;
        authToken = getAuthToken(username, passwd);

        pushThread = new PushNotificatinThread();
        pushThread.start();

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return pushThread.isRunning();

    }

    /**
     * Connects to Google Talk.
     * 
     * @param username    The username (Google Mail Address).
     * @param authToken   Google Auth Token for Service <tt>SERVICE_NAME (chromiumsync)</tt> to use.
     * @return   <tt>true</tt> on success, othewise <tt>false</tt>.
     */
    public boolean connectWithToken(String username, String authToken) {
        if (pushThread != null) {
            return pushThread.isRunning();
        }

        this.username = username;
        this.authToken = authToken;

        pushThread = new PushNotificatinThread();
        pushThread.start();

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return pushThread.isRunning();
    }

    /**
     * Disconnects from Google Talk.
     */
    public void disconnect() {
        if (pushThread != null) {
            pushThread.terminate();
            pushThread = null;
        }
    }

    /**
     * Checks if the connection to Google Talk still exist.
     * 
     * @return   <tt>true</tt> on success, othewise <tt>false</tt>.
     */
    public boolean isConnected() {
        if (pushThread != null && pushThread.isAlive()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Gets the Google Auth Token.
     * 
     * @param username   The username to use.
     * @param passwd   The password to use.
     * @return
     */
    private String getAuthToken(String username, String passwd) {
        CloudPrintConnection gcp = new CloudPrintConnection();
        gcp.connect(username, passwd, SERVICE_NAME, "", null);
        String authToken = gcp.getAuthToken();
        gcp.disconnect();

        return authToken;
    }

    /**
     * Adds a listener to the list of listeners.
     * 
     * @param listener   Listener to add.
     */
    public void addListener(PushListener listener) {
        listeners.add(listener);
    }

    /**
     * Removes a listener from the list of listeners.
     * 
     * @param listener   Listener to remove.
     */
    public void removeListener(PushListener listener) {
        listeners.remove(listener);
    }

    /**
     * The Thread that connects to Google Talk and waits for Push Notifications.
     * 
     * @author Jochen Ruehl
     *
     */
    class PushNotificatinThread extends Thread {
        private boolean running = false;

        private Socket socket = null;

        @Override
        public void run() {
            try {
                socket = new Socket(GTALK_SERVER, GTALK_PORT);
                OutputStream out = socket.getOutputStream();
                InputStream in = socket.getInputStream();

                String line;

                // a
                out.write((STREAM_START).getBytes());
                // b
                line = nextResponse(in, STREAM_START_RESULT);
                // c
                out.write((STANZA_C).getBytes());
                // d
                line = nextResponse(in, STANZA_C_RESULT);
                // TLS Negotiation
                SSLSocket sslSocket = startTLSHandshake(socket);
                InputStream inSsl = sslSocket.getInputStream();
                OutputStream outSsl = sslSocket.getOutputStream();
                // e
                outSsl.write((STREAM_START).getBytes());
                // f
                line = nextResponse(inSsl, STREAM_START_RESULT);
                // Build credential
                String credential = Base64.encodeBase64String(("\0" + username + "\0" + authToken).getBytes());
                // h
                String authStanza = STANZA_H.replace("[credential]", "\n" + credential);
                outSsl.write((authStanza).getBytes());
                // i
                line = nextResponse(inSsl, STANZA_H_RESULT);
                // SASL Authentication Handshake End
                // j
                outSsl.write((STREAM_START).getBytes());
                // k
                line = nextResponse(inSsl, STREAM_START_RESULT);
                // l
                String stanza_l = STANZA_L.replace("[resource name]", resourceName);
                outSsl.write((stanza_l).getBytes());
                // m
                line = nextResponse(inSsl, STANZA_L_RESULT);
                // Get bare JID
                String bareJid = line.replaceAll(".*<jid>(.*?)/.*", "$1");
                // n
                outSsl.write((STANZA_N).getBytes());
                // o
                line = nextResponse(inSsl, STANZA_N_RESULT);
                // p
                String stanza_p = STANZA_P.replace("[bare jid]", bareJid);
                outSsl.write((stanza_p).getBytes());
                // q
                line = nextResponse(inSsl, STANZA_P_RESULT);

                running = true;
                latch.countDown();

                // Notify listeners
                for (PushListener listener : listeners) {
                    listener.onConnect();
                }

                // Waiting for Push Notifications
                while (true) {
                    line = nextResponse(inSsl, PUSH_MESSAGE_RESULT);
                    String printerId = new String(
                            Base64.decodeBase64(line.replaceAll(".*<push:data>(.*?)</push:data>.*", "$1")));

                    // Notify listeners
                    for (PushListener listener : listeners) {
                        listener.onReceive(printerId);
                    }
                }
            } catch (Exception e) {
                if (running == true) {
                    // Notify listeners
                    for (PushListener listener : listeners) {
                        listener.onDisconnect();
                    }
                }

                running = false;

                if (!(e instanceof java.net.SocketException)) {
                    e.printStackTrace();
                }
                latch.countDown();

            }

        }

        public void terminate() {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        public boolean isRunning() {
            return running;
        }
    }

    /*
     * XMPP Methods
     */

    /**
     * Reads the next response from the input stream until <tt>readUntil</tt>
     * is found
     * 
     * @param in
     *            The Input Stream.
     * @param readUntil
     *            Read until this String.
     * @return The String result.
     * @throws IOException
     */
    public static String nextResponse(InputStream in, String readUntil) throws IOException {
        String result = "";
        int c;
        while ((c = in.read()) != -1) {
            result += (char) c;
            if (result.endsWith(readUntil))
                break;
        }

        return result;
    }

    /**
     * Sets up the SSL socket for use with XMPP.
     * 
     * @param socket
     *            the socket to do TLS over.
     * @return   The SSL Socket.
     * @throws IOException
     */
    public static SSLSocket setupSSLSocket(Socket socket) throws NoSuchAlgorithmException, KeyManagementException,
            KeyStoreException, UnrecoverableKeyException, IOException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        TrustManagerFactory tfactory = TrustManagerFactory.getInstance("SunPKIX");
        tfactory.init(keyStore);
        SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        SSLSocket sslsocket = (SSLSocket) factory.createSocket(socket, socket.getInetAddress().getHostAddress(),
                socket.getPort(), true);
        sslsocket.setUseClientMode(true);
        return sslsocket;
    }

    /**
     * Starts the TSL (SSL) handshake.
     * 
     * @param socket   The Socket to use.
     * @return The SSL Socket.
     * @throws KeyManagementException
     * @throws NoSuchAlgorithmException
     * @throws UnrecoverableKeyException
     * @throws KeyStoreException
     * @throws IOException
     */
    public static SSLSocket startTLSHandshake(Socket socket) throws KeyManagementException,
            NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, IOException {
        SSLSocket sslsocket = setupSSLSocket(socket);
        sslsocket.startHandshake();
        return sslsocket;
    }
}