Push updates from an ICN service to your client with CometD

One feature I really miss in ICN right now is a 2 ways communication channel between service and client. At this moment, you call your service and then it returns something when the execute method is done. That creates an issue, because users have no patience… More than one minute on the same loading spinner and you can be sure most of them will refresh the page thinking ICN crashed. That won’t stop your service, but if this one is not done by the time the user comes back, he will run the service again…

So here is a way to push information from the server to the client.

Principle

I used CometD to implement that because ICN is already shipped with some CometD client libraries (actually not all of them, discussed below)). The reason for that is ICN uses CometD for the Sync and Share feature. That way we don’t need to add any library to the client. About the server, the PluginService class is not a servlet or a web service, it is actually called by a Struts action so forget about asynchronous servlet, websocket and so on since we are not master of our servlet. On the other hand, we can’t change the navigator application either (well we could and redeploy but we want to be update proof), so we can’t add support for any “push” framework and ICN doesn’t have that natively. That’s why I’ve decided to use another light web application to achieve that. Here is the schema to help you understand how it works.

ICN_CometD_push

 

Principle is quite easy. Before calling the service, the client (usually in your action calling the service), connects to the CometD server and gets a unique client ID. A session is kept open on the CometD server for this client.

Then the client calls the ICN service and provides along with the service parameters its CometD client unique ID. The service starts the long process and from time to time commit updates by posting information to a servlet from the CometD for ICN web application, providing the client UID and a message to push. The CometD for ICN web application search for the session matching the unique client ID, and if it can find it, uses this session to push the message. The client is notified of the new message and can do whatever it needs to do with it. In our example we will change the StatusDialog message.

Then the service finishes its job and returns something. That calls the client’s requestCompleteCallback function, where the client does whatever it needs to, and now also closes properly the CometD session in order for the server to release the session.

Implementation

Client

As I said, the client already embedded some cometD javascript libraries in the dojox/cometd package. We will use these classes, however one of the transport protocol has a bug and gives a wrong content-type to the cometD server (text/json instead of application/json), so we will just have to override this transport protocol.

Actually I noticed that some ICN versions do not have these class. I couldn’t figure out which one yet though, because a same version sometimes has it, sometimes doesn’t. I suspect that could be related to the Sync and Share feature activation. Anyway, if you are experiencing issues loading these classes, copy these files into your plugin (for instance in a cometd folder) and import them from your package instead of the dojox one. I already fixed the transport protocol so you won’t have to do it later.

I’ll explain step by step and then I will give you the complete class you can use as it is. It will make integration a lot easier.

We are going to use the dojox/comet/_base module to connect to our CometD server, so you’ll have to import it.

define([
    "dojo/_base/declare", "dojo/_base/lang", "dojox/cometd/_base", "dojo/io/script"
], function (
    declare, lang
) {
    return declare(null, {
    
    ));
});

Replace dojox with your package if you had to copy the classes because your ICN version doesn’t have them.

We also need dojo/io/script because one of the transport protocol will use it and it is not using a proper AMD loader (still using an old Dojo model). You don’t need to map them to any variable for the same reason (it’s not using the proper AMD model), they will be available via global variable (dojox.cometd.*). That’s ugly but that way we are using what’s available and we don’t need to embed new JavaScript files in our plugin.

Now let’s write the connect function to connect to the CometD server and retrieve the client unique ID. First, add the two transport protocols for longPolling (same domain) and callback polling (cross domain via JSONP) so you can deploy the CometD web app on the same server or another one if you prefer. We’ll add also the Deferred class since we are going to use it.

define([
    "dojo/_base/declare", "dojo/_base/lang", "dojo/Deferred", "dojox/cometd/_base", "dojo/io/script", "dojox/cometd/longPollTransport", "dojox/cometd/callbackPollTransport"
], function (
    declare, lang, Deferred
) {
    return declare(null, {
    
    });
});

Again, replace dojox with your package if you had to copy the classes because your ICN version doesn’t have them.

As I told you, we will add a third protocol for long polling (same domain) based on JSON encoding which fixes the content type issue, but we will do that at the end. Now the init method:

init: function () {
    var res = new Deferred();
    dojox.cometd.init('http://host:port/context/cometd');

    var h = topic.subscribe("/cometd/meta", lang.hitch(this, function (args) {
        // format: {cometd:this,action:"handshake",successful:true,state:this.state()}
        if (args.action == "connect" && args.successful) {
            h.remove();
            this.clientId = args.cometd.clientId;
            this._subscribeToEcho().then(lang.hitch(this, function () {
                res.resolve(this.clientId);
            }));
        } else if (args.action == "connect" && !args.successful) {
            h.remove();
            res.resolve(null);
        }
    }));

    setTimeout(lang.hitch(this, function () {
        if (!res.isResolved()) {
            res.reject("Cannot connect to cometd server");
        }
    }), 15000);

    return res.promise;
},
_subscribeToEcho: function () {
    var res = new Deferred();
    this.handler = dojox.cometd.subscribe("/echo", this, "onMessage");
    this.handler.addCallback(lang.hitch(this, function () {
        res.resolve();
    }));
    return res.promise;
},

As you can see, we also subscribe to an echo channel so the server can push us updates. Now we need to do something when we receive updates from the server (coming from the ICN service vie the new web app). We will also allow developers to add listener to our class, and if there is no listeners, the default behavior is to update the StatusDialog message.

onMessage: function (m) {
    if (m.data == 'close') {
        dojox.cometd.unsubscribe(this.handler);
    } else {
        this._fire(m.data);
    }
},
_fire: function (message) {
    var i;
    if (!this.listeners || this.listeners.length == 0) {
        this.defaultBehavior(message);
        return;
    }
    for (i = 0; i < this.listeners.length; i++) {
        if (this.listeners[i].onMessage) {
            this.listeners[i].onMessage(message);
        }
    }
},
defaultBehavior: function (message) {
    var layout = ecm.model.desktop.getLayout();
    layout._statusDialog.contentNode.innerHTML = message;
}

And finally we need a way to disconnect properly and to add listeners to our class:

addListener: function (listener) {
    if (!this.listeners) {
        this.listeners = [];
    }
    this.listeners.push(listener);
},
disconnect: function () {
    dojox.cometd.unsubscribe(this.handler);
    dojox.cometd.disconnect();
}

Now as I promised, here is the last transport protocol fixed, paste this before the return of your class so we can have everything it one file. You don’t need to do that if you are using the classes I gave you because you didn’t have CometD embedded in your client.

