Métaprogrammation et réflectivité Ruby

ruby est particulièrement appréciable pour sa dynamicité C'est ce que nous allons voir dans cet article avec une présentation de la métaprogrammation et de la réflexivité dans ce langage.

Qu'est-ce que la métaprogrammation et la réflexivité ?

Le livre The Ruby Programming Language chez O'Reilly dit :

Loosely defined, metaprogramming is writing programs (or frameworks) that help you write programs.
To put it another way, metaprogramming is a set of ways for extending ruby's syntax in ways that make programming easier.

Et en Français :

En définissant rapidement, la métaprogrammation consiste à créer des programmes (ou des frameworks) qui aident à écrire des programmes. Pour dire autrement, la métaprogrammation est une liste de méthodes permettant d'étendre la syntaxe de ruby afin de faciliter le développement.

La réflexivité consiste examiner l'état et la structure d'un objet. Ainsi, un programme pourra obtenir la liste de toutes les méthodes définies dans un objet

En gros : nous allons voir, dans cet article, comment un objet peut analyser son état ... Et altérer celui-ci en créant ou remplaçant des méthodes.

Réflectivité

Supposons la classe suivante :

class Testing < Hash

    def basic_method
        "Hello World"
    end
end

Plusieurs méthodes de réflexivité nous sont alors disponibles pour celle-ci.

t = Testing.new
t.class        #=> Testing
t.superclass #=> Hash
t.instance_of? Testing    #=> true
t.is_a? Hash      #=> true
t.respond_to? :basic_method    #=> true

Quelles sont ces diverses méthodes que nous exécutons ici ?

  • class - Retourne la classe de l'objet. Ici, Testing.
  • superclass - Retourne la classe parente de l'objet. Ici, Hash.
  • instance_of? - Détermine si l'objet est de la classe passée en paramètre.
  • is_a?/kind_of? - Détermine si l'objet est de la classe passée en paramètre ou d'une classe qui en hérite. Si il s'agit d'un module, elle indique si la classe inclus celui-ci.
  • respond_to? - Indique si l'objet définit la méthode (privée ou protégée) dont le nom est passé en paramètre. Il est possible de passer un second paramètre true si vous souhaitez également vérifier les méthodes privées.

Ajouté à celles-ci sont disponibles divers raccourcis permettant de tester de manière similaire divers objets.
Supposons les modules et la classe suivante :

module A
end

module B
    include A
end

class C
    include B
end

Nous avons alors accès aux méthodes suivantes :

C < B  #=> true
B < A  #=> true
C < A #=> true

Avec l'opérateur <, nous vérifions si une classe hérite d'une autre. Ou bien qu'un module est inclus.
Le test est bien évidemment fait pour toutes les classes parentes.

Bien évidemment, nous pouvons également récupérer une liste de toutes les classes parentes à la notre.
Si nous reprenons notre classe Testing plus haut, nous aurons donc :

Testing.ancestors #=> [Hash]

Qui nous retourne un tableau de toutes les classes dont dépends la classe courante.

L'équivalent pour les modules existe également, nous premettant d'obtenir la liste de tous les modules inclus dans la classe ou le module fourni.

A.included_modules  #=> []
B.includes_modules  #=> [A]
C.includes_modules #=> [B, A, Kernel]

Enfin une fonction booléen existe également pour savoir si la classe ou le module inclus la classe ou le module fournis.

C.includes?(A) #=> true

Avec ces fonctions de réflexivité, vous pouvez ainsi analyser vos classes lors de l'exécution de votre programme.
Ceci est particulièrement pratique lors de l'écriture de tests mais peut également s'avérer très utile lorsque vous créez dynamiquement de nouvelles méthodes.

Evaluation de code

Chaines de caractères et blocs

L'une des fonctionnalités les plus puissantes, mais aussi les plus dangereuses de Ruby est la méthode eval.
Celle-ci vous permet d'évaluer une chaine de caractères comme s'il s'agissant de code ruby.

x = 1
eval "x + 1" #=> 2

Eval est particulièrement puissant. Mais à moins que vous ne cherchiez à écrire un shell ruby (tel que irb), vous ne l'utiliserez jamais.

Plus que "vous ne l'utiliserez jamais", on devrait même dire "vous ne devriez jamais l'utiliser". C'est la meilleure manière d'obtenir des failles de sécurité gigantesques. Qui plus est, il est toujours possible de faire sans.

instance_eval, class_eval et module_eval

Instance_eval et class_eval sont, quant à eux, beaucoup plus utilisés. Dans rails par exemple, supposons que vous ayez, dans vos routes :

resources   :users

Cette route de ressources, créera plusieurs méthodes accessibles dans les contrôleurs et dans les vues :

users_path
users_url

user_path(user)
user_url(user)

edit_user_path(user)
edit_user_url(user)

new_user_path
new_user_url

Quand les quatre premières sont gerées différemment, les quatre dernières sont générées dynamiquement. Vous pouvez voir ceci à la ligne 145 de polymorphic_routes.rb

instance_eval, class_eval et module_eval permettent de modifier dynamiquement le contenu d'un module, d'une classe ... Ou d'une instance de classe.

Ainsi, nous pouvons ajouter une nouvelle méthode dans une chaine de caractères, nous permettant ainsi de la nommer de la manière que nous désirons.

class_eval et module_eval

nous avons module_eval utilisé dans rails plus haut. Class_eval fonctionne de la même manière pour un objet.

Lorsqu'il est utilisé, il permet de modifier dynamiquement le contenu d'un module ou d'une classe ... Et de répercuter ces modifications sur tous les modules ou classes qui incluent celui-ci on en héritent.

Voyons par exemple :

class A
end

class B < A
end

La méthode "hello" n'est pas définie dans la classe A. Donc elle ne l'est pas non plus dans la classe B.

B.hello #=> NoMethodError

Nous pouvons avec class_eval définir celle-ci pour A et pour B.

A.class_eval do
    def self.hello
        "world"
    end
end

La méthode hello est maintenant définie et nous pouvons l'utiliser dans A et toutes les classes qui en étendent.

A.hello #=> "world"
B.hello #=> "world"

module_eval fonctionne exactement de la même manière en modifiant toutes les classes qui le module.

instance_eval

Instance_eval est encore plus puissant puisque, comme son nom l'indique, il permet de modifier une seule instance d'une classe ...

Reprenons nos classes A et B de tout à l'heure.

class A
end
class B < A
end

Et supposons le code suivant :

b = B.new
c = B.new

Maintenant, nous utilisons instance_eval afin de définir une méthode hello sur l'instance "b" de la classe B.

b.instance_eval do
    def hello
        "world"
    end
end

Et maintenant si nous faisions appel à cette methode :

b.hello #=> "world"
c.hello #=> NoMethodError

Les deux instances "b" et "c" sont tous des instances de B.
Mais maintenant que nous avons utilisé instance_eval, elles n'ont plus les même méthodes.

Supprimer des méthodes

Nous avons vu comment définir de nouvelles méthodes.
La logique veut que, si nous pouvons ajouter de nouvelles méthodes à un objet, nous puissions également en supprimer.

