The MetaSieve Blog

August 25, 2010

Using Cometd 2.x with Grails

Filed under: Uncategorized — Tags: , , , — Björn Wilmsmann @ 8:16 pm

I’ve been struggling quite a bit to get CometD (see this for more information on the Comet Pattern) working with Grails and the corresponding Grails CometD plugin, the documentation being sparse and the new version of the CometD API being somewhat confusing with lots of new interfaces and abstractions.

Anyway, I’ve got it working so I thought, I might share the experience (see code sections at the end of the article for copying source code):

Assuming that you would like a Grails service to communicate via CometD, this is how a basic implementation of a simple service that finds Account entities for search parameters would look like:

AccountService.groovy

AccountService.groovy

There are a few things to note here. The service implements the interface InitializingBean provided by Spring. This interface supplies an afterPropertiesSet() method that allows you to initialize additional properties after Spring has wrought its magic. This way you can initialize and handshake with the Bayeux server using the bayeux bean that’s injected by the CometD plugin.

The runAsync closure in findAccounts(def query, def params = [:]) is provided by the Grails Executor plugin, which allows you to run background threads in your application without losing the Hibernate session. This is exactly why runAsync is needed here. As findAccounts(def query, def params = [:]) will be called asynchronously we have to make sure everything that occurs inside this method is thread-safe.

Via the subscribe() method you can have a so-called SubscriberListener subscribe to a messaging channel:

MySubscriberListener.groovy

MySubscriberListener.groovy

Such a SubscriberListener has to extend the abstract class SessionChannel.SubscriberListener as provided by org.cometd.bayeux.client.SessionChannel. This class demands that its sub-classes provide an onMessage(SessionChannel channel, Message message) method, which will act as a callback method upon new messages on the channels the listener is subscribed to.

Apart from that, there’s some nice Groovy magic happening here that allows you to instantiate a MySubscriberListener which calls a service method without actually hard-coding that service method in the MySubscriberListener class. In this case, the listener will call the service’s findAccounts(def query, def params = [:]) method.

The client side of this little example would look something like this (assuming you’re using the jQuery version of the CometD client), with cometd-subscriptions.js being loaded first and init.js afterwards:

cometd-subscriptions.js

cometd-subscriptions.js

init.js

init.js

Once this is all set up, you can do something like this to nudge AccountService from your JavaScript client:

$.cometd.publish('/requests/search', { 'payload': { q: 'some query', params: { offset: 10 } } });

I hope this primer helps you to overcome initial problems with CometD and Grails. If you’ve got any questions or additional contributions, please feel free to leave a comment.

Source code for copy & paste:

AccountService.groovy:

package myapp

import grails.converters.JSON

import org.springframework.beans.factory.InitializingBean

class AccountService implements InitializingBean {

def bayeux
def bayeuxSession

static transactional = true

void afterPropertiesSet() {
bayeuxSession = bayeux.newLocalSession()
bayeuxSession.handshake()
bayeuxSession.getChannel('/requests/search').subscribe(new MySubscriberListener
(this, 'findAccountsToFollow', ['q', 'params']))
}

def findAccounts(def query, def params = [:]) {
runAsync {
def accounts = Account.findAllByName(query, params)

// publish to search results channel
bayeuxSession.getChannel('/results/search').publish(['payload':['accounts':accounts]] as JSON)
}
}
}

MySubscriberListener.groovy:

package myapp

import org.cometd.bayeux.Message
import org.cometd.bayeux.client.SessionChannel

class MySubscriberListener extends SessionChannel.SubscriberListener {
def callbackService
def callbackMethod
def callbackParams

public MySubscriberListener(def service, def methodName, def params = []) {
callbackService = service
callbackMethod = methodName
callbackParams = params
}

public void onMessage(SessionChannel channel, Message message) {
// callback
def callbackParamValues = []
callbackParams.each { param ->
callbackParamValues.add(message.data.payload."${param}")
}

callbackService."${callbackMethod}"(*callbackParamValues)
}
}

cometd-subscriptions.js:

var subscriptions = {};

function refreshCometSubscriptions(channels, callbackFunctions) {
for (var i in channels) {
if (typeof channels[i] == 'string') {
unsubscribeFromCometChannel(channels[i]);
subscribeToCometChannel(channels[i], callbackFunctions[channels[i]]);
}
}
}

function unsubscribeFromCometChannel(channel) {
if (subscriptions[channel]) {
$.cometd.unsubscribe(subscriptions[channel]);
}
subscriptions[channel] = null;
}

function subscribeToCometChannel(channel, callbackFunction) {
subscriptions[channel] = $.cometd.subscribe(channel, callbackFunction);
}

init.js:

// initialize cometd
var channels = ['/test', '/results', '/results/search'];

var testCallback = function() { };
var resultsCallback = function() { };
var searchResultsCallback = function(message) {
renderFollowerSearchResults(JSON.parse(message.data).payload.accounts, '#resultsContainer', 'resultList', '#box2');
};

var callbackFunctions = { '/test' : testCallback, '/results' : resultsCallback, '/results/search' : searchResultsCallback };

$.cometd.init('../../cometd');
$.cometd.addListener('/meta/connect', function(message) {
if ($.cometd.isDisconnected()){
return;
}
if (message.successful) {
$.cometd.publish('/test', { 'data': { 'message':'Connection with CometD server has been established.' } });
}
});
refreshCometSubscriptions(channels, callbackFunctions);

Advertisements

August 20, 2010

i18n in web apps with Dashcode

Filed under: Uncategorized — Tags: , , — Björn Wilmsmann @ 1:54 pm

For quite some time I’ve been searching for a way to properly localize strings in a web app created with Apple‘s otherwise fantastic Dashcode application.

When creating a Dashboard widget there is a special panel for adding languages and translations to your project. However, for some reason that panel is missing from web application projects in Dashcode, so there seems to be no straightforward way to provide a web app created with Dashcode in multiple languages.

Apple’s documentation on the subject – being sparse as usual – wasn’t any help either.

So finally, be reverse engineering the way localization is done in Dashboard widgets I was able to come with my own solution. Here goes:

  1. Add a file named utilities.js with the following content to the project root:

    function getUrlParameters()
    {
    var vars = [], hash;
    var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
    for(var i = 0; i < hashes.length; i++)
    {
    hash = hashes[i].split('=');
    vars.push(hash[0]);
    vars[hash[0]] = hash[1];
    }
    return vars;
    }
  2. Add a file named i18n.js with the following content to the project root:

    var lang = getUrlParameters()['lang'];
    if (!lang || lang === '') {
    lang = 'en_GB';
    }
  3. Add a file named localizedStrings.js with the following content to the project root (The hash entries and languages are of course mere examples.):

    var localizedStrings = {}

    if (lang === 'de_DE') {
    localizedStrings = {
    "Konto": "Konto",
    };
    }

    if (lang === 'en_GB') {
    localizedStrings = {
    "Konto": "Account",
    };
    }

  4. Add the following lines (in exactly the same order) to your index.html right above the entry for main.js:

    <script type="text/javascript" src="../utilities.js" charset="utf-8"></script>
    <script type="text/javascript" src="../i18n.js" charset="utf-8"></script>
    <script type="text/javascript" src="../localizedStrings.js" charset="utf-8"></script>

Voilà, you’re ready to go. In the example above any text, e.g. button labels, in your app that equals “Konto” will automatically be translated to “Account” if lang is set to “en_GB”.

Create a free website or blog at WordPress.com.