GCM XMPP Server using Smack 4.1.0

B.A. picture B.A. · May 5, 2015 · Viewed 7.9k times · Source

I'm trying to adapt the example provided here for Smack 4.1.0. and getting a little confused.

Specifically I'm struggling to understand what the GcmPacketExtension should now extend, how the constructor should work and how the Providermanager.addExtensionProvider should be updated to tie in with it.

I'm sure someone must have done this before but I can't find any examples and I seem to be going round in circles using just the documentation.

Any help would be much appreciated, I'm sure the answer is very simple!

Current Code (is compiling but not running):

    static {

    ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE, new  ExtensionElementProvider<ExtensionElement>() {
        @Override
        public DefaultExtensionElement parse(XmlPullParser parser,int initialDepth) throws org.xmlpull.v1.XmlPullParserException,
        IOException {
            String json = parser.nextText();
            return new GcmPacketExtension(json);
        }
    });
}

and:

private static final class GcmPacketExtension extends DefaultExtensionElement   {

    private final String json;

    public GcmPacketExtension(String json) {
        super(GCM_ELEMENT_NAME, GCM_NAMESPACE);
        this.json = json;
    }

    public String getJson() {
        return json;
    }

    @Override
    public String toXML() {
        return String.format("<%s xmlns=\"%s\">%s</%s>",
                GCM_ELEMENT_NAME, GCM_NAMESPACE,
                StringUtils.escapeForXML(json), GCM_ELEMENT_NAME);
    }

    public Stanza toPacket() {
        Message message = new Message();
        message.addExtension(this);
        return message;
    }
}

Current exception:

Exception in thread "main" java.lang.NoClassDefFoundError: de/measite/minidns/DNSCache
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Unknown Source)
at org.jivesoftware.smack.SmackInitialization.loadSmackClass(SmackInitialization.java:213)
at org.jivesoftware.smack.SmackInitialization.parseClassesToLoad(SmackInitialization.java:193)
at org.jivesoftware.smack.SmackInitialization.processConfigFile(SmackInitialization.java:163)
at org.jivesoftware.smack.SmackInitialization.processConfigFile(SmackInitialization.java:148)
at org.jivesoftware.smack.SmackInitialization.<clinit>(SmackInitialization.java:116)
at org.jivesoftware.smack.SmackConfiguration.getVersion(SmackConfiguration.java:96)
at org.jivesoftware.smack.provider.ProviderManager.<clinit>(ProviderManager.java:121)
at SmackCcsClient.<clinit>(SmackCcsClient.java:58)
Caused by: java.lang.ClassNotFoundException: de.measite.minidns.DNSCache
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 10 more

Answer

B.A. picture B.A. · May 7, 2015

OK, so I managed to get it working after much reading and pain, so here is a VERY crude server implementation that actually works. Obviously not for production and feel free to correct anything that's wrong. I'm not saying this is the best way to do it but it does work. It will send a message and receive messages but only shows them in the log.

import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.DefaultExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.StringUtils;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.xmlpull.v1.XmlPullParser;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.roster.Roster;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;


import javax.net.ssl.SSLSocketFactory;

/**
 * Sample Smack implementation of a client for GCM Cloud Connection Server. This
 * code can be run as a standalone CCS client.
 *
 * <p>For illustration purposes only.
 */
public class SmackCcsClient {

    private static final Logger logger = Logger.getLogger("SmackCcsClient");

    private static final String GCM_SERVER = "gcm.googleapis.com";
    private static final int GCM_PORT = 5235;

    private static final String GCM_ELEMENT_NAME = "gcm";
    private static final String GCM_NAMESPACE = "google:mobile:data";