C'est ce que permettant remove_method et undef_method.

Remove_method supprime la méthode spécifiée de la classe. Mais si la même méthode est définie par une classe parente alors, c'est celle-ci qui sera maintenant appelée.

Under_method est plus stricte. Il supprime la méthode dans la classe fournie mais également dans toutes les classes parentes.

Aliaser des méthodes

Lorsque vous surchargez une méthode, c'est probablement dans le but de modifier son fonctionnement originel ou d'y ajouter une fonctionnalité.
Mais dans ce cas, vous perdez la fonctionnalité originelle et êtes obligé de la réimplémenter.

Afin d'éviter cela, il est possible d'aliaser des méthodes.
Supposons la classe suivante :

class B
    def hello
        "world"
    end
end

Supposons maintenant que vous souhaitiez que cette méthode hello ne retourne plus "world" mais "hello world".
Nous allons évidemment utiliser class_eval afin de surcharger la classe. Cependant nous souhaitons utiliser le code déjà implémenté dans la classe originelle.

B.class_eval do
    alias       :old_hello    :hello
end

Ce code va dynamiquement faire une copie de la méthode hello dans une nouvelle méthode old_hello. Nous pouvons donc maintenant surcharger notre classe sereinement, nous avons toujours accès à l'ancienne méthode.

B.class_eval do
    alias       :old_hello    :hello

    def hello
        "hello #{old_hello}"
    end
end

Si maintenant, nous faisons appel à B, nous aurons :

B.new.hello #=> "hello world"

Freeze

Vous avez vu que nous pouvons modifier autant que nous le désirons toute classe.
Cependant vous pouvez désirer, pour une raison ou une autre, que l'une de vos classes ne doit surtout pas être modifiée.

class B; end
B.freeze
B.class_eval do
    def hello
        "world"
    end
end #=> TypeError: can't modify frozen class

Vous pouvez ainsi empêcher volontairement toute modification dynamique de l'une de vos classes.

Conclusion

Comme vous pouvez le constater, les fonctionnalités dynamiques de ruby sont particulièrement puissantes.
Attention cependant. Avec de grands pouvoirs viennent de grandes responsabilités.

Définir dynamiquement des méthodes à mauvais escient peut fortement nuire à la vitesse de votre application, voir à sa sécurité.
Réflechissez-y à deux fois avant de modifier dynamiquement l'une de vos classes.

Pourquoi et comment nous avons migré de CouchDB vers MongoDB

Après quelques hésitations et suite à de nombreuses discussions, nous avons décidé de migrer l'application sur laquelle je travaille de CouchDB vers MongoDB.

Je vais tenter, dans cet article, de vous expliquer pourquoi et comment nous avons décidé de faire cela.

Le pourquoi du comment

L'image ci-contre est l'interface de recherche de l'application sur laquelle je travaille.

Dans cette application, nous avons divers enregistrements (chacun représentant un forage). Chacun de ces enregistrements possède divers attributs tels que ceux qui sont affichés ici (nom; longueur et date de forage).

La partie haute de l'image correspond aux filtres. On peut ajouter un ou plusieurs filtres sur les attributs. Ici par exemple, nous avons deux filtres : un sur le numéro de série de l'appareil qui a analysé le forage et un sur une intervalle de dates.

Les enregistrements affichés sont ceux qui correspondent à tous les filtres présents. La raison principale nous ayant poussé à migrer vers MongoDB est ici !

Fonctionnement sous CouchDB

Afin de rechercher l'enregistrement approprié avec CouchDB, nous créons dynamiquement diverses vues.

Par exemple la recherche ci-dessus fait appel à la vue by_boreholename_creation_date. Vue qui nous retourne "simplement" tous les enregistrements avec, comme clé, un tableau de tous les attributs présentés. A l'appel de cette vue, nous faisons une recherche avec startkey et endkey, qui nous permet alors de filtrer suivant les valeurs fournies.

Cependant l'article startkey et endkey dont le lien est plus haut dit :

CouchDB fait en effet une comparaison d'égalité pour chacun des éléments du tableau, sauf le dernier.

Du coup supposons la recherche suivante :

  • Sur une intervalle de date de début.
  • Puis sur un nom de forage.

Le second élément de la clé sera cherché correctement. Cependant pour l'intervale, la recherche faite sera en ==. En conséquent nous n'aurons absolument pas les résultats escomptés. Nous n'aurons que les enregistrements correspondant à la date appropriée.

Lors du WebWorkersCamp, j'ai abordé ce problème à Benoit Chesneau qui est contributeur CouchDB. Bien que nous n'ayons pas vraiment eu le temps d'en parler en détail, la solution qu'il semblait me proposer était de créer une vue pour chaque filtre et de faire autant d'appels que de filtres.

A l'heure actuelle, certains de nos clients ont plus de 700 enregistrements en base et cela va en grimpant de manière exponentielle. Nos requêtes sont déjà (trop) lentes. Il n'est pas envisageable de faire une requête pour chaque filtre.

Fonctionnement sous MongoDB

Du coup nous avons décidé de migrer sous MongoDB. J'ai commencé à travailler sur cette migration le 26 juillet et celle-ci a été déployée en production cette semaine même (30 aout).

Les requêtes sous MongoDB sont beaucoup plus simples. Déjà car nous n'avons pas à nous préoccuper de créer des vues en javascript. Elles sont générées automatiquement par le moteur. Les opérateurs permettent de gérer tous nos besoins.

Nous utilisons mongoid (sous rails 2.3). Et un simple appel à la méthode where() avec un hash d'arguments correspondant à la recherche suffit pour récupérer la liste de nos enregistrements.

Nous n'avons même plus de problématique d'ordre des filtres. Nos recherches sont toujours effectuées sur un intervalle lorsqu'il y en a un ou une expression régulière lorsqu'il s'agit d'une chaine.

Le comment du pourquoi

Avec CouchDB, nous utilisions CouchRest. Avec MongoDB, nous utilisons Mongoid.

La principale probématique du "comment" fût la migration des données. Je ne souhaitais pas vraiment faire du trop bas niveau.

J'ai donc migré les modèles CouchDB vers le dossier lib/ (afin qu'ils ne soient pas chargés automatiquement en production) et je leur ai mis le contenu suivant :

module Couchdb  
    class Record < CouchRest::ExtendedDocument
        def self.to_s
            "Record"
        end
    end
end

Ainsi mon modèle récupère les données de CouchDB correspondant au namespace "Record" et non pas "Couchdb::Record" comme cela aurait été le cas originellement.

Puis dans une tâche rake, je parcours tous les documents CouchDB et les ajoute dans MongoDB.

Couchdb::Record.all.each do |record|
    record.delete '_rev'
    record.delete '_attachments'
    record.delete 'couchrest-type'
    r = Record.new record     
    puts "Record saved"
  end
  puts "There are now #{Record.count} records in the mongo db"

CouchRest, faisant étendre les modèles de Hashes (ce qui est une décision fortement discutable) rends la chose particulièrement aisée puisqu'il nous suffit de transmettre le document sans les paramètres métiers.