var LongPollTransportJsonEncoded = declare(null, {
    // This is an alternative implementation to that provided in longPollTransportFormEncoded.js
    // that sends messages as text/json rather than form encoding them.

    _connectionType: "long-polling",
    _cometd: null,

    check: function (types, version, xdomain) {
        return ((!xdomain) && (dojo.indexOf(types, "long-polling") >= 0));
    },

    tunnelInit: function () {
        var message = {
            channel:	"/meta/connect",
            clientId:	this._cometd.clientId,
            connectionType: this._connectionType,
            id:	this._cometd.messageId.toString()
        };
        this._cometd.messageId++;
        message = this._cometd._extendOut(message);
        this.openTunnelWith([message]);
    },

    tunnelCollapse: function () {
        // TODO handle transport specific advice

        if (!this._cometd._initialized) {
            return;
        }

        if (this._cometd._advice && this._cometd._advice.reconnect == "none") {
            return;
        }
        if (this._cometd._status == "connected") {
            setTimeout(dojo.hitch(this, function () {
                this._connect();
            }), this._cometd._interval());
        } else {
            setTimeout(dojo.hitch(this._cometd, function () {
                this.init(this.url, this._props);
            }), this._cometd._interval());
        }
    },

    _connect: function () {
        if (!this._cometd._initialized) {
            return;
        }
        if (this._cometd._polling) {
            return;
        }

        if ((this._cometd._advice) && (this._cometd._advice.reconnect == "handshake")) {
            this._cometd._status = "unconnected";
            this._initialized = false;
            this._cometd.init(this._cometd.url, this._cometd._props);
        } else if (this._cometd._status == "connected") {
            var message = {
                channel:	"/meta/connect",
                connectionType: this._connectionType,
                clientId:	this._cometd.clientId,
                id:	this._cometd.messageId.toString()
            };
            this._cometd.messageId++;
            if (this._cometd.connectTimeout >= this._cometd.expectedNetworkDelay) {
                message.advice = {
                    timeout: (this._cometd.connectTimeout - this._cometd.expectedNetworkDelay)
                };
            }
            message = this._cometd._extendOut(message);
            this.openTunnelWith([message]);
        }
    },

    deliver: function (message) {
        // Nothing to do
    },

    openTunnelWith: function (messages, url) {
        this._cometd._polling = true;
        var post = {
            url: (url || this._cometd.url),
            postData: dojo.toJson(messages),
            contentType: "application/json;charset=UTF-8",
            handleAs: this._cometd.handleAs,
            load: dojo.hitch(this, function (data) {
                this._cometd._polling = false;
                this._cometd.deliver(data);
                this._cometd._backon();
                this.tunnelCollapse();
            }),
            error: dojo.hitch(this, function (err) {
                this._cometd._polling = false;
                var metaMsg = {
                    failure: true,
                    error: err,
                    advice: this._cometd._advice
                };
                this._cometd._publishMeta("connect", false, metaMsg);
                this._cometd._backoff();
                this.tunnelCollapse();
            })
        };

        var connectTimeout = this._cometd._connectTimeout();
        if (connectTimeout > 0) {
            post.timeout = connectTimeout;
        }

        this._poll = dojo.rawXhrPost(post);
    },

    sendMessages: function (messages) {
        var i;
        for (i = 0; i < messages.length; i++) {
            messages[i].clientId = this._cometd.clientId;
            messages[i].id = this._cometd.messageId.toString();
            this._cometd.messageId++;
            messages[i] = this._cometd._extendOut(messages[i]);
        }
        return dojo.rawXhrPost({
            url: this._cometd.url || dojo.config.cometdRoot,
            handleAs: this._cometd.handleAs,
            load: dojo.hitch(this._cometd, "deliver"),
            postData: dojo.toJson(messages),
            contentType: "application/json;charset=UTF-8",
            error: dojo.hitch(this, function (err) {
                this._cometd._publishMeta("publish", false, {messages: messages});
            }),
            timeout: this._cometd.expectedNetworkDelay
        });
    },

    startup: function (handshakeData) {
        if (this._cometd._status == "connected") {
            return;
        }
        this.tunnelInit();
    },

    disconnect: function () {
        var message = {
            channel: "/meta/disconnect",
            clientId: this._cometd.clientId,
            id:	this._cometd.messageId.toString()
        };
        this._cometd.messageId++;
        message = this._cometd._extendOut(message);
        dojo.rawXhrPost({
            url: this._cometd.url || dojo.config.cometdRoot,
            handleAs: this._cometd.handleAs,
            postData: dojo.toJson([message]),
            contentType: "application/json;charset=UTF-8"
        });
    },

    cancelConnect: function () {
        if (this._poll) {
            this._poll.cancel();
            this._cometd._polling = false;
            this._cometd._publishMeta("connect", false, {cancel: true});
            this._cometd._backoff();
            this.disconnect();
            this.tunnelCollapse();
        }
    }
});

var lp = new LongPollTransportJsonEncoded();

dojox.cometd.connectionTypes.register("long-polling", lp.check, lp);
dojox.cometd.connectionTypes.register("long-polling-json-encoded", lp.check, lp);

And finally here is the complete class. We will see how to use it right after the code. You can remove line 4 to 176 if you are using my classes since the protocol is already fixed. If you do so, change the imports to use the classes from your plugin.

