web-dev-qa-db-fra.com

GCM XMPP Server utilisant Smack 4.1.0

J'essaie d'adapter l'exemple fourni ici pour Smack 4.1.0. et devenir un peu confus.

Plus précisément, je peine à comprendre ce que l'extension GcmPacketExtension doit maintenant être étendue, comment le constructeur doit fonctionner et comment le fournisseur Providermanager.addExtensionProvider doit être mis à jour.

Je suis sûr que quelqu'un a déjà fait cela auparavant, mais je ne trouve aucun exemple et il semble que je tourne en rond en utilisant simplement la documentation.

Toute aide serait très appréciée, je suis sûr que la réponse est très simple!

Code actuel (en cours de compilation mais non en cours d'exécution):

    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);
        }
    });
}

et:

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;
    }
}

Exception actuelle:

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
16
B.A.

OK, alors j’ai réussi à le faire fonctionner après de nombreuses lectures et beaucoup de peine. Voici donc une implémentation de serveur TRÈS grossière qui fonctionne réellement. Évidemment pas pour la production et n'hésitez pas à corriger tout ce qui ne va pas. Je ne dis pas que c'est la meilleure façon de le faire mais ça fonctionne. Il enverra un message et recevra des messages mais ne les affichera que dans le journal.

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
                        
                }
    }
}

J'ai également importé les fichiers JAR externes suivants: (ils ne sont peut-être pas tous obligatoires, mais la plupart le sont!)

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

Pour le client, j’ai utilisé le projet exemple GCM ici . (Faites défiler vers le bas de la page pour le lien source)

J'espère que cela aide quelqu'un!

[23-Oct-2015] J'édite cette réponse pour les autres utilisateurs de Gradle ... vous trouverez ci-dessous toutes les dépendances dont j'avais besoin pour la compiler (à ajouter au bas de votre fichier build.gradle).

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'
}
27
B.A.

Il existe deux implémentations de référence fournies par Google pour le serveur de connexion cloud GCM (point de terminaison XMPP).

Les deux sont ici:

https://github.com/googlesamples/friendlyping/tree/master/server

Le serveur Java utilise la bibliothèque Smack XMPP.

Le serveur Go utilise la propre bibliothèque go-gcm de Google - https://github.com/google/go-gcm

Le serveur Go est également utilisé dans l'exemple GCM Playground - https://github.com/googlesamples/gcm-playground - il semble donc que le serveur Go puisse être préféré par Google. Être Go, il peut être déployé sans aucune dépendance, ce qui est un avantage sur le serveur Java.

1
dodgy_coder