Vous noterez que nous ne supprimons pas le _id de CouchRest. Ainsi, notre document conserve le même id.

Cette tâche rake est exécutée manuellement lors du déploiement en production.

Conclusion

N'abandonnez pas CouchDB à cause de cet article ! Bien que ce moteur ne convienne, in fine, pas à nos besoins de recherche multi attributs, il n'en reste pas moins quelque chose d'excellent que je n'hésiterai pas à recommander.

Envie de travailler sur l'application que je viens de décrire ? Nous cherchons un développeur javascript/ruby pour me remplacer ! contactez moi pour plus d'informations

Par ailleurs la bonne nouvelle est que, MongoDB stockant ses données en json binaire, l'accès à celles-ci est beaucoup plus rapide. Nous n'avons pas encore de réelles statistiques en production. Mais le temps d'exécution de nos tests a été réduit de 20% !

Créer des traitements personnalisés pour CarrierWave

Si vous avez besoin de traiter des images sur une application rails, je ne peux que vous conseiller CarrierWave.

Cette librairie vous permettra aisément de gérer de l'upload de fichier. Mais également du post traitement automatisé sur les images, tel que le redimensionnement de celles-ci.

J'utilise CarrierWave sur ce blog, pour une refonte graphique qui devrait être publiée dans deux à trois semaines. Chaque catégorie et chaque article as ainsi une image. Cette image possède différentes versions qui sont chacune plus ou moins grande.

Pour l'une de ces versions, je souhaite obtenir des bords arrondis.
Le résultat désiré est l'image à gauche ici.

Voyons en détails comment j'en suis arrivé au résultat ci-contre.

 

Post processing CarrierWave

Imaginons le modèle d'upload suivant :

class CategoryUploader < CarrierWave::Uploader::Base
    include CarrierWave::RMagick
    version :thumb do
        process :resize_to_fill => [248, 163]
    end
end

Nous avons ici un uploader de catégorie qui créera une seconde image "thumb". Cette image sera de taille 248 * 163 pixels.

Vous pouvez trouver le processeur "resize_to_fill", qui redimensionnera l'image à lib/carrierwave/processing/rmagick.rb.

Afin de créer des bords arrondis, nous allons devoir créer notre propre processeur. Je l'ai placé dans lib/dmathieu/carrier_wave/round.rb.

unless defined? Magick 
    begin
        require 'rmagick'
    rescue LoadError
        require 'RMagick'
    rescue LoadError
        puts "WARNING: Failed to require rmagick, image processing may fail!"
    end
end

module Dmathieu
    module CarrierWave
        module Round

            module ClassMethods
                def rounded_corner(radius = 10)
                    process :rounded_corner => [radius]
                end
            end

            ##
            # Makes the image's corners round
            #
            #
            # === Parameters
            #
            # [radius (#to_s)] the corner radius
            #
            # === Yields
            #
            # [Magick::Image] additional manipulations to perform
            #
            def rounded_corner(radius = 10)
                #
                # Voir plus loin pour le code métier
                #
            end
        end
    end

Puis, dans votre uploader, vous devez inclure ce processeur nouvellement créé.

class CategoryUploader < CarrierWave::Uploader::Base
    include CarrierWave::RMagick
    include Dmathieu::CarrierWave::Round

    version :thumb do
        process :resize_to_fill => [248, 163]
        process :rounded_corner
    end
end

Vous pouvez constater que, dans notre module, nous créons une méthode rounded_corner, qui fera l'action d'ajouter des coins arrondis à notre image. Cette action sera appelée automatiquement par CarrierWave lors de la génération de cette version de l'image.

Le code métier

Afin de manipuler notre image, nous utilisons rmagick.

Pour créer notre image, nous allons faire la chose suivante :

  • Créer une nouvelle image de la même taille que la notre, qui contiendra un rectangle aux bords arrondis.
  • Placer ce rectangle au dessus de notre image.

Plutôt simple non ? ;-)

def rounded_corner(radius = 10)
    manipulate! do |img|
        masq = ::Magick::Image.new(img.columns, img.rows).matte_floodfill(1, 1)         
        ::Magick::Draw.new.
            fill('transparent').
            stroke('black').
            stroke_width(1).
            roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius).
            draw(masq)

        img.composite!(masq, 0, 0, Magick::LightenCompositeOp)
        img = yield(img) if block_given?
        img
    end
end

Nous venons ici de définir la méthode rounded_corner, qui se situe dans le module expliqué plus haut.

La méthode manipulate! se situe dans le processeur rmagick.rb et nous permet d'obtenir l'image courante, de la modifier et de la transmettre au processeur suivant (via le yield).

masq = ::Magick::Image.new(img.columns, img.rows).matte_floodfill(1, 1)

Nous créons ici une nouvelle image de la même taille que la précédente mais avec intégralement transparente.

::Magick::Draw.new.
        fill('transparent').
        stroke('black').
        stroke_width(1).
        roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius).
        draw(masq)

Nous dessinons maintenant dans l'image précédemment créée.

fill('transparent').

Le fonds du rectangle que nous dessinons sera transparent.

stroke('black').
stroke_width(1).

Notre rectangle aura une bordure noire de 1 pixel.

roundrectangle(0, 0, img.columns - 1, img.rows - 1, radius, radius).
draw(masq)

Nous créons ici le rectangle et l'ajoutons à l'image créée juste auparavant.

img.composite!(masq, 0, 0, Magick::LightenCompositeOp)

Enfin nous superposons cette image nouvellement créée à celle qui existe déjà, rendant l'effet escompté.

img = yield(img) if block_given?
img

CarrierWave fonctionne de manière imbriquée. Chaque processeur est exécuté l'un après l'autre, transmettant ensuite la main au suivant et retournant l'image modifiée. C'est ce que nous faisons ici avec yield.

Et enfin nous retournons l'image qui pourra alors être sauvegardée.

Conclusion

Comme vous pouvez le constater, CarrierWave permet de créer des processeurs de manière fortement générique afin de rendre vos images telles que vous en avez besoin.

Je suis d'ailleurs assez étonné qu'il n'y ait pas déjà des processeurs open source qui se baladent.

Construire une vraie application avec node.js

Nous avons vu dans un article précédent une introduction à node.js. Cependant si vous avez lu cet article, vous vous êtes probablement dit, tout comme moi : "qu'est-ce qu'il veut que je fasse avec ça ? Ca rends mon code illisible son truc".

Et je vous l'accorde. C'est pas lisible et ça deviendra très rapidement impossible à maintenir pour des grosses applications. Alors oui on peut écrire des modules de manière à diviser notre application en de multiples briques. Mais cela ne résouds pas réellement le problème de lisibilité du code.

Nous allons voir comment remédier à cela dans cet article en utilisant connect et express

Npm, rubygems mais en javascript

Afin d'installer les librairies requises, nous allons utiliser l'un des gestionnaires de packets proposés pour node.js, npm

L'installation est simple :