define(["dojo/_base/declare", "dojo/_base/lang", "dojo/Deferred", "dojo/topic", "ecm/LoggerMixin", "genericActionsPluginDojo/Config", "dojox/cometd/_base", "dojo/io/script", "dojox/cometd/longPollTransport", "dojox/cometd/callbackPollTransport"], function (declare, lang, Deferred, topic, LoggerMixin, Config) {
    
    
    var LongPollTransportJsonEncoded = declare(null, {
        // This is an alternative implementation to that provided in longPollTransportFormEncoded.js
        // that sends messages as text/json rather than form encoding them.

        _connectionType: "long-polling",
        _cometd: null,

        check: function (types, version, xdomain) {
            return ((!xdomain) && (dojo.indexOf(types, "long-polling") >= 0));
        },

        tunnelInit: function () {
            var message = {
                channel:	"/meta/connect",
                clientId:	this._cometd.clientId,
                connectionType: this._connectionType,
                id:	this._cometd.messageId.toString()
            };
            this._cometd.messageId++;
            message = this._cometd._extendOut(message);
            this.openTunnelWith([message]);
        },

        tunnelCollapse: function () {
            // TODO handle transport specific advice

            if (!this._cometd._initialized) {
                return;
            }

            if (this._cometd._advice && this._cometd._advice.reconnect == "none") {
                return;
            }
            if (this._cometd._status == "connected") {
                setTimeout(dojo.hitch(this, function () {
                    this._connect();
                }), this._cometd._interval());
            } else {
                setTimeout(dojo.hitch(this._cometd, function () {
                    this.init(this.url, this._props);
                }), this._cometd._interval());
            }
        },

        _connect: function () {
            if (!this._cometd._initialized) {
                return;
            }
            if (this._cometd._polling) {
                return;
            }

            if ((this._cometd._advice) && (this._cometd._advice.reconnect == "handshake")) {
                this._cometd._status = "unconnected";
                this._initialized = false;
                this._cometd.init(this._cometd.url, this._cometd._props);
            } else if (this._cometd._status == "connected") {
                var message = {
                    channel:	"/meta/connect",
                    connectionType: this._connectionType,
                    clientId:	this._cometd.clientId,
                    id:	this._cometd.messageId.toString()
                };
                this._cometd.messageId++;
                if (this._cometd.connectTimeout >= this._cometd.expectedNetworkDelay) {
                    message.advice = {
                        timeout: (this._cometd.connectTimeout - this._cometd.expectedNetworkDelay)
                    };
                }
                message = this._cometd._extendOut(message);
                this.openTunnelWith([message]);
            }
        },

        deliver: function (message) {
            // Nothing to do
        },

        openTunnelWith: function (messages, url) {
            this._cometd._polling = true;
            var post = {
                url: (url || this._cometd.url),
                postData: dojo.toJson(messages),
                contentType: "application/json;charset=UTF-8",
                handleAs: this._cometd.handleAs,
                load: dojo.hitch(this, function (data) {
                    this._cometd._polling = false;
                    this._cometd.deliver(data);
                    this._cometd._backon();
                    this.tunnelCollapse();
                }),
                error: dojo.hitch(this, function (err) {
                    this._cometd._polling = false;
                    var metaMsg = {
                        failure: true,
                        error: err,
                        advice: this._cometd._advice
                    };
                    this._cometd._publishMeta("connect", false, metaMsg);
                    this._cometd._backoff();
                    this.tunnelCollapse();
                })
            };

            var connectTimeout = this._cometd._connectTimeout();
            if (connectTimeout > 0) {
                post.timeout = connectTimeout;
            }

            this._poll = dojo.rawXhrPost(post);
        },

        sendMessages: function (messages) {
            var i;
            for (i = 0; i < messages.length; i++) {
                messages[i].clientId = this._cometd.clientId;
                messages[i].id = this._cometd.messageId.toString();
                this._cometd.messageId++;
                messages[i] = this._cometd._extendOut(messages[i]);
            }
            return dojo.rawXhrPost({
                url: this._cometd.url || dojo.config.cometdRoot,
                handleAs: this._cometd.handleAs,
                load: dojo.hitch(this._cometd, "deliver"),
                postData: dojo.toJson(messages),
                contentType: "application/json;charset=UTF-8",
                error: dojo.hitch(this, function (err) {
                    this._cometd._publishMeta("publish", false, {messages: messages});
                }),
                timeout: this._cometd.expectedNetworkDelay
            });
        },

        startup: function (handshakeData) {
            if (this._cometd._status == "connected") {
                return;
            }
            this.tunnelInit();
        },

        disconnect: function () {
            var message = {
                channel: "/meta/disconnect",
                clientId: this._cometd.clientId,
                id:	this._cometd.messageId.toString()
            };
            this._cometd.messageId++;
            message = this._cometd._extendOut(message);
            dojo.rawXhrPost({
                url: this._cometd.url || dojo.config.cometdRoot,
                handleAs: this._cometd.handleAs,
                postData: dojo.toJson([message]),
                contentType: "application/json;charset=UTF-8"
            });
        },

        cancelConnect: function () {
            if (this._poll) {
                this._poll.cancel();
                this._cometd._polling = false;
                this._cometd._publishMeta("connect", false, {cancel: true});
                this._cometd._backoff();
                this.disconnect();
                this.tunnelCollapse();
            }
        }
    });

    var lp = new LongPollTransportJsonEncoded();

    dojox.cometd.connectionTypes.register("long-polling", lp.check, lp);
    dojox.cometd.connectionTypes.register("long-polling-json-encoded", lp.check, lp);
    
    
    
    
    return declare([LoggerMixin], {
        clientId: null,
        handler: null,
        listeners: null,
        /**
         * Init the connectio nto the cometD server and return a promise resolving sith the unique id
         */
        init: function () {
            this.logEntry("init");
            var res = new Deferred();
            Config.getCometDconnectUrl().then(lang.hitch(this, function (cometDconnectUrl) {
                dojox.cometd.init(cometDconnectUrl);
            
                var h = topic.subscribe("/cometd/meta", lang.hitch(this, function (args) {
                    // format: {cometd:this,action:"handshake",successful:true,state:this.state()}
                    this.logDebug("init", "received:" + args.action + " " + args.successful);
                    if (args.action == "connect" && args.successful) {
                        h.remove();
                        this.clientId = args.cometd.clientId;
                        this.logDebug("init", "cometD id: " + this.clientId);
                        this._subscribeToEcho().then(lang.hitch(this, function () {
                            res.resolve(this.clientId);
                        }));
                    } else if (args.action == "connect" && !args.successful) {
                        h.remove();
                        res.resolve(null);
                    }
                }));

                setTimeout(lang.hitch(this, function () {
                    if (!res.isResolved()) {
                        this.logError("init", "Cannot connect to cometD server, timeout reached");
                        res.reject("Cannot connect to cometd server");
                    }
                }), 15000);
            }));
            
            return res.promise;
        },
        _subscribeToEcho: function () {
            this.logEntry("_subscribeToEcho");
            var res = new Deferred();
            this.handler = dojox.cometd.subscribe("/echo", this, "onMessage");
            this.handler.addCallback(lang.hitch(this, function () {
                this.logInfo("_subscribeToEcho", "subscription to /echo established");
                res.resolve();
			}));
            return res.promise;
        },
        disconnect: function () {
            this.logEntry("disconnect");
            dojox.cometd.unsubscribe(this.handler);
            dojox.cometd.disconnect();
        },
        onMessage: function (m) {
            this.logDebug("onMessage", "received: " + JSON.stringify(m));
            this._fire(m.data);
            if (m.data == 'close') {
                console.log('unsubscribing');
                dojox.cometd.unsubscribe(this.handler);
            }
        },
        addListener: function (listener) {
            this.logEntry("addListener");
            if (!this.listeners) {
                this.listeners = [];
            }
            this.listeners.push(listener);
        },
        _fire: function (message) {
            var i;
            if (!this.listeners || this.listeners.length == 0) {
                this.logDebug("_fire", "fire with default behavior: " + message);
                this.defaultBehavior(message);
                return;
            }
            for (i = 0; i < this.listeners.length; i++) {
                if (this.listeners[i].onMessage) {
                    this.logDebug("_fire", "fire to listener: " + message);
                    this.listeners[i].onMessage(message);
                }
            }
        },
        defaultBehavior: function (message) {
            var layout = ecm.model.desktop.getLayout();
            layout._statusDialog.contentNode.innerHTML = message;
        }
    });
});