    private static final String YOUR_PROJECT_ID = "<your ID here>";
    private static final String YOUR_API_KEY = "<your API Key here>"; // your API Key
    private static final String YOUR_PHONE_REG_ID = "<your test phone's registration id here>";
    
    
    static {

        ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE, new  ExtensionElementProvider<ExtensionElement>() {
            @Override
            public DefaultExtensionElement parse(XmlPullParser parser,int initialDepth) throws org.xmlpull.v1.XmlPullParserException,
            IOException {
                String json = parser.nextText();
                return new GcmPacketExtension(json);
            }
        });
    }

    private XMPPTCPConnection connection;

    /**
     * Indicates whether the connection is in draining state, which means that it
     * will not accept any new downstream messages.
     */
    protected volatile boolean connectionDraining = false;

    /**
     * Sends a downstream message to GCM.
     *
     * @return true if the message has been successfully sent.
     */
    public boolean sendDownstreamMessage(String jsonRequest) throws
            NotConnectedException {
        if (!connectionDraining) {
            send(jsonRequest);
            return true;
        }
        logger.info("Dropping downstream message since the connection is draining");
        return false;
    }

    /**
     * Returns a random message id to uniquely identify a message.
     *
     * <p>Note: This is generated by a pseudo random number generator for
     * illustration purpose, and is not guaranteed to be unique.
     */
    public String nextMessageId() {
        return "m-" + UUID.randomUUID().toString();
    }

    /**
     * Sends a packet with contents provided.
     */
    protected void send(String jsonRequest) throws NotConnectedException {
        Stanza request = new GcmPacketExtension(jsonRequest).toPacket();
        connection.sendStanza(request);
    }

    /**
     * Handles an upstream data message from a device application.
     *
     * <p>This sample echo server sends an echo message back to the device.
     * Subclasses should override this method to properly process upstream messages.
     */
    protected void handleUpstreamMessage(Map<String, Object> jsonObject) {
        // PackageName of the application that sent this message.
        String category = (String) jsonObject.get("category");
        String from = (String) jsonObject.get("from");
        @SuppressWarnings("unchecked")
        Map<String, String> payload = (Map<String, String>) jsonObject.get("data");
        payload.put("ECHO", "Application: " + category);

        // Send an ECHO response back
        String echo = createJsonMessage(from, nextMessageId(), payload,
                "echo:CollapseKey", null, false);

        try {
            sendDownstreamMessage(echo);
        } catch (NotConnectedException e) {
            logger.log(Level.WARNING, "Not connected anymore, echo message is not sent", e);
        }
        
    }

    /**
     * Handles an ACK.
     *
     * <p>Logs a INFO message, but subclasses could override it to
     * properly handle ACKs.
     */
    protected void handleAckReceipt(Map<String, Object> jsonObject) {
        String messageId = (String) jsonObject.get("message_id");
        String from = (String) jsonObject.get("from");
        logger.log(Level.INFO, "handleAckReceipt() from: " + from + ",messageId: " + messageId);
    }

    /**
     * Handles a NACK.
     *
     * <p>Logs a INFO message, but subclasses could override it to
     * properly handle NACKs.
     */
    protected void handleNackReceipt(Map<String, Object> jsonObject) {
        String messageId = (String) jsonObject.get("message_id");
        String from = (String) jsonObject.get("from");
        logger.log(Level.INFO, "handleNackReceipt() from: " + from + ",messageId: " + messageId);
    }

    protected void handleControlMessage(Map<String, Object> jsonObject) {
        logger.log(Level.INFO, "handleControlMessage(): " + jsonObject);
        String controlType = (String) jsonObject.get("control_type");
        if ("CONNECTION_DRAINING".equals(controlType)) {
            connectionDraining = true;
        } else {
            logger.log(Level.INFO, "Unrecognized control type: %s. This could happen if new features are " + "added to the CCS protocol.",
                    controlType);
        }
    }

    /**
     * Creates a JSON encoded GCM message.
     *
     * @param to RegistrationId of the target device (Required).
     * @param messageId Unique messageId for which CCS sends an
     *         "ack/nack" (Required).
     * @param payload Message content intended for the application. (Optional).
     * @param collapseKey GCM collapse_key parameter (Optional).
     * @param timeToLive GCM time_to_live parameter (Optional).
     * @param delayWhileIdle GCM delay_while_idle parameter (Optional).
     * @return JSON encoded GCM message.
     */
    public static String createJsonMessage(String to, String messageId,
            Map<String, String> payload, String collapseKey, Long timeToLive,
            Boolean delayWhileIdle) {
        Map<String, Object> message = new HashMap<String, Object>();
        message.put("to", to);
        if (collapseKey != null) {
            message.put("collapse_key", collapseKey);
        }
        if (timeToLive != null) {
            message.put("time_to_live", timeToLive);
        }
        if (delayWhileIdle != null && delayWhileIdle) {
            message.put("delay_while_idle", true);
        }
      message.put("message_id", messageId);
      message.put("data", payload);
      return JSONValue.toJSONString(message);
    }

    /**
     * Creates a JSON encoded ACK message for an upstream message received
     * from an application.
     *
     * @param to RegistrationId of the device who sent the upstream message.
     * @param messageId messageId of the upstream message to be acknowledged to CCS.
     * @return JSON encoded ack.
     */
        protected static String createJsonAck(String to, String messageId) {
        Map<String, Object> message = new HashMap<String, Object>();
        message.put("message_type", "ack");
        message.put("to", to);
        message.put("message_id", messageId);
        return JSONValue.toJSONString(message);
    }

    /**
     * Connects to GCM Cloud Connection Server using the supplied credentials.
     *
     * @param senderId Your GCM project number
     * @param apiKey API Key of your project
     */
    public void connect(String senderId, String apiKey)
            throws XMPPException, IOException, SmackException {
    	XMPPTCPConnectionConfiguration config =
    			XMPPTCPConnectionConfiguration.builder()
    			.setServiceName(GCM_SERVER)
    		     .setHost(GCM_SERVER)
    		     .setCompressionEnabled(false)
    		     .setPort(GCM_PORT)
    		     .setConnectTimeout(30000)
    		     .setSecurityMode(SecurityMode.disabled)
    		     .setSendPresence(false)
    		     .setSocketFactory(SSLSocketFactory.getDefault())
    		    .build();
    	
        connection = new XMPPTCPConnection(config);
        
        //disable Roster as I don't think this is supported by GCM
        Roster roster = Roster.getInstanceFor(connection);
        roster.setRosterLoadedAtLogin(false);

        logger.info("Connecting...");
        connection.connect();

        connection.addConnectionListener(new LoggingConnectionListener());

        // Handle incoming packets
        connection.addAsyncStanzaListener(new MyStanzaListener() , new MyStanzaFilter() );

        // Log all outgoing packets
        connection.addPacketInterceptor(new MyStanzaInterceptor(), new MyStanzaFilter() );

        connection.login(senderId + "@gcm.googleapis.com" , apiKey);
        
    }
    
    private class MyStanzaFilter implements StanzaFilter
    {
    
			@Override
			public boolean accept(Stanza arg0) {
				// TODO Auto-generated method stub
				if(arg0.getClass() == Stanza.class )
					return true;
				else 
				{
					if(arg0.getTo()!= null)
						if(arg0.getTo().startsWith(YOUR_PROJECT_ID) )
							return true;
				
				}
				
				return false;
			}
    }
    
    private class MyStanzaListener implements StanzaListener{
		
        @Override
        public void processPacket(Stanza packet) {
            logger.log(Level.INFO, "Received: " + packet.toXML());
            Message incomingMessage = (Message) packet;
            GcmPacketExtension gcmPacket =
                    (GcmPacketExtension) incomingMessage.
                    getExtension(GCM_NAMESPACE);
            String json = gcmPacket.getJson();
            try {
                @SuppressWarnings("unchecked")
                Map<String, Object> jsonObject =
                        (Map<String, Object>) JSONValue.
                        parseWithException(json);

                // present for "ack"/"nack", null otherwise
                Object messageType = jsonObject.get("message_type");

                if (messageType == null) {
                    // Normal upstream data message
                    handleUpstreamMessage(jsonObject);

                    // Send ACK to CCS
                    String messageId = (String) jsonObject.get("message_id");
                    String from = (String) jsonObject.get("from");
                    String ack = createJsonAck(from, messageId);
                    send(ack);
                } else if ("ack".equals(messageType.toString())) {
                      // Process Ack
                      handleAckReceipt(jsonObject);
                } else if ("nack".equals(messageType.toString())) {
                      // Process Nack
                      handleNackReceipt(jsonObject);
                } else if ("control".equals(messageType.toString())) {
                      // Process control message
                      handleControlMessage(jsonObject);
                } else {
                      logger.log(Level.WARNING,
                              "Unrecognized message type (%s)",
                              messageType.toString());
                }
            } catch (ParseException e) {
                logger.log(Level.SEVERE, "Error parsing JSON " + json, e);
            } catch (Exception e) {
                logger.log(Level.SEVERE, "Failed to process packet", e);
            }
        }
    
    }
    
    private class MyStanzaInterceptor implements StanzaListener
    {
    	@Override
        public void processPacket(Stanza packet) {
    		logger.log(Level.INFO, "Sent: {0}", packet.toXML());
    	}
    	
    }
    

    public static void main(String[] args) throws Exception {
        
        SmackCcsClient ccsClient = new SmackCcsClient();

        ccsClient.connect(YOUR_PROJECT_ID, YOUR_API_KEY);
        
        // Send a sample hello downstream message to a device.
        String messageId = ccsClient.nextMessageId();
        Map<String, String> payload = new HashMap<String, String>();
        payload.put("Message", "Ahha, it works!");
        payload.put("CCS", "Dummy Message");
        payload.put("EmbeddedMessageId", messageId);
        String collapseKey = "sample";
        Long timeToLive = 10000L;
        String message = createJsonMessage(YOUR_PHONE_REG_ID, messageId, payload,
                collapseKey, timeToLive, true);

        ccsClient.sendDownstreamMessage(message);
        logger.info("Message sent.");
        
        //crude loop to keep connection open for receiving messages
        while(true)
        {;}
    }

    /**
     * XMPP Packet Extension for GCM Cloud Connection Server.
     */
    private static final class GcmPacketExtension extends DefaultExtensionElement   {

        private final String json;

        public GcmPacketExtension(String json) {
        	super(GCM_ELEMENT_NAME, GCM_NAMESPACE);
            this.json = json;
        }

        public String getJson() {
            return json;
        }

        @Override
        public String toXML() {
            return String.format("<%s xmlns=\"%s\">%s</%s>",
                    GCM_ELEMENT_NAME, GCM_NAMESPACE,
                    StringUtils.escapeForXML(json), GCM_ELEMENT_NAME);
        }

        public Stanza toPacket() {
            Message message = new Message();
            message.addExtension(this);
            return message;
        }
    }

    private static final class LoggingConnectionListener
            implements ConnectionListener {

        @Override
        public void connected(XMPPConnection xmppConnection) {
            logger.info("Connected.");
        }
        

        @Override
        public void reconnectionSuccessful() {
            logger.info("Reconnecting..");
        }

        @Override
        public void reconnectionFailed(Exception e) {
            logger.log(Level.INFO, "Reconnection failed.. ", e);
        }

        @Override
        public void reconnectingIn(int seconds) {
            logger.log(Level.INFO, "Reconnecting in %d secs", seconds);
        }

        @Override
        public void connectionClosedOnError(Exception e) {
            logger.info("Connection closed on error.");
        }

        @Override
        public void connectionClosed() {
            logger.info("Connection closed.");
        }

		@Override
		public void authenticated(XMPPConnection arg0, boolean arg1) {
			// TODO Auto-generated method stub
			
		}
    }
}

I also imported the following external JARs: (they might not all be required but most are!)

json-simple-1.1.1.jar

jxmpp-core-0.4.1.jar

jxmpp-util-cache-0.5.0-alpha2.jar

minidns-0.1.3.jar

commons-logging-1.2.jar

httpclient-4.3.4.jar

xpp3_xpath-1.1.4c.jar

xpp3-1.1.4c.jar

For the Client I used the GCM sample project here. (Scroll to the bottom of the page for the source link)

Hope this helps someone!

[23-Oct-2015] I'm editing this answer for others who are using Gradle ... below is all the dependencies that I needed to get this to compile (add to the bottom of your build.gradle file).

dependencies {
    compile 'com.googlecode.json-simple:json-simple:1.1.1'
    compile 'org.igniterealtime.smack:smack-java7:4.1.4'
    compile 'org.igniterealtime.smack:smack-tcp:4.1.4'
    compile 'org.igniterealtime.smack:smack-im:4.1.4'
    compile 'org.jxmpp:jxmpp-core:0.5.0-alpha6'
    compile 'org.jxmpp:jxmpp-util-cache:0.5.0-alpha6'
}