curl http://npmjs.org/install.sh | sudo sh
Suivant votre installation de node (en sudo ou non), vous n'avez pas forcément besoin de sudo sh. Dans ce cas, un sh seul suffit

Connect, rack mais en javascript

Connect est un équivalent de rack pour node. Les applications viennent se greffer dessus sous la forme de middlewares, permettant ainsi de manipuler l'application à n'importe quel niveau.

Installer Connect

Installons Connect ! Pour cela, nous pouvons utiliser le gestionnaire de packets précédemment installé, npm.

npm install connect

Et roulez jeunesse !

Utiliser Connect

Le bête Hello World de Connect, tiré de la documentation est ainsi :

var Connect = require('connect');
var server = Connect.createServer(function(req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World');
});

server.listen(3000);

Nous ne gérons même pas l'url courante (il faudrait, pour cela, faire un if. Je vous le déconseille). Mais nous pouvons renvoyer du contenu texte très simplement. Nous venons de créer notre premier middleware !

Express, sinatra mais en javascript

En ruby, Sinatra permet de créer de petites applications légères tournant sous rack simplement. Express permet de faire la même chose.

Installer Express

Continuons sur notre bonne lancée de npm pour installer Express.

npm install express

Qui a dit que l'informatique était compliqué ? ;)

Utiliser Express

C'est la que node devient réellement intéressant. Avec Express, nous allons enfin pouvoir commencer à créer des applications maintenables. Voici notre Hello World avec Express :

var app = express.createServer();
app.get('/', function(req, res){
    res.send('Hello World');
});
app.listen(3000);

Nous définissons une route sur l'url /, qui rendra alors comme contenu "Hello World". Toute URL autre que celle-ci retournera une erreur 404.

Bien évidemment nous pouvons définir autant d'url que nous désirons avec les méthodes get/post/put/delete.

Fichiers de templates

Reste encore un problème cependant : plus haut, nous définissons un contenu directement dans le javascript. Ceci n'est vraiment pas cool et on voudrait bien avoir un système de templating.

Par défaut, Express supporte haml afin de créer vos vues. Dans le même dossier que le fichier principal de notre application, créons un dossier views.

Dans ce dossier, nous allons ajouter deux fichiers :

layout.haml

Ce fichier sera inclus automatiquement comme layout de votre application. Placez-y votre design.

!!!
%html
  %head
    %title Bonjour tout le monde
  %body
    = body

index.haml

C'est le fichier de notre page d'index.

%h1
  Hello World !

Puis dans notre fichier javascript remplaçons le get de la page principale afin d'afficher le contenu de ce fichier haml.

app.get('/', function(req, res){
    res.render('index.haml');
});

Redémarrez votre serveur; rechargez la page et admirez votre beau template parsé en html et affiché !

Fichiers statiques

Maintenant que nous pouvons afficher nos templates comme nous le désirons, il ne nous reste plus qu'à ajouter un peu de style à la page. Pour cela, il faut inclure un fichier CSS statiques.

Express gère nativement les fichiers statiques. Tout fichier situé dans le répertoire /public et ne correspondant pas à une route déjà définie sera alors affiché.

Configuront le répertoire publique. Avant le app.listen(3000);, ajoutez la ligne suivante :

app.use(express.staticProvider(__dirname + '/public'));

Ainsi nous indiquons à Express d'utiliser le dossier /public courant pour afficher les fichiers statiques.

Créez le fichier /public/style.css Puis incluons ce fichier style.css dans notre layout :

%head
  %title Testing
  %link{rel: 'stylesheet', href: '/style.css' }

Vous n'avez plus qu'à ajouter le design que vous désirez pour votre page.

Conclusion

Du fait de sa relative jeunesse, node évolue encore très rapidement. Et Express en est la preuve visible. Il y a quelques mois encore, réaliser une grosse application entièrement avec node était inenvisageable.

Aujourd'hui, bien que Express ne propose qu'une couche assez basse, il permet déjà de développer de bien meilleures applications que en node brut.

Node nous réserve encore beaucoup de surprises. Les trois années qui viennent devraient être fortement excitantes à ce niveau.

Encore un changement

Mercredi soir, Franck tweetait :

When you don't change anything in your life, you actively reject opportunities. Life is a collection of these, don't avoid living your life.

Ou, en Français :

Lorsque vous ne changez rien dans votre vie, vous rejetez activement des opportunités. La vie est une collection de celles-ci, vivez la.

Le hasard a voulu que cette phrase tombe à pic puisque j'ai justement (encore) un changement en cours, suite à une opportunité que j'ai accepté. En effet il y a quelques semaines, Nicolas m'a proposé de venir travailler pour Oahu Labs. J'ai donc posé ma démission la semaine dernière et signé mon contrat aujourd'hui. Je commencerai à bosser début octobre.

Tu change déjà ? Mais ça fait même pas un an que t'es chez dans ta boite actuelle

La, j'ai envie de dire "et alors ?". Je suis jeune, encore aucune attache. Pourquoi refuser des propositions qui sont particulièrement intéressantes ? Enfin j'espère tout de même ne pas continuer trop sur cette lancée ;)

Mais la boite est à Paris. Tu pars de Lyon ??

Ah bah oui, mais non. Je pars pas de Lyon (encore moins pour aller à Paris ...). Je travaillerai de chez moi et monterai sur la capitale un jour/mois en même temps que les autres de l'équipe (qui bossent également de chez eux).

Nous nous arrangerons au possible pour que ces journées coincident avec des évènements Parisiens tels que les apéros ruby.

Télétravail ? Tu va devenir un associal

Tout de suite ... Au contraire ! Être en télétravail me permettra (encore plus) de priviligier la qualité de mon travail, sans, parfois, temporiser parce qu'il n'est pas encore 18h ... Et puis j'ai 3 collocataires qui vont m'engueuler assez rapidement si je deviens trop associal.

Et puis comme dit plus haut, je suis jeune et sans attaches. C'est le moment idéal pour tester ce mode de vie un peu "anodin".

Par ailleurs je prévois déjà d'appliquer certaines méthodes de gestion du temps afin d'améliorer ma productivité, à commencer par Pomodoro

Qui plus est je suis engagé de manière associative en tant que trésorier de La sCène. Ainsi que dans l'organisation d'évènements geeks avec LyonRB. Je ne vais pas arrêter cela.

Conclusion

Wala ! Je ferme la parenthèse "ma vie, en long, en large et en travers".

Rails : Modèles polymorphes

Supposons que vous soyez en train de développer un blog (du genre de celui-ci ;-) ). Vous avez en conséquent avoir des billets qui peuvent avoir des commentaires. Cependant ces commentaires, eux, peuvent être présent dans un billet ou bien dans une page. Afin de rendre notre code propre, nous ne désirons pas avoir plusieurs types de commentaires. Que vous postiez un commentaire sur une page ou sur un billet, il s'agit d'une entrée dans la même base de données et du même modèle.

Dans le modèle

Pour cela, notre modèle Commentaire sera lié à une page ou un billet de manière polymorphe.

class Comment < ActiveRecord::Base
    belongs_to :commentable, :polymorphic => true
end

class Post < ActiveRecord::Base
    has_many :comments, :as => :commentable
end

class Page < ActiveRecord::Base
    has_many :comments, :as => :commentable
end

Vous pouvez donc assigner un commentaire à un billet ou à une page (mais pas aux deux évidemment). Pour fonctionner, une relation polymorphe requiert tout de même que vous ajoutiez un champ "type" à la base comments.

Ainsi dans vos migrations :

class CreateComments < ActiveRecord::Migration
    def self.up
        create_table :comments do |t|
            t.integer  :commentable_id
            t.string    :type

            t.timestamps
        end
    end

    def self.down
        drop_table :comments
    end
end

Le type correspondra à Post ou Page et permettra à rails de savoir quel est le type de relation du commentaire. Ainsi, depuis votre commentaire, vous pouvez faire :

@comment.commentable

Ce qui retournera l'objet commentable. Une page ou un billet, en fonction du type donné.

Et depuis la page et le billet, vous pouvez faire :

@object.comments

Ce qui retournera tous les commentaires relatifs à cette page ou à ce billet.

Dans le contrôleur

Nos modèles gèrent assez aisément les relations polymorphes. Mais qu'en est-il de notre contrôleur ? Il n'existe pas de solution native. Mais en bidouillant un peu, nous pouvons obtenir quelque chose de propre, nous permettant d'avoir un seul contrôleur pour tous nos commentaires.

Dans le répertoire lib de notre application, créons un module que nous nommerons ParentObject. Votre fichier doit par conséquent être nommé parent_object.rb

module ParentObject

    def self.included(klass)
        klass.class_eval do
            class << self
                attr_reader :parents

                protected
                def parent_resources(*parents)
                    @parents = parents
                end
            end

            def parent_id(parent)
                params["#{parent.to_s}_id"]
            end

            def parent_type
                self.class.parents.detect { |parent| parent_id(parent) }
            end

            def parent_class
                parent_type && parent_type.to_s.classify.constantize
            end

            def parent_object
                parent_class && parent_class.find(parent_id(parent_type))
            end
        end
    end
end

Nous créons un module ruby que nous devrons par la suite inclure dans notre contrôleur. Pour les détails, je vous invite à vous renseigner sur ce en quoi consiste la méta programmation en ruby. Nous n'allons pas sortir du sujet ici.

class << self
    attr_reader :parents

    protected
    def parent_resources(*parents)
        @parents = parents
    end
end

Ici, nous définissons une méthode qui sera accessible à la base de notre contrôleur (en dehors des actions) et qui nous permettra de définir la liste de tous les objets qui pourront être parents de nos commentaires.

def parent_id(parent)
    params["#{parent.to_s}_id"]
end

Cette méthode, à laquelle nous transmettons un type de parent (par exemple "post"), nous retournera l'identifiant de l'élément passé en paramètre. Celui-ci doit être nommé "parent_id" (par exemple "post_id").

def parent_type
    self.class.parents.detect { |parent| parent_id(parent) }
end

Cette méthode parcourera tous les parents transmis et cherchera pour chacun d'eux si un paramètre y est présent. Dès qu'elle en trouvera un, elle retournera ce parent. Ainsi, nous savons si nous avons affaire à un post ou une page.

def parent_class
    parent_type && parent_type.to_s.classify.constantize
end

Cette méthode, en se basant sur la méthode précédente (qui retourne une chaine de caractère) nous retournera la classe de l'objet parent.

def parent_object
    parent_class && parent_class.find(parent_id(parent_type))
end

Enfin cette méthode prends la classe de l'objet parent et effectue la requête appropriée dans la base de données afin d'en retourner l'objet.

Nous n'avons plus qu'à utiliser ceci dans notre application !

class ApplicationController < ActionController::Base
    include ParentObject
end

Dans notre contrôleur principal, nous incluons notre module dernièrement créé, afin de pouvoir l'utiliser dans n'importe quel contrôleur.

class CommentsController < ApplicationController
    parent_resources :post, :page

     def new
         @parent = parent_object
         @comment = Comment.new
     end

     def create
         @parent = parent_object
         @comment = @parent.comments.build params[:comment]

         if @comment.valid? and @comment.save
             redirect_to @parent
         else
             render :new
         end
     end
end

Assez simple non ? :-) Que faisons-nous ?

parent_resources :post, :page

Nous définissons, pour ce contrôleur, la liste de tous les parents possibles. Puis dans les actions new et create nous récupérons l'objet parent et permettons de poster un nouveau commentaire relatif à cet objet.

Les routes

Lorsque nous créons nos routes relatives à ces commentaires, nous utilisons bien évidemment les ressources. Et c'est là le seul endroit ou nous avons un peu de répétition de code à faire.

resources :posts do
    resources :comments
end
resources :pages do
    resources :comments
end

Pour chaque post et chaque page, nous ajoutons des ressources de commentaires.

Ces routes fonctionneront avec rails3. Adaptez les en fonction pour une application rails 2 !

Tester ceci

Vu que j'ai utilisé ceci dans un cas réel avant d'écrire cet article, j'ai également écrit des tests. Vous pouvez visualiser et utiliser ceux-ci comme bon vous semble. Vous les trouverez sur gist@github

Et nos contrôleurs ?

Lorsque vous allez écrire des tests fonctionnels pour vos contrôleurs, vous allez vouloir tester chacun des différents types d'objets parents. Il est cependant inutile de répéter 50 fois votre floppée de tests. Cela ne serait pas très DRY non plus.

require 'spec_helper'
describe CommentsController do

    [:post, :page].each do |parent|

        before :each do
            @parent = Factory(parent)
        end

        describe 'new' do
            it 'should display the form page' do
                get :new, "#{parent}_id".to_sym => @parent.id
            end
        end

        describe 'create' do
            it 'should create a new comment' do
                lambda do
                    post :create,
                        :comment => Factory.attributes_for(:comment),
                        "#{parent}_id".to_sym => @parent.id
                end
            end
        end
    end
end

Que faisons-nous ici ?

[:post, :page].each do |parent|
    ...
end

Nous définissons un tableau contenant tous les types de parents que nous pouvons avoir pour ce contrôleur. Et nous parcourons ce tableau. Tous nos tests seront effectués à l'intérieur de la boucle. Ainsi ils seront effectués pour chacun des éléments parents.

before :each do
    @parent = Factory(parent)
end

Avant chaque test, nous définissons l'objet parent. Ici, nous utilisons factory girl et chaque modèle a sa factory du même nom.

Enfin nous créons nos tests.

get :new, "#{parent}_id".to_sym => @parent.id

Lorsque nous faisons un appel à une page, nous devons transmettre le paramètre qui correspond au parent donné.

Conclusion

Comme vous pouvez le constater ici, il est assez simple d'éviter d'inutiles répétitions de code lorsque vos commentaires sont tous similaires. Si ce n'était pas le cas, peut-être qu'utiliser Single Table Inheritance serait plus utile et modulaire.

Ecrire des modules pour node.js

Nous avons déjà vu une introduction à node.js. Dans cet article, nous allons voir comment il vous est possible, au sein d'une application node.js, de concevoir des modules, vous permettant ainsi de compartimenter votre application et d'en faciliter la maintenance.

Créer un module

Un module est un fichier javascript pouvant être plus ou moins compliqué.

exports.answer = 42; // C'est quoi la question ?

Voici un module ! Dans celui-ci, nous nous contentons de définir une variable "answer", qui sera exportée en dehors du module pour être accessible au reste de l'application.

Nommez ce fichier "question.js" et placez le à la racine de votre projet.

Utiliser ce module

Si on ne peut l'utiliser, ce module va se révéler assez inutile. Incluons le dans notre application !

var sys = require('sys'),
      question = require('question.js')
sys.puts(question.answer); // Affichera 42

Utiliser uniquement des parties de modules

Ici, nous incluons à chaque fois le module dans son entier. Cependant il peut s'avérer utile de n'en inclure que des parties.

var puts = require('sys').puts;
puts("Hello World !");

C'est ce que nous faisons ici, en n'incluant que la méthode puts du module natif sys.

Conclusion

Derrière cet article particulièrement simple se cache le secret qui vous permettra de concevoir des applications modulaires avec Node.js. N'ayez jamais peut de modulariser au maximum votre application. C'est ce qui vous permettra de continuer à la maintenir aisément.

Par ailleurs l'API de node.js contient beaucoup de modules qui vous permettront d'effectuer un fort grand nombre de choses. Regardez-en la documentation !

Pourquoi vous ne devriez pas utiliser la Fork Queue de GitHub

GitHub propose une fonctionnalité qui peut sembler magique : la fork queue. Cette fonctionnalité est en soi assez simple à expliquer : elle affiche à l'écran tous les commits faits dans des forks de votre projet et vous permet, en quelques clics, d'appliquer ceux-ci à votre version du projet. Vu comme cela, c'est assez sympa. En pratique, je vous déconseille de l'utiliser. Nous allons voir pourquoi et comment faire sans.

Pourquoi ne dois-je pas utiliser la fork queue ?

La fonctionnalité de la fork queue est similaire à ce que vous pourriez faire manuellement avec git (et que nous détaillons plus bas). Elle prends les commits que vous cochez, en fait un [patch]/patch et commit celui-ci dans votre repository. C'est une fonctionnalité de GIT largement utilisée. C'est ainsi que vous serez ainsi invité à faire si vous soumettez un patch à rails.

Ici cependant, un petit problème se pose : vous pouvez visualiser le contenu du commit. Mais pas le tester. Dès que vous cochez et acceptez ce commit, celui-ci est validé dans votre repository distant et accessible à toute personne ayant accès au projet. En conséquent, si ce commit semble en apparence correct mais qu'il casse une autre fonctionnalité et que vous n'avez pas constaté cela au premier coup d'oeil, vous ne vous en rendrez compte que lorsque vous récupérerez à nouveau le contenu de votre projet et que vous exécuterez ses tests. Et si vous avez accepté plusieurs commits, vous risquez d'avoir beaucoup de mal à trouver lequel a implémenté ce bug et à le corriger.

Le fait qu'un commit passe correctement, c'est à dire qu'il ne crée pas de conflit lors de son merge ne signifie pas qu'il ne casse pas de fonctionnalité par ailleurs.

Oui mais je fais comment à la place ?

Grande question ! :) Tout d'abord vous devez vous imaginer que chaque repository GIT distant est en fait une branche différente. Lorsque vous faites un fetch, vous récupérez le contenu de cette branche.

Supposons que un utilisateur "joe" ait forké mon projet open source jack. Joe a donc son fork situé sur github à joe/jack. Il fait quelques commits puis pousse les modifications effectuées. Pour ma part, je veux placer ces modifications dans le repository principal.

git clone git@github.com:dmathieu/jack.git

Je commence par cloner le repository. A supposer que je ne l'ai pas encore ne local.

git remote add joe git://github.com/joe/jack.git

Ensuite je dois ajouter un nouveau repository distant, que je vais nommer joe. Je pourrai ainsi récupérer les derniers commits effectuée.

git fetch joe master:joe

Maintenant je récupère les commits effectués par joe sur son repository et les place dans une nouvelle branche locale, que je nomme joe.

git checkout joe

Je me rends dans cette nouvelle branche. Le répertoire courant de mon projet contient maintenant les modifications effectuées par joe. Je peux ainsi effectuer tranquillement les tests que je désire sur cette application et ainsi vérifier que toutes les modifications effectuées par joe sont correctes.

Une fois que tous mes tests sont effectués, je vais les valider dans ma branche master. Pour cela je dois remettre cette branche master comme branche active.

git checkout master

Puis je merge la branche joe dans celle-ci.

git merge joe

Tous les commits effectués par joe ont ainsi été approuvés dans ma branche master. Je n'ai plus qu'à pousser les commits ajoutés

git push origin master

C'est bien évidemment plus long et plus complexe que d'utiliser la Fork Queue. Mais la qualité de votre application s'y retrouvera puisque vous limiterez fortement le risque d'erreurs non désirées.

Et si je ne veux accepter que certains commits ?

Supposons que joe ait effectué deux commits. Un est intéressant. Quant à l'autre il casse tout ... Nous ne désirons pas accepter les deux, mais un seul. Pour cela, nous allons utiliser cherry-pick.

Cherry Pick nous permet de sélectionner un identifiant de commit et de l'approuver dans master. Commençons par trouver cet identifiant de commit. Avec joe comme branche active, tapez

git log

Qui vous retournera le log des derniers commits effectués. Supposons que vous ayez le log suivant :

commit 7797aec6a35d928045f2a90fdddd9001dbf1a567
Author: Joe Doe <joe.doe@example.com>
Date:   Tue Jun 15 12:23:45 2010 +0200

    Ca sers à rien ça

commit 1cd60cc03530cd88689f19e6c5e3fd5f95b4ed70
Author: Joe Doe <joe.doe@example.com>
Date:   Tue Jun 15 11:56:38 2010 +0200

    Ouh lala le méchant bug

commit 7feb18184cdeff2363d6b608201ac6431f53e070
Author: Damien Mathieu <damien@example.com>
Date:   Tue Jun 15 11:49:52 2010 +0200

    Release v1.0.0

Vous pouvez voir que chaque commit est identifié par un hash. C'est en passant ce hash que nous allons pouvoir identifier un commit en particulier et effectuer le cherry-pick. Retournez dans la branche master.

Puis nous incluons uniquement le premier commit de Joe.

git cherry-pick 1cd60cc03530cd88689f19e6c5e3fd5f95b4ed70

Et voila ! Vous n'avez pas mergé tous les commits de joe dans votre repository principal, mais que celui que vous désirez.

GIT est capable de reconnaitre une partie de hash. Ainsi, nous aurions pu taper `git cherry-pick 1cd60cc`, le commit aurait été reconnu et aurait été mergé dans dev

Conclusion

La fonctionnalité de Fork Queue est peut-être aisée lorsque votre projet est petit et que vous avez peu de commits différents ou de risques de dommages collatéraux. Mais dès que celle-ci commence à prendre de l'ampleur, vous devrez alors appliquer les commits de personnes tiers avec plus d'attention. La, la Fork Queue devient un ennemi et un facteur d'erreurs pour votre application. Utilisez la avec parcimonie !

Création de générateurs pour rails 3

Lorsque vous développez un plugin pour rails, il peut être utile que l'utilisateur de ce plugin crée divers fichiers (par exemple de configuration) dans son projet. Par exemple, rspec crée le fichier spec/spec_helper.rb. Ainsi que vos fichiers de test à chaque fois que vous créez un nouveau modèle, contrôleur ou autre.

Pour faciliter la création de ces fichiers dans votre projet, rails propose divers helpers permettant de créer des templates de fichiers. Supposons un plugin que nous nommerons foo. Dans ce plugin, nous désirons créer le modèle User. Ce modèle contiendra, comme son nom l'indique, des utilisateurs !

Un premier générateur

Commencez par créer le dossier foo/lib/generators, dans lequel vous créerez le fichier user_generator.rb. Dans ce fichier, nous placerons la classe suivante :

Nous créons une classe du nom de notre générateur (le nom est très important et doit correspondre au nom du fichier. Sinon rails ne trouvera pas le générateur).

Puis nous définissons le chemin vers les fichiers de template :

source_root File.expand_path("../templates", __FILE__)

Enfin nous définissons une méthode qui créera le fichier de notre modèle et y insérera le contenu du template :

def add_user_model
    template "user_model.rb", "app/models/user.rb"
end

Vous pouvez créer autant de méthodes que vous le désirez. Toutes les méthodes publiques seront exécutées dans l'ordre dans lequel elles sont définies.

Puis créez un fichier foo/lib/generators/templates/user_model.rb Qui contiendra ceci :

class User < ActiveRecord::Base

end

Parce que notre générateur s'appelle "user", vous pouvez l'exécuter après avoir installé le plugin dans votre projet rails, en tapant :

script/rails generate foo:user

Ce qui aura pour effet de créer le modèle User dans le répertoire approprié.

Générateurs nommés

Si vous avez un petit peu d'expérience avec rails, vous avez probablement déjà utilisé l'un des générateurs natifs. Par exemple nous pourrions créer notre précédent modèle User de la manière suivante :

script/rails generate model user

Si vous souhaitez créer ce type de générateur, vous pouvez faire hériter celui-ci de Rails::Generators::NamedBase. Ainsi, diverses méthodes supplémentaires seront à votre disposition.

  • class_path et file_name sont extraits des paramètres passés. Par exemple si vous avez entré admin/posts, class_path sera égal à admin et file_name à posts.
  • file_path est class_path et file_name séparés par un /.
  • class_name est le nom de la classe. Par exemple admin/posts deviendrait Admin::Posts
  • human_name est le nom de la classe "humanisé". Par exemple admin/posts deviendrait Admin posts.
  • plural_name est le nom du fichier au pluriel. Par exemple post deviendrait posts.
  • i18n_scope est le file_path avec les slash (/) remplacé par des points (.). Par exemple admin/posts deviendrait admin.posts.

Ainsi nous pourrions créer un générateur plus générique avec la commande

script/rails generate foo:user people

Qui créerait un modèle nommé People et non plus User. Exemple :

Vous noterez l'utilisation de la variable file_name nous permettant de définir de manière dynamique le nom du fichier qui sera créé.

Et le contenu de notre template :

class <%= class_name %> < ActiveRecord::Base

end

Vous noterez l'utilisation de la variable class_name qui nous permet de définir de manière dynamique le nom de notre nouveau modèle.

Conclusion

Avec les générateurs, vous pouvez ainsi créer tous les fichiers dont votre plugin ou gem aura besoin pour tourner correctement autour de votre application rails. Si vous souhaitez en savoir plus quant à ces générateurs, je vous invite à lire l'article Creating and Customizing Rails Generators.

Notifications push avec Android

La dernière version de l'OS pour appareils mobiles Android, FroYo (Android 2.2) a été annoncée lors du Google I/O le 20 mai dernier.

Cette nouvelle version compte de nombreuses nouveautés. Notamment le support de flash, la possibilité de transformer un téléphone en hotspot wifi. Et une autre fonctionnalité, Android Cloud Device Messaging. C2DM permet à une application web de transmettre des données à une application installée sur un appareil Android.

Ainsi, si votre application est un lecteur de flux RSS par exemple, votre serveur pourra transmettre de manière asynchrone à un appareil qui utilise celle-ci que de nouveaux éléments sont à lire. Alors, l'application n'aura plus qu'à aller automatiquement rafraichir la liste des entrées.

Voici en bref comment fonctionne C2DM :

  • Lorsque vous installez l'application sur un appareil Android, celle-ci s'enregistre auprès de Google, obtient un identifiant d'enregistrement et transmet cet identifiant à votre serveur en ligne.
  • Lorsque le serveur a besoin de pousser un message à l'appareil, il fait une requête POST chez Google.
  • Google transmet ce message à l'appareil, qui exécute alors l'évènement approprié.

A n'importe quel moment, l'application peut se désinscrire de C2DM afin d'arrêter de recevoir les notifications de push.

Android Cloud Device Messaging est encore en version alpha car supporté par très peu d'appareils mobiles Android.
En conséquent cet article est plus théorique que autre chose car je n'ai pas pu tester son utilisation sur une réelle application.

Enregistrement d'une application à C2DM

Afin de pouvoir recevoir des notifications, votre application doit s'enregistrer dans C2DM. Pour cela, vous devez tout d'abord avoir les permissions appropriées configurées dans votre fichier AndroidManifest.xml. Voici un exemple de contenu pour ce document permettant à votre application de recevoir des push :

<manifest package="com.example.myapp" ...>

    <!-- Seule cette application peut recevoir les messages et le résultat de l'enregistrement à C2DM -->
    <permission android:name="com.example.myapp.permission.C2D_MESSAGE" android:protectionLevel="signature" />
    <uses-permission android:name="com.example.myapp.permission.C2D_MESSAGE" />

    <!-- Cette application a l'autorisation de recevoir des messages -->
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

    <!-- Il est nécessaire d'avoir l'autorisation d'accéder à Internet pour pouvoir recevoir les messages. -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Seuls les serveurs C2DM peuvent transmettre des messages à l'application. -->
    <receiver android:name=".C2DMReceiver" android:permission="com.google.android.c2dm.permission.SEND">
        <!-- L'application peut recevoir des messages -->
        <intent-filter>
            <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            <category android:name="com.example.myapp" />
        </intent-filter>

        <!-- L'application peut recevoir l'identifiant d'enregistrement au serveur C2DM -->
        <intent-filter>
            <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
            <category android:name="com.example.myapp" />
        </intent-filter>
</receiver>

Notre application est maintenant autorisée à s'enregistrer à C2DM. Mais cela n'est pas encore fait. Pour cela nous allons transmettre un Intent permettant d'identifier notre application.

Intent regIntent = new Intent("com.google.android.c2dm.intent.REGISTER");
regIntent.putExtra("app", PendingIntent.getBroadcast(this, 0, new Intent(), 0);
regIntent.putExtra("sender", "you@example.com");
startService(regIntent);

public void onReceive(Context context, Intent intent) {
    if (intent.getAction().equals("com.google.android.c2dm.intent.REGISTRATION")) {
        String registration = intent.getStringExtra("registration_id"); 

        if (intent.getStringExtra("error") != null) {
             // L'enregistrement a échoué. Réessayez plus tard.
        } else if (registration != null) {
            // L'enregistrement a correctement été effectué.
           // Transmettez ici l'identifiant d'enregistrement au serveur Web qui transmettra les messages.
        }
    }
}

Nous commençons ici par initialiser un Intent de type C2DM.intent.REGISTER. Puis nous transmettons deux valeurs à celui-ci :

  • Une identification de notre application, permettant de restreindre l'envoi de messages de push uniquement au serveur autorisé à cela.
  • Un email, identifiant du compte autorisé à transmettre des messages de push à l'application. C'est généralement un email défini par le développeur de l'application.

    startService(regIntent);

Puis nous initialisons le service. Ensuite nous écoutons la réception de tout message. Si il s'agit d'un message C2DM indiquant que nous avons réussi à nous enregistrer, nous validons cet enregistrement.

String registration = regIntent.getStringExtra("registration_id"); 

Nous récupérons l'identifiant d'enregistrement transmis à l'application.

if (regIntent.getStringExtra("error") != null) {
     // L'enregistrement a échoué. Réessayez plus tard.
} else if (registration != null) {
    // L'enregistrement a correctement été effectué.
    // Transmettez ici l'identifiant d'enregistrement au serveur Web qui transmettra les messages.
}

Enfin nous vérifions que l'enregistrement a été correctement été effectué et que l'identifiant a été correctement créé. Si c'est bien le cas, il ne vous restera plus qu'à transmettre cet identifiant à votre serveur web qui pourra alors commencer à envoyer des messages de push.

Les messages de push pourront maintenant être transmis à notre application. Voyons comment les recevoir et les traiter.

Transmission d'un message à l'application

Pour pouvoir transmettre des messages de push, il faut que votre serveur soit capable de transmettre des données en POST à Google. La requête doit être faite à l'URL https://android.apis.google.com/c2dm/send

Plusieurs paramètres sont requis pour transmettre un message à l'application :

  • registration_id - Il s'agit de l'identifiant d'enregistrement transmis à l'application lorsque celle-ci a activé C2DM.
  • collapse_key - Un identifiant permettant de regrouper divers messages similaires. Ainsi, si l'appareil est hors ligne lorsque vous envoyez ce message, il ne sera transmis que plus tard. Tous les messages ayant la même collapse_key seront transmis en un seul lors de la reconnexion de l'appareil, évitant ainsi trop de messages similaires en simultané.
  • data. - Une liste de clés/valeurs qui seront transmises à l'application. Vous pouvez en transmettre autant que vous le désirez.
  • delay_with_idle - Si l'appareil est en veille lorsque le message est envoyé, il ne sera alors pas reçu tout de suite mais uniquement lorsque l'utilisateur réactivera la machine.
  • Authorization: GoogleLogin auth=[AUTH_TOKEN] - Votre clé d'identification GoogleLogin.

La clé d'identification est transmise au serveur lorsque vous utilisez ClientLogin for Installed Applications. Elle permet de vous identifier. Vous devez être identifié avec l'email transmis et autorisé lorsque l'appareil s'est enregistré auprès de C2DM.

Google répondra alors à cette requête avec trois codes HTTP possibles :

  • 200 - Aucune erreur technique ne s'est produite. Cela ne signifie cependant pas que votre message a été envoyé. Le contenu de la page renvoyée peut contenir les choses suivantes :
    • id=<ID> - Identifiant du message si celui-ci a correctement été transmis.
    • Error=<code d'erreur> - Code d'erreur si le message n'a pas pu être transmis.
      • QuotaExceeded - Trop de messages ont été transmis. Réessayez plus tard.
      • DeviceQuotaExceeded - Trop de messages ont été transmis sur un appareil en particulier. Réessayez plus tard.
      • InvalidRegistration - identifiant d'enregistrement manquant ou invalide. Vous devez arrêter d'envoyer des messages à cet appareil.
      • NotRegistred - L'enregistrement n'est plus valide. Vous devez arrêter d'envoyer des messages à cet appareil.
      • MessageTooBig - Le message est trop gros. Réduisez le.
      • MissingCollapseKey - la collapse_key est manquante et doit être include.
  • 503 - Indique que le serveur est actuellement indisponible. Un entête Retry-After est alors transmis. Votre application ne doit pas retransmettre de requête avant la date transmise.
  • 401 - Indique que votre clé ClientLogin n'est pas valide.

Réception d'un message depuis l'application

Une fois que votre serveur a correctement transmis un message à Google, celui-ci retransmettra ce même message à votre appareil Android. A vous alors de l'intercepter. Pour cela, de la même manière que lorsque tout à l'heure, nous écoutions la réception de la validation de l'enregistrement de l'appareil, nous allons écouter la réception d'un nouveau message.

public void onReceive(Context context, Intent intent) {
    if (intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")) {
        String accountName = intent.getExtras().getString(Config.C2DM_ACCOUNT_EXTRA);
        String message = intent.getExtras().getString(Config.C2DM_MESSAGE_EXTRA);

        Log.d("PUSH", "Requête de push transmise pour le compte " + accountName);
     }
 }

Nous écoutons donc la réception d'un nouveau message.

if (intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")) {

Et nous vérifions qu'il s'agit bien de la réception d'un message de push.

String accountName = intent.getExtras().getString(Config.C2DM_ACCOUNT_EXTRA);

Ici, nous récupérons le nom du compte Google depuis lequel le message de push a été transmis. Toutes les clés/valeurs que vous aurez transmis dans la requête POST seront transmis et accessibles ici.

Log.d("PUSH", "Requête de push transmise pour le compte " + accountName);

Enfin ici, nous ne faisons que loguer la réception d'un nouveau message de push. A vous de faire ce que vou désirez.

Désinscription d'une application de C2DM

A tout moment, vous devez permettre aux utilisateurs de vos applications d'arrêter de recevoir des messages de push. Pour cela, il vous suffit de définir un Intent com.google.android.c2dm.intent.UNREGISTER, qui transmettra alors à Google votre demande de désinscription du push pour cette application.

Intent unregIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
unregIntent.putExtra("app", PendingIntent.getBroadcast(this, 0, new Intent(), 0));
startService(unregIntent);

Vous pouvez aisément constater que nous définissons notre intent de la même manière que nous avons défini celui nous permettant de nous enregistrer au push, seul le nom de celui-ci change.

Conclusion

Avec C2DM, Google nous propose (enfin) une fonctionnalité permettant de transmettre des notifications diverses aux applications installées sur un appareil de manière intantanée (ou presque).

A vous de faire preuve d'imagination pour développer ces applications !