To use this class, connect to the cometD server before calling the service, and when it’s done, call your service by providing the client ID:

var serviceParams = {};
serviceParams[Constants.PARAM_REPOSITORY] = repository.id;
serviceParams[Constants.PARAM_SERVER_TYPE] = repository.type;
// Add params you need here

var sl = new ServiceListener();

var callService = lang.hitch(this, function () {
    // Calling service
    Request.invokePluginService("YouPluginId", "YouServiceId", {
        requestParams : serviceParams,
        requestCompleteCallback : lang.hitch(this, function (response) {
            sl.disconnect();
            // Do whatever you need to
        })
    });
});

sl.init().then(
    lang.hitch(this, function (clientId) {
        serviceParams[Constants.PARAM_COMETD_CLIENTID] = clientId;
        callService();
    }),
    lang.hitch(this, function () {
        callService();
    })
);

That’s all you need to do, just init our CometD support and add the client ID to the service parameters. Then disconnect when it’s done.

Server

CometD for ICN web application

The web application is quite basic. There are only two classes. One is the CometD service listening for connection and used to push information to the clients. The other is the servlet called by the ICN service to ask our web app to push updates to clients.

We also need to register the cometD servlet in our web.xml so it can listen for new client connection.

That’s all we need to do for this web application. Here are the code for the two class and the web.xml.

package com.ibm.ditatools.icn.extensions.cometd;
import javax.inject.Inject;

import org.cometd.annotation.Listener;
import org.cometd.annotation.Service;
import org.cometd.annotation.Session;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;

@Service
public class BayeuxService
{
    
    @Inject
    private BayeuxServer bayeuxServer;
    
    @Session
    private LocalSession sender;
    
    public boolean send(String cometdId, String message) {
        ServerSession session = bayeuxServer.getSession(cometdId);
        if (session == null) { 
            return false;
        } else {
            session.deliver(sender, "/echo", message, null);
            return true;
        }
        
    }
    
    @Listener("/service/hello")
    public void processClientHello(ServerSession session, ServerMessage message)
    {
        for (ServerSession ss : bayeuxServer.getSessions()) {
            if (ss.getId().equals(session.getId())) {
                ss.deliver(sender, "/echo", "I found you with your ID, soon everything will work hehe", null);
            };
        }
    }
}
package com.ibm.ditatools.icn.extensions.cometd;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.cometd.annotation.ServerAnnotationProcessor;
import org.cometd.bayeux.server.BayeuxServer;
import org.json.JSONObject;

public class TranslateServlet extends GenericServlet {

    private static final long serialVersionUID = -5388174315096488756L;
    private final List<Object> services = new ArrayList<Object>();
    private ServerAnnotationProcessor processor;
    private BayeuxService service;

    @Override
    public void init() throws ServletException {
        BayeuxServer bayeux = (BayeuxServer) getServletContext().getAttribute(BayeuxServer.ATTRIBUTE);
        processor = new ServerAnnotationProcessor(bayeux);
        service = new BayeuxService();
        processor.process(service);
        services.add(service);
    }

    @Override
    public void destroy() {
        for (Object service : services) {
            processor.deprocess(service);
        }
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        JSONObject json = parseStream(req.getInputStream());
        PrintWriter writer = res.getWriter();
        String clientId = (String)json.get("clientId");
        String message = (String)json.get("message");
        writer.write(Boolean.toString(service.send(clientId, message)));
        writer.flush();
        writer.close();
    }
    
    /**
     * Parse an input stream to a {@link JSONObject}
     * @param is the {@link InputStream}
     * @return the {@link JSONObject}
     * @throws IOException if exception during reading stream
     */
    private JSONObject parseStream(InputStream is) throws IOException {
        BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        StringBuilder jsonString = new StringBuilder();

        String inputStr;
        while ((inputStr = streamReader.readLine()) != null)
            jsonString.append(inputStr);

        return new JSONObject(jsonString.toString());
    }

}
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	version="2.5">
	<display-name>CometD for ICN</display-name>

	<servlet>
		<servlet-name>cometd</servlet-name>
		<servlet-class>org.cometd.annotation.AnnotationCometdServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet>
		<servlet-name>translate</servlet-name>
		<servlet-class>com.ibm.ditatools.icn.extensions.cometd.TranslateServlet</servlet-class>
		<load-on-startup>2</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>cometd</servlet-name>
		<url-pattern>/cometd</url-pattern>
	</servlet-mapping>

	<servlet-mapping>
		<servlet-name>translate</servlet-name>
		<url-pattern>/translate</url-pattern>
	</servlet-mapping>

	<filter>
		<filter-name>continuation</filter-name>
		<filter-class>org.eclipse.jetty.continuation.ContinuationFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>continuation</filter-name>
		<url-pattern>/cometd</url-pattern>
	</filter-mapping>
</web-app>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd"
	version="3.1">
	<display-name>CometD for ICN</display-name>

	<servlet>
		<servlet-name>cometd</servlet-name>
		<servlet-class>org.cometd.annotation.AnnotationCometDServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
		<async-supported>true</async-supported>
	</servlet>

	<servlet>
		<servlet-name>translate</servlet-name>
		<servlet-class>com.ibm.ditatools.icn.extensions.cometd.TranslateServlet</servlet-class>
		<load-on-startup>2</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>cometd</servlet-name>
		<url-pattern>/cometd</url-pattern>
	</servlet-mapping>

	<servlet-mapping>
		<servlet-name>translate</servlet-name>
		<url-pattern>/translate</url-pattern>
	</servlet-mapping>

</web-app>

You will have to add CometD to your dependencies. I used a verion 2.x along with servlet 2.5, which requires a filter of jetty continuation, because my WebSphere server is still under Java EE 6, but you can use the version 3.x along with servlet 3.1 if you are under Java EE 7. Only difference will be:

  • in the web.xml, the servlet changed name and takes a D instead of a d (org.cometd.annotation.AnnotationCometDServlet), you can get rid of the filter and add the async support to the CometD servelt.
  • in the BayeuxService class, the deliver method (used twice) doesn’t need the last argument anymore (it was null anyway)
  • in the POM, change the cometd dependencies to 3.x and replace the servlet api 2.5 with a 3.1

Here are the war files if you don’t want to do it yourself using the version 2.9.1 of Cometd (and servlet 2.5), and using the version 3.0.4 of CometD (and servlet 3.1).

Here are the full projects as Maven projects for the version 2.9.1 and 3.0.4.

In the ICN service

Last step of this tutorial is actually calling our CometD for ICN web application from our service. This is quite simple, we just need one method to post our data to the servlet:

public static final boolean commitProgress(String clientId, String message) {
    URL translateServlet = null;
    try {
        translateServlet = new URL("http://host:port/context/translate");
        HttpURLConnection servletConnection = (HttpURLConnection) translateServlet.openConnection();
        servletConnection.setRequestMethod("POST");
        servletConnection.setRequestProperty("content-type", "application/json; charset=utf-8");
        servletConnection.setRequestProperty("accept-charset", "UTF-8");
        servletConnection.setDoOutput(true);
        
        JSONObject json = new JSONObject();
        json.put("clientId", clientId);
        json.put("message", message);
        
        byte[] postData = json.toString().getBytes( Charset.forName( "UTF-8" ));
        
        OutputStream os = servletConnection.getOutputStream();
        os.write(postData);
        os.flush();
        os.close();
        
        String line;
        BufferedReader reader = new BufferedReader(new InputStreamReader(servletConnection.getInputStream()));

        boolean res = false;
        while ((line = reader.readLine()) != null) {
            res = Boolean.parseBoolean(line);
        }
        reader.close(); 
        
        return res;

    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return false;
}

Of course you’ll have to add proper log and exception handler, this is just a POC for now.

Putting all together

To make this all work together, here is a summary:

  1. Edit the client to add our Dojo class (only first time), import the module in your class calling the service and use the init function to get the CometD client ID and give that to your service
  2. Deploy the web application on a application server (only once for all) and edit the client and service to use the right URL. (That should be in your pugin configuration but I skipped that here to keep things simple)
  3. Use the given method in your service to call the servlet and your client should receive the updates

Demo

And finally a demo of the use of CometD within ICN. We are adding 1,300 documents within our service and let the user know about the progression since it takes a long time.

 

2 thoughts on “Push updates from an ICN service to your client with CometD

  1. Sasha

    Hi!

    An off topic question about Request.invokePluginService and isEnabled in Action Plugin.

    I nead to perform Request.invokePluginService in isEnabled function to retrieve some data related to whether the action is enabled or not.
    As you may know invokePluginService is asynchronous, and the return of isEnabled is irrelevant to what it should do.

    Maybe you have some ideas about how to handle this situation?

    P.S.
    Your blog is very useful! Thank you!

    Reply
    1. Guillaume Post author

      Hi Sasha,

      Sorry for the delay, I’ve been really busy lately. Unfortunately I don’t have a perfect answer for that and I would say there is no perfect solution anyway. Allowing us to that (or overwiting some part of the JS api to that ourself :)) would be pretty non-user friendly because I don’t think having delay on a right click is something the user wants.

      Some work around I’m using:
      – if you just need an attribute, add a column in the content view, that will retrieve it from the server and make in available via the contentItem.attributes.yourAttr field in the isVisible function.
      – If you really need to call a service, then change your action to more generic name and do your service call in the performAction. If it shouldn’t be visible, just display a MessageDialog saying the action isn’t available for this item or something similat.

      I know that’s not the answer you want but I have found nothing better right now. However when I have more time I will try to dive deeper into ICN code see if we can do something better but I doubt it, since having the right click synchronous is immutable to me, or maybe displaying a menu “loading menu” at first might be OK. I will comment back here if I find more, and maybe write a new post as well since we are not the only one interested.

      Reply

Leave a Reply to Guillaume Cancel reply