22 juillet 2015

BizCommands

Business command objects, or “BizCommands”, are at the core of our architecture.

There are several applications, or “AppCenters”, and one business core, or “BizMaster”.

States

  • Each application may have its own data storage.
  • The real data is the data managed at the business level.
  • The application data may be seen as a view of the business state.
  • States (that is, data storages) can be seen as caches.

Commands

  • Each business command is a transition from a business state “n” to another state “n+1”.
  • Ideally, the business data could be reconstructed from a savepoint, by performing all business commands again.
  • Business commands hold the real information.

Default Architecture

The applications post business commands, and the business core is responsible for handling them. Then the business core calls the application level back, which, notified of a business change, performs the corresponding changes in the application data. This may be seen as caching.

This can be done synchronously or asynchronously. The default mode is asynchronous.

The default architecture follows the following pattern:

Default Architecture

The two data models “app data1” and “app data2” may be completely different.

The “b_converter” parts make sure the BizMaster doesn’t access the application data directly.

The main idea is that it’s the BizMaster that answers business commands sent by the AppCenters, and not the AppCenters that call the BizMaster synchronously.

Let’s look in detail at the communication mechanism between an AppCenter and the BizMaster:

Details

We can see that:

  • The AppCenter never connects to the BizMaster.
    • Note: This can be nice even from a security perspective.
  • The BizMaster connects to the AppCenter:
    1. Via a queue subscription, to get notified when a new business command is posted.
    2. Synchronously, to manage remote commands.
    3. Synchronously, to inject changes into the app data.

Generally, business commands are also stored within the “business data” resource on the BizMaster’s side (generally a database), but semantically, business commands and business data may be separated.

Workers are not event-driven: They are really asynchronous. Think of an infinite loop performing business tasks. Using a queue for notifications is a default implementation.

On the contrary, the business converter (“b_converter”) and the command manager (“cmd_mgr”) are event-driven.

Benefits

  • All business command is logged, even when it didn’t succeed: Very easy to audit the app from a business perspective.
  • Data migrations and app migrations can be run smoothly. If a delta exists between business states before and after a migration, it means that some new business commands have been performed in the old system prior to switching to the new one: This can be resorbed by running those same business commands on the new system (this should be idempotent with respect to the corresponding app data, though).
  • Data replication (even at the business level) is non-blocking.

Following are a list of migration scenarios, taking benefits of the loose coupling between the AppCenter and the BizMaster.

  • Migration Scenario #1: webapp
  • Migration Scenario #2: b_converter
  • Migration Scenario #3: webapp + app data + b_converter
  • Migration Scenario #4: business workers
  • Migration Scenario #5: b_converter + business workers
  • Migration Scenario #6: business data + business workers

Migration Scenario #1: webapp

We switch from a v0 app to a v1 app, but no changes are required in the app data.

Deploy the new app:

MigrationScenario1_1

Link the new app to the existing resources:

MigrationScenario1_2

Switch the entry point (namely, update the nginx configuration and restart nginx):

MigrationScenario1_3

Get rid of the old app:

MigrationScenario1_4

Migration Scenario #2: b_converter

We switch from a v0 converter to a v1 converter, but no changes are required in the app data or in the web app.

Deploy the new converter:

MigrationScenario2_1

Link the new converter to the existing resources:

MigrationScenario2_2

Stop the existing workers. Incoming business commands will be held in the queue. Restart the workers, having them point to the new converter:

MigrationScenario2_3

Get rid of the old converter:

MigrationScenario2_4

Migration Scenario #3: webapp + app data + b_converter

Here we have a data migration.

The old data can take a while to copy and translate into the new schema, so it’s likely the system will receive some new business commands from the moment the copy starts and the moment it ends.

Say we start the copy+translation while the v0 app data is in state “1205001”. We capture the current pending business commands.

MigrationScenario3_1

When we finish the copy+translation, we have a v1 app data in state “1205001”, and a v0 app data in state “1205489”.

The delta results from the business commands that have been performed (and added to app data v0) during the copy.

MigrationScenario3_2

At this point, we can fill the delta with some ad hoc batch programs:

MigrationScenario3_3

Once the batch programs are done reaching the “1205489” state in our v1 data, some more new business commands have been added to our v0 data state, which is now “1205506”.

We shutdown the workers, so the v0 data will not be updated any more, and keep running the batch programs.

See that the web app is still running as v0, recording business commands and holding them in the queue, but no business command is actually performed.

From a user perspective, we can also freeze the web app (message: “Sorry, we are in maintenance mode!”)

MigrationScenario3_4

Once the “1205506” state has been reached for our v1 data, we can shutdown the batch programs, restart the workers and have them point to the new v1 converter. It is safe to switch the app to v1.

MigrationScenario3_5

We get rid of old components.

Migration Scenario #4: business workers

Deploy the new workers:

MigrationScenario4_1

Make them point to the local resources, but not yet subscribe to the queue and/or call the remote b_converter:

MigrationScenario4_2

Stop the old workers. At this point, no business command is being performed at all.

MigrationScenario4_3

Make the new workers point to the remote b_converter and subscribe to the queue:

MigrationScenario4_4

Migration Scenario #5: b_converter + business workers

Deploy the new components:

MigrationScenario5_1

Link them to the resources:

MigrationScenario5_2

Stop the old workers:

MigrationScenario5_3

Start the new workers and have them point to the new converter:

MigrationScenario5_4

Migration Scenario #6: business data + business workers

Just as in Scenario #3, we have a data migration.

Say we start the copy+translation while the v0 business data is in state “9315001”. We capture the current pending business commands.

MigrationScenario6_1

When we finish the copy+translation, we have a v1 business data in state “9315001”, and a v0 business data in state “9315489”.

The delta results from the business commands that have been performed during the copy.

MigrationScenario6_2

At this point, we can fill the delta with some ad hoc batch programs:

MigrationScenario6_3

Once the batch programs are done reaching the “9315489” state in our v1 data, some more new business commands have been added to our v0 data state, which is now “9315506”.

We shutdown the old workers, so the v0 data will not be updated any more, and keep running the batch programs.

See that the web app is still recording business commands and holding them in the queue, but no business command is actually performed.

From a user perspective, we can also freeze the web app (message: “Sorry, we are in maintenance mode!”)

MigrationScenario6_4

Once the “9315506” state has been reached for our v1 data, we can shutdown the batch programs, and start the new workers.

MigrationScenario6_5

We get rid of old components.

19 mars 2014

Appium et cibles multiples

Pour les tests automatisés d’applications iOS, j’ai dit utiliser Appium — voir : iOS et tests automatisés

Pour plusieurs raisons (public ciblé, non régression, couverture des tests, captures d’écrans…) il est important de tester sur plusieurs cibles. En se limitant à l’iPhone / iPod Touch, citons en particulier :
  • iOS Simulator : iOS 6.0 / 6.1 (non Retina)
  • iOS Simulator : iOS 6.0 / 6.1, Retina 3.5-inch
  • iOS Simulator : iOS 6.0 / 6.1, Retina 4-inch
  • iOS Simulator : iOS 7.0 / 7.1, Retina 3.5-inch
  • iOS Simulator : iOS 7.0 / 7.1, Retina 4-inch 
  • (real device) iPod Touch 4e génération : iOS 6.1.3
  • (real device) iPod Touch 5e génération : iOS 7.0.3
iOS Simulator Résolution iOS 6.0 / 6.1 iOS 7.0 / 7.1
iPhone 320×480 OK n/a
iPhone Retina 3.5-inch 640×960 OK OK
iPhone Retina 4-inch 640×1136 OK OK

Voici comment je m’en sors avec Appium 0.16.0, Xcode 5.1 et Xcode 4.6.3 installés sous OS X 10.8.5.

Pour iOS 7.1, facile, puisque c’est le niveau de SDK du simulateur par défaut. On fera donc simplement dans le test JUnit :
final DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("deviceName", "iPhone Retina (3.5-inch)");
driver = new RemoteWebDriver(new URL("http://localhost:4723/wd/hub"), capabilities);
ou :
(...)
capabilities.setCapability("deviceName", "iPhone Retina (4-inch)");
(...)
Rappelons que pour le simulateur, la capability « app » pointe vers l’application sur le poste de développement :
capabilities.setCapability("app", "/Users/dandriana/Library/Developer/Xcode/DerivedData/"
    + MON_APPLI_DIR // XXX-abcdefetc.
    + "/Build/ProductsDebug-iphonesimulator/"                       
    + MON_APPLI_NAME + ".app"
);
Pour iOS 6.1, c’est un peu plus compliqué, car pour lancer le simulateur, Apple Instruments sélectionne systématiquement le dernier SDK du simulateur par défaut dans Xcode. Il va donc falloir se passer de Xcode 5.1 et utiliser Xcode 4.6.3, grâce à la ligne de commande suivante :
$ sudo xcode-select -switch /Applications/Xcode-4.6.3.app/Contents/Developer/
Et pour choisir la résolution, c’est le même code que ci-dessus, avec comme possibilités :
capabilities.setCapability("deviceName", "iPhone");

capabilities.setCapability("deviceName", "iPhone Retina (3.5-inch)");

capabilities.setCapability("deviceName", "iPhone Retina (4-inch)");
Il n’y a pas à changer la capability "app".

En ce qui concerne les real devices, Appium va seulement avoir besoin de l’UDID et du bundleID :
capabilities.setCapability("device", "f98cda012etc.");
capabilities.setCapability("app", "com.avcompris.MON-APPLI"); 
À propos des simulateurs, pour info voici ce qui est donné par défaut :
$ xcode-select --print-path 
/Applications/Xcode.app/Contents/Developer 
$ defaults read com.apple.iphonesimulator 
{ 
   LocationMode = 3102; 
   SimulateDevice = "iPhone Retina (4-inch 64-bit)"; 
   SimulatorWindowCenterX = 657; 
   SimulatorWindowCenterY = 497; 
   SimulatorWindowLastScale = 1; 
   currentSDKRoot = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.1.sdk"; 
   windowOrientation = 3; 
}
Modifier ces valeurs soi-même (defaults write) ne change rien : Apple Instruments les remet à jour avec le SDK du simulateur par défaut.

Notons aussi que, même si Xcode 4.6.3 est également installé sur ma machine et sert bien d’aiguillage pour lancer un autre simulateur que celui du SDK 7.1, il est a priori impossible d’ouvrir le projet iOS avec cette installation de Xcode, puisque des formats de fichiers, notamment les Xib, sont incompatibles entre 4.6.3 et 5.1.

03 février 2014

iOS et tests automatisés

En faisant un peu de programmation iOS, je me suis posé la question d’automatiser mes tests. Apple fournit une panoplie d’outils tels que UIAutomation, qui permet d’écrire des tests automatisés en Javascript et inclus dans le projet, mais vu que j’ai déjà quelques sanity checks codés en Java (typiquement, l’extraction de points de contrôles des fichiers statiques xib de la configuration de l’UI, et leur vérification), bah, j’aimerais bien :
  1. pouvoir coder mes tests en Java,
  2. qu’ils se lancent à partir de Maven
  3. et que les résultats soient lisibles par Jenkins.
Je débute et j’explore.

Appium


J’apprends qu’on peut utiliser un WebDriver (oui, le même qu’avec Selenium) qui va taper sur un outil Appium (open source), qui va lui-même interagir avec Instruments (Apple), qui va déclencher l’application iOS à tester.

Note : j’ai découvert Appium grâce à cette vidéo : GTAC 2013: Appium: Automation for Mobile Apps — 43 min.

On a deux chaînes intéressantes :
  • Dev : Eclipse → JUnit → Appium (en local) → iOS Simulator
  • CI : Jenkins → Maven → JUnit → Appium (par HTTP) → iOS Simulator

Cela dit, le WebDriver ne permet pas de récupérer toutes les informations de l’application iOS, et notamment la totalité des caractéristiques des UIViews. Il va donc falloir instrumentaliser nous-mêmes l’application pour en savoir davantage.

J’ai par exemple quatre cas de tests qui font intervenir des chaînes différentes :
  1. Vérification de layout
    — exemple : tels boutons doivent être alignés et avoir les mêmes dimensions
     
  2. Vérification de la mise à jour du texte d’un UILabel
    — exemple : après avoir cliqué sur tel bouton, tel label doit afficher tel texte
      
  3. Vérification de la mise à jour de la couleur d’un UILabel
    — exemple : après avoir cliqué sur tel bouton, tel texte doit être affiché de telle couleur

A. Vérification de layout


— exemple : tels boutons doivent être alignés et avoir les mêmes dimensions

C’est la toute première chose à laquelle j’ai dû penser après avoir vu que,  pour définir le design des écrans, Interface Builder allait être utilisé plus ou moins systématiquement. Ce machin est tellement instable que même en consultant ses propres travaux précédents et en faisant « Pomme-S » (réflexe d’avant le cloud), on n’est pas sûr que des modifications n’aient pas eu lieu. En sus, on a beau définir toutes les contraintes imaginables, elles sautent à la moindre occasion et/ou au final ne s’appliquent pas comme prévu.

La seule vérification fiable est au runtime, d’où l’écriture de tests qui pilotent iOS Simulator.

Voyons comment on s’y prend.

Primo, on va donner dans Xcode (en fait, dans le xib) un identifiant au bouton auquel on va vouloir accéder en test. À savoir, le xib permet entre autres de définir pour une UIView les identifiants suivants :
  • userLabel : un texte arbitaire pour aider à retrouver ses petits dans Interface Builder → ne passe pas l’étape du build : n’est pas accessible au runtime
  • tag : un numérique (1, 2, 3…), interne → éventuellement utile pendant le coding Objective-C, présent ensuite au runtime, mais pas accessible avec le WebDriver
  • accessibilityLabel : utilisé par VoiceOver notamment → accessible par By.name(xxx)
  • accessibilityIdentifier : interne → accessible par By.id(xxx)

Pour définir un tag, aller dans l’Attributes inspector :


Pour définir un accessibilityLabel, aller dans l’Identity inspector : 


Rappel : c’est cet accessibilityLabel que lira à haute voix VoiceOver s’il est activé.

Pour définir un accessibilityIdentifier, aller aussi dans l’Identity inspector…


et ajouter à la main un champ String « accessibilityIdentifier » :


On accède ensuite au bouton depuis le code Java du test de la façon suivante :
final WebElement button = driver.findElement(By.id("L20"));
// or: button = driver.findElement(By.name("L20"));
// or: button = driver.findElement(By.name("Bouton de validation"));
et à ses propriétés comme ceci :
assertEquals(75, button.getLocation().x);
assertEquals(61, button.getLocation().y);
assertEquals(106, button.getDimension().width);
assertEquals(127, button.getDimension().height);
Noter qu’on peut également aller chercher soi-même ces informations dans le driver.getPageSource(), où elles apparaissent comme ceci :
{
 "name":"L20",
 "type":"UIAButton",
 "label":"Bouton de validation",
 "value":"Bouton de validation",
 "rect":{"origin":{"x":75,"y":61},"size":{"width":106,"height":127}},
 "dom":null,"enabled":true,"valid":true,"visible":true,
 "children":[],
 "hint":"Blah Blah Blah"
}
La présence des deux attributs accessibilityIdentifier et accessibilityLabel, ou d’un seul des deux, a une influence sur les méthodes qu’on va utiliser et les attributs renvoyés :

id, label="toto" id="123", label="toto" id="123", label
findElement(By.id(?)) By.id("toto") By.id("123") By.id("123")
findElement(By.name(?)) By.name("toto") By.name("123") By.name("123")
getAttribute("name") "toto" "123" "123"
getAttribute("value") "toto" "toto" "" (vide)


Bon, je préfère évidemment l’accessibilityIdentifier :
  • il peut être unique à travers les écrans de l’appli, alors que l’accessibilityLabel sera probablement répété dans des pages connexes (ben oui, pour des histoires d’accessibilité, on répète forcément les mêmes accessibilityLabels pour les mêmes boutons, afin de ne pas perdre l’utilisateur),
  • il est fixe dans le temps, alors que l’accessibilityLabel sera appelé à évoluer en fonction des retours utilisateurs,
  • il n’a pas à être traduit.

B. Vérification de la mise à jour du texte d’un UILabel


— exemple : après avoir cliqué sur tel bouton, tel label doit afficher tel texte

Bon, on se dit que ça devrait être simple :

final WebElement button = driver.findElement(By.id("truc"));
final WebElement label = driver.findElement(By.id("machin"));

assertEquals("Avant", label.getText()); // Alors, ça marche ?
button.click();
assertEquals("Après", label.getText()); // Alors, ç
a marche ?
Eh ben non, ça ne fonctionne pas, getText() ne renvoie en fait rien, et ce simplement parce qu’il y a un bug dans l’UIAutomation d’Apple : Appium does not return correct value from UIAStaticText

Dommage, l’API de WebDriver avait pourtant tout prévu : la méthode getText() existe pour WebElement, ça sentait à peu près bon.

Bien, alors quel est le souci, en fait ? C’est que lorsqu’Appium traduit nos appels Java en appels Javascript pour UIAutomation, celui-ci a un bug qui ne récupère pas les bonnes valeurs auprès de l’environnement d’exécution de notre application en Objective-C.

Du coup, on va aller invoquer Objective-C directement depuis Java, et cela nous amène au cas suivant.

C.  Vérification de la mise à jour de la couleur d’un UILabel


— exemple : après avoir cliqué sur tel bouton, tel texte doit être affiché de telle couleur

Pour ce cas, c’est clair : l’interface WebDriver ne prévoit absolument pas de récupérer la couleur d’un élément. Il y a bien une méthode getAttribute(xxx) dans WebElement, mais, euh, un coup d’œil à la pageSource (voir plus haut) confirme que c’est assez pauvre et qu’en l’occurrence la couleur n’en fait pas partie.

Il va falloir instrumentaliser d’une façon ou d’une autre notre propre application iOS en Objective-C, d’une façon qui rende ces informations disponibles au code Java de nos tests.

Alors, en ce qui me concerne, j’ai ajouté ceci :
  1. Un bouton « DumpUIViews » à mon IHM, actif uniquement en phase de debug (et donc désactivé à tous les niveaux en phase de release).
     
  2. Une méthode « dumpUIViews » qui se déclenche quand on clique sur ce bouton, et qui écrit dans un fichier toutes les caractéristiques que j’ai trouvées intéressantes, et ce pour toutes les UIViews : tag, accessibilityIdentifier, backgroundColor… en plus de caractéristiques des sous-classes : UILabel.text, [UIButton titleColorForState:xxx], etc.
     
  3. Côté instrumentalisation Java, de quoi lire ce fichier, le transformer en XML (et lire à l’intérieur grâce à avc-binding-dom, évidemment juste un détail).
     



Alors c’est un peu long et un peu riche, sans doute, de dumper toute la hiérarchie d’objets UIView après chaque interaction et notamment après chaque clic, mais, euh… ça fonctionne.

Il faut voir ça comme une capture d’écran — que permet le WebDriver relié à Appium, d’ailleurs.

Considérations diverses


Sur les tags : rien n’impose qu’un tag d’objet UIView soit unique dans toute l’application, et d’ailleurs parfois il vaut mieux avoir des objets différents avec le même tag. Donc, si c’est une contrainte de développement, autant avoir un garde-fou d’une sorte ou d’une autre : test JUnit…

Sur les accessibilityIdentifiers : même remarque, sauf que là, on s’attendrait quand même à ce que l’unicité soit obligatoire.

Sur le fait de réaliser des captures de layouts en plus de captures d’écrans : cela permet une analyse a posteriori de certains éléments plus fins. On peut donc voir le processus de test en deux phases :
  • Une première phase qui remplit les formulaires, clique sur les boutons, et opère toutes les captures de layouts et d’écrans possible ;
  • Une deuxième phase qui analyse ces données à froid : vérification des contraintes de tailles, de couleurs, d’alignements, validation des libellés…

Et ce peut être le même code qui, passé deux fois, s’occupe de chacune des phases.



21 janvier 2014

avc-binding-dom et polymorphisme

On a des cas où une structure XML a évolué et où l’on aimerait pouvoir accéder aux contenus des différentes versions à travers une unique interface Java. Pour cela, avc-binding-dom permet de déclarer des sous-interfaces et suit un polymorphisme.

Prenons un exemple, avec une interface Java candidate à récupérer des informations d’un fichier xib (Apple iOS) :
public interface Xib { boolean hasUseAutoLayout(); }
Les deux versions du XML qu’on veut savoir traiter renseignent l’information « useAutoLayout » de deux façons différentes.
Ici avec Xcode 5.0.2 :
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="4514" systemVersion="12F45" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES"> ... </document>
Ici avec Xcode 4.6.3 :
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="8.00"> <data> ... <int key="IBDocument.defaultPropertyAccessControl">3</int> <bool key="IBDocument.UseAutolayout">YES</bool> <string key="IBCocoaTouchPluginVersion">2083</string> </data> </archive>
Écrivons des interfaces de binding pour ces deux versions :
@XPath("/document[@version = '3.0']") public interface Xib3 extends Xib { @Override @XPath("@useAutolayout = 'YES'") boolean hasUseAutoLayout(); }
et :
@XPath("/archive[@version = '8.00']") public interface Xib8 extends Xib { @Override @XPath( "data/bool[@key = 'IBDocument.UseAutolayout'] = 'YES'") boolean hasUseAutoLayout(); }


Pour lire des documents XML de versions différentes, on utilise les sous-interfaces pour le binding effectif, en ne déclarant pour l’accès aux données qu’une seule variable du type de la super-interface :
Xib xib; xib = DomBinderUtils.xmlContentToJava(new File( "MyViewController_iPhone_3_0.xib"), Xib3.class); assertTrue(xib.hasUseAutoLayout()); xib = DomBinderUtils.xmlContentToJava(new File( "MyViewController_iPhone_8_0.xib"), Xib8.class); assertTrue(xib.hasUseAutoLayout());
Maintenant, ajoutons des méthodes pour lire des sous-structures contenues dans le fichier xib :
public interface Xib { boolean hasUseAutoLayout(); IBUIView getIBUIViewByTag(int tag); // e.g. 1104 interface IBUIView { String getFrame(); // e.g. "{{82, 77}, {43, 43}}" String getUserLabel(); // e.g. "LButton 20" } }

Évidemment, les expressions XPath ne sont pas du tout les mêmes pour les deux structures XML.

Pour Xcode 5.0.2 :
@XPath("/document[@version = '3.0']") public interface Xib3 extends Xib { @Override @XPath("@useAutolayout = 'YES'") boolean hasUseAutoLayout(); @Override @XPath("//*[@tag = $arg0]") IBUIView3 getIBUIViewByTag(int tag); interface IBUIView3 extends Xib.IBUIView { @Override @XPath("concat('{{', rect[@key = 'frame']/@x, ', '" + ", rect[@key = 'frame']/@y, '}, {'" + ", rect[@key = 'frame']/@width, ', '" + ", rect[@key = 'frame']/@height, '}}')") String getFrame(); // e.g. "{{82, 77}, {43, 43}}" @Override @XPath("@userLabel") String getUserLabel(); // e.g. "LButton 20" } }
Et pour Xcode 4.6.3 :
@XPath("/archive[@version = '8.00']") public interface Xib8 extends Xib { @Override @XPath( "data/bool[@key = 'IBDocument.UseAutolayout'] = 'YES'") boolean hasUseAutoLayout(); @Override @XPath("//object[int[@key = 'IBUITag'] = $arg0] ") IBUIView8 getIBUIViewByTag(int tag); interface IBUIView8 extends Xib.IBUIView { @Override @XPath("string[@key = 'NSFrame']") String getFrame(); // e.g. "{{82, 77}, {43, 43}}" @Override @XPath("//object[@class = 'IBObjectRecord'" + " and reference[@key = 'object']/" + "@ref = $this/@id]/" + "string[@key = 'objectName']") String getUserLabel(); // e.g. "LButton 20" } }
Quel que soit le binding initial de l’objet « xib », on pourra valider le code suivant grâce au polymorphisme :
final IBUIView view = xib.getIBUIViewByTag(1104); assertEquals("{{130, 100}, {128, 128}}", view.getFrame()); assertEquals("Bouton OK", view.getUserLabel());

Passons maintenant à de l’héritage qui ajoute des fonctionnalités :


Dans notre exemple, les interfaces IBUIButton dérivent des interfaces IBUIView, en leur ajoutant la méthode « getNormalTitle() », dont l’annotation @XPath dépend encore une fois de la version du document XML.

avc-binding-dom permet alors d’écrire ceci :
final IBUIView view = xib.getIBUIViewByTag(1104); final IBUIButton button = BinderUtils.rebind( view, IBUIButton8.class); assertEquals("OK", button.getNormalTitle());
Eh oui, les relations d’héritage dans notre diagramme ne permettent pas de remonter à IBUIButton8 à partir de « Xib8.getIBUIViewByTag(int): IBUIView8 ». Il faut faire une sorte de transtypage grâce à la méthode rebind().
Noter que d’autres possibilités de transtypage existent, par exemple la méthode self(Class<?>) quand l’interface de binding dérive explicitement de Binding<Node>, mais rebind() est une solution qui fonctionne dans tous les cas.

Pour que le transtypage fonctionne de façon transparente avec des interfaces génériques, c’est-à-dire si on voulait pouvoir écrire « rebind(view, IBUIButton.class) » au lieu de « rebind(view, IBUIButton8.class) », il faudrait plutôt que « Xib8.getIBUIViewByTag(int) » renvoie directement un IBUIButton8 et non un IBUIView8.
Le diagramme devient alors :


Et le code :
final IBUIView view = xib.getIBUIViewByTag(1104); // ni rebind(), ni IBUIButton8, ni IBUIButton3 final IBUIButton button = (IBUIButton) view; assertEquals("OK", button.getNormalTitle());

Voilà :-)

Edit : pour le dernier exemple, je montre qu’on peut utiliser un cast, plus simple qu’un rebind().


Note : les bouts de code et les exemples de cet article sont inclus dans les tests unitaires de avc-binding-dom.

11 juin 2013

Une API de binding Java/XML : avc-binding-dom

Si vous avez un besoin d’extraire en Java des données d’un document XML, je vous présente avc-binding-dom, une magnifique petite API.

Le principe est simple : on déclare le chemin XPath de la donnée qu’on veut lire à l’aide d’une annotation Java.

Exemple :
interface Annuaire {
@XPath("/annuaire/departement/@id")  
  String getDepartementId(); 
  @XPath("/annuaire/@annee") 
  int getAnnee(); 
} 
... 
// static import net.avcompris.binding.dom.helper.DomBinderUtils.*;
File file = ... ; // un fichier XML 
Annuaire annuaire = xmlContentToJava(file, Annuaire.class);
... annuaire.getAnnee(); // hop, ça marche

L’API est en 0.1.1, et, si elle est prometteuse, elle n’a été testée que sur des petits projets – en particulier, il doit lui manquer quelques types primitifs, la gestion des dates doit être brute de décoffrage, des trucs comme ça.

L’API permet d’ores et déjà de faire des requêtes paramétrées :
@XPath("//person[email = $arg0]/name") 
String getPersonNameByEmail(String email);

À partir d’un document XML, elle peut récupérer des tableaux, des couples clés / valeurs, divers types Java, des sous-structures, elle peut vérifier qu’un élément est présent, compter le nombre de éléments vérifiant une condition, faire des calculs à la volée…
Je vous renvoie à la page Return Types de la documentation.

Pour ceux qu’intéresse la mécanique interne, l’API attache l’interface Java passée en paramètre au nœud DOM. Cela signifie en particulier que si le document DOM est modifié, les méthodes Java getXxx() annotées avec des chemins XPath renverront les données mises à jour.

L’écosystème de cette API est le suivant :

Pour la petite histoire : j’avais initialisé une première version de cette API chez Capgemini pour un projet Java à l’automne 2010. Capgemini a ensuite validé le fait de passer ce développement open source, qui a continué de vivre sa vie sous la forme de XmlField, maintenu notamment par Nicolas Richeton, Jean-Pierre Grillon et Mabrouk Belhout.

Liens pour XmlField :
Blog de Nicolas Richeton : http://blog.richeton.com/
L’API XmlField a notamment la capacité de modifier les nœuds DOM depuis des méthodes Java de type setXxx() en suivant la même logique que les getXxx(), ce qui est très pratique pour de la manipulation de données XML.

J’ai ouvert le projet avc-binding-dom pour des besoins spécifiques, en réécrivant tout à la base, et en me concentrant sur mes besoins immédiats. Les méthodes setXxx() ne sont ainsi quasiment pas gérées dans avc-binding-dom.


27 décembre 2012

Deux ans…

Eh, ça fait plus de deux ans maintenant.
Voyons, que s’est-il passé en deux ans ?…

18 septembre 2010

Je passe à Mercurial

J'avais des repositories Subversion sur un serveur à la cave, et il a planté hier. Heureusement qu'il y a les backups automatiques journaliers (backup-manager + cron qui envoyait tout ça sur une VM xen sur un serveur distant).

Je vais en profiter pour migrer la totalité de mes projets vers Mercurial, un DVCS bien sympa, même que le Project Hosting de Google Code l'utilise.

L'architecture que j'ai choisie est celle-ci :
  • hébergement chez Google Code quand le projet a vocation à être open source
  • hébergement sur une VM sur un serveur dédié loué quand le projet est privé
  • backups dans tous les sens grâce aux repositories locaux sur mes machines
Simple et pas prise de tête.

Le serveur qui vient de flancher hébergeait déjà quelques repositories Mercurial, histoire de découvrir. J'avais fait ceci :
  • accès par SSH et clefs publiques (tout comme j'utilisais Subversion)
  • j'avais installé mercurial-server (mauvaise idée car j'ai dû pour cela passer à la sid (squeeze) au lieu de rester en lenny),
  • et tout le bataclan des clefs nommées, etc. (cela dit c'est propre)
Maintenant je fais la chose suivante :

Fichier .hgrc sous Windows
[ui]
username = dandriana
ssh="C:\Program Files\Putty\PLINK.EXE" -ssh -l hg -i "D:\Mes Documents\ma_clef.ppk"
J'utilise PAGEANT sous Windows pour ajouter la clef en début de session.

Sur la VM (xen) du serveur distant, j'installe Mercurial et crée un utilisateur Unix hg. Mes projets seront dans /home/hg/projects/

Sur le Dom0 (le serveur distant), je crée un utilisateur Unix hg, mais sans installer Mercurial.

Pour cet utilisateur je crée un exécutable de nom hg, que je mets dans le PATH, et qui contient ceci :
#!/bin/sh
ssh "hg $*"
En effet, quand je ferai sous Windows « hg -v clone ssh:////home/hg/projects/toto » (attention au double slash « // » avant « home » quand il s'agit d'un chemin absolu) pour récupérer le projet « toto », voici ce qui se passera :
  • le programme hg sous Windows (installation standard de Mercurial) se connectera via SSH au serveur distant avec le nom d'utilisateur hg (voir plus haut, directive « -ssh -l hg » de PLINK),
  • il essaiera d'y lancer la commande « hg -R /home/hg/projects/toto serve --stdio » (c'est l'option « hg -v » plus haut qui permet d'afficher ce détail),
  • donc le serveur distant lancera en fait « ssh "hg -R /home/hg/projects/toto serve --stdio" »,
  • c'est-à-dire se connectera à la VM, où s'exécuteront les opérations Mercurial.
Boah, ça fonctionne bien.

Je trouve deux énormes intérêts à un DVCS :
  1. les commits en mode déconnecté. Je peux poser des savepoints alors que je travaille dans le train, ou que je progresse pas à pas.
  2. le fait de ne pas à faire de choix structurant quant à utiliser un serveur plutôt qu'un autre (à la maison ? loué ? partagé ?…)
Je vais sans doute utiliser hgsvn pour finir la migration.

14 août 2010

Réorientation professionnelle

Mes années dans le service informatique…

2000, Sinclair&Partners à Paris (La Défense),

2001, Logica > LogicaCMG > Logica à Bordeaux (Gradignan),

2004, Cap Gemini Ernst & Young > Capgemini à Bordeaux (Pessac > Mérignac),

2006, indépendant sous l'enseigne Avantage Compris à Bordeaux (Pessac > Mérignac),

2010, je signe un nouveau contrat chez Capgemini, cette fois-ci à Paris (Suresnes et La Défense).

Capgemini est un grand groupe, avec qui j'avais déjà pu faire de belles choses en étant basé à Bordeaux, et notamment une petite mission Flying Squad à New York en 2005. Gageons que l'avenir saura se montrer à la hauteur :-)
Je remarque d'ores et déjà les changements sur le marché : dans les cas où l'agilité est une évidence méthodologique elle ne fait plus peur, SOA est une notion intégrée dans le paysage — j'ai même vu des clients parler naturellement de gouvernance —, et les développements offshore maîtrisés semblent être la norme.
Confronté à un nombre réduit de clients, même s'ils étaient souvent prestigieux — technologies obligent —, je n'avais pas forcément en tant qu'indépendant suffisamment de champ de vision pour arriver à cette idée. Le fait de me retrouver dans l'entreprise (80 000 collaborateurs inscrits sur la partie KM d'un intranet, ça calme toujours) est une bonne chose.

Et le statut d'indépendant : plus jamais ? C'était pas bien ? Je dirais au contraire que c'est une excellente expérience, qui outre le fait d'obliger à gérer chacun des aspects de sa structure, rapproche du sens business de notre métier. En tant qu'indépendant on voit mieux ses forces et ses faiblesses… celles des autres aussi. Je pense qu'on est davantage poussé à la coopération. On attache peut-être davantage d'importance à la complémentarité, ce qui est un plus ensuite.

Bref, c'est reparti :-)

Ah, et je laisse mes amis kabbalistes discuter du sens de « Rê-orientation » et de la figure de l'as de pique :-)

Blog de « Création Mohair », à Limoges

Tiens, du coup « Création Mohair » a mis à jour son blog : http://www.creationmohair.com/blog/

02 juillet 2010

Boutique « Création Mohair », à Limoges

J'ai découvert tout à l'heure la boutique « Création Mohair », à Limoges.

La boutique expose de nombreuses créations très chouettes, en mohair, mohair et soie, ou coton. J'avoue en être tombé sous le charme, et du coup je me fends d'une petite note.

Les créateurs de « Création Mohair » ont une approche assez sympa et écolo : oui, la matière première vient d'animaux estampillés bio (Ecocert), et les teintes sont naturelles (en tout cas celles que j'ai vues). Mais il y aussi le soin du détail dans toute la chaîne de production, qui se retrouve évidemment dans leurs produits et la finition (eh oui, j'aime bien ceux qui fignolent les détails).

Leur site web permet d'acheter par correspondance.

Leur blog, pas hyper mis à jour, affiche des photos des chèvres et du chien :



Manque de bol, je ne trouve pas sur le site les créations les plus récentes, notamment une petite robe en coton tricotée main assez géniale, et qui à mon avis mériterait un site de vente pour elle toute seule.

Ils ont aussi des petits bonnets pour enfants tout à fait craquants, assez originaux, mais hélas je ne les retrouve pas non plus sur le site (là, j'ai peut-être mal cherché).

Alors, est-ce que c'est vraiment le moment, alors qu'il fait 30°C en ville, de parler mohair ? Eh bien pour l'été ils ont une étole mohair et soie tricotée machine, très légère, euh… enfin à chacun de se faire son idée, bien sûr ! En vitrine en tout cas, elle tape pas mal à l'œil :-)

Quant à la toute récente petite robe en coton, c'est clairement une robe d'été. Il faut demander à la voir, car elle n'est même pas encore exposée !

J'ai beaucoup aimé ce magasin et son esprit :-)

28 mars 2010

Gilles Lipovetsky

Chez Nicolas Bordas, un article qui parle de Gilles Lipovetsky intitulé « Et si la culture-monde avait aussi du bon ? ».

Pour reprendre quelques thèmes évoqués dans l'article, je ne crois pour ma part ni à une fin programmée de la morale, ni à une disparition des questions de classes économiques, ni à la possibilité de se démarquer asymptotiquement de l'hyperconsommation.

Le bon point, c'est que se poser ces questions à l'heure où la confirmation affective est accessible, en particulier via le web quand il consolide et nourrit nos réseaux, nous aide à nous considérer de nouveau comme des êtres multidimensionnels.

28 février 2010

Application packagée et ressources externes

Dans le monde J2EE/JEE on déploie une application web sous forme d'archive compressée : l'extension « .war » du fichier signifie « webapp archive ».

Dans le monde PHP, pour ne citer que lui, on peut rencontrer le même fonctionnement, avec les fichiers à déployer fournis sous forme d'archive compressée « .zip » par exemple.

Pourtant, sauf en de rares exceptions, une application ne se limite pas à du code exécutable et à des ressources statiques, mais a besoin de ressources externes telles qu'une base de données, un service de messages, ou des services tiers comme des services web.

La base de données est une ressource externe au code déployé

Prenons le cas le plus courant, où l'application utilise une base de données seulement.


En ce qui concerne ce lien, l'application contient du code exécutable, qui encapsule du SQL, qui attaque base de données.

Pour le déploiement, d'un côté on a créé la structure de la base de données, de l'autre on a déposé l'archive de l'application.


Ces deux déploiements conjoints doivent produire un environnement global cohérent. Penser aux « contrats d'interfaces ».


La correspondance d'interfaces entre le client et la ressource invoquée, est cruciale.

Apporter la cohérence par le déploiement lui-même

L'application qui crée la base de données dont elle a besoin

Une astuce qu'emploient de nombreuses applications destinées au grand public est d'opérer la création de la base de données depuis la base de données elle-même.


(J'ai barré pour dire que cela ne me convient pas)

On met l'accent sur la facilité pour l'utilisateur lambda d'initialiser l'environnement d'exécution.

C'est le modèle choisi pour quasiment toutes les applications PHP. Plusieurs frameworks Java offrent également cette possibilité.

Cette approche est sympathique, mais n'est pas forcément cohérente avec une logique de production, et, surtout, de maintenance.

Je trouve d'autre part que c'est prendre un risque opérationnel, car même en production, le code qui permet de supprimer et de recréer les tables de la base de données est embarqué.

L'outil de déploiement couteau suisse


En posant la distinction entre composants déployés et outillage d'administration, on peut utiliser un outil de déploiement qui saura s'occuper de déployer et d'administrer aussi bien l'application que la base de données.


Même si elle a un très gros intérêt, une faiblesse de cette approche est qu'elle réclame la mise en œuvre d'un outil qui sache tout faire, et surtout son utilisation systématique : que se passe-t-il si un administrateur passe outre la procédure outillée ? Un ALTER TABLE en manuel est si vite arrivé…


Contrôler la cohérence avant le démarrage de l'application

Pour la qualité il n'est souvent pas nécessaire d'engager de grandes manœuvres ou de mettre en place des outils contraignants : il suffit parfois de contrôler quelques critères et de bloquer ou de continuer la procédure en fonction.

Une approche consiste alors à embarquer dans le module applicatif déployé une description du modèle physique de données attendu, et de le comparer avec la structure réelle de la base de données.


On bloque le processus de démarrage de l'application si les deux ne correspondent pas (un simple « hash » peut suffire).

Personnellement j'utilise comme référence du modèle un fichier YAML posé dans le répertoire WEB-INF/ de l'archive déployée, ce qui me permet d'avoir une approche unique pour PHP et J2EE/JEE.
Pour Java, on peut penser à introspecter les classes persistantes afin d'en déduire le modèle attendu.

Une sécurité supplémentaire est d'embarquer dans l'application elle-même, non pas du code de manipulation de base de données, mais l'outil de comparaison. L'application elle-même refuse alors de démarrer si la structure de la base de données n'est pas cohérente avec le modèle attendu.



Autres vérifications

Le fichier de référence qui décrit le MPD cohérent avec l'exécution de l'application peut servir à d'autres contrôles, et notamment les suivants :
  • cohérence entre le MPD et les requêtes SQL externalisées (penser aux objets PreparedStatement)
  • cohérence entre le MPD et les classes persistantes des divers frameworks ou approches (Hibernate, JDO…)
Environnements de tests

En phase de tests, on souhaite d'une façon ou d'une autre que l'application soit autoporteuse : à la fois déployer une application et exécuter des scripts SQL de modification de la base de données (de tests), peut être inutilement coûteux.

J'ai parlé plus haut des frameworks qui permettent ça nativement, en disant cependant que l'approche qui mixe modules applicatif et d'administration ne me convenait pas.

Le fait d'embarquer, dans le module applicatif lui-même, une description de référence du modèle attendu, permet de recréer à volonté une base de données de tests cohérente.



Pour ma part je fais cela avec un plugin Maven.

Approche générale, autres ressources

Le point crucial de ce qui précède est de bloquer le démarrage de modules dans le cas où on a décelé que les chaînes de liaison ne sont pas correctes.

Pour cela, l'idée proposée est simplement que le module client sache dire de quelle interface il a besoin, et que la ressource invoquée sache dire au runtime quelle est sa structure.


On obtient une cartographie « à froid » des composants déployés.

On fait ensuite une vérification statique des structures lues, grâce à des outils externes aux modules et ressources déployés.

Aucun outillage lourd n'est nécessaire en amont.

Il faut se rappeler du reste que, dans certains cas réels, l'outillage lourd en amont n'est pas suffisant à assurer la cohérence entre tous les composants déployés, en particulier quand les procédures de déploiement ne sont pas entièrement automatisées.

L'approche pragmatique présentée, complémentaire à l'outillage amont, et qui fait des sanity checks simples, permet de détecter des failles évidentes.

26 février 2010

Asso&Co

Je cite : « La Banque Postale est à l'initiative d'un nouveau site dédié au monde associatif » : http://assoandco.fr/

De bonnes idées. Pas vu si un même compte peut servir à gérer plusieurs associations.

13 février 2010

Restauration quotidienne de sauvegardes

Voici un état de mon architecture de sauvegardes, qui a un peu évolué depuis que j'avais parlé du sujet il y a deux ans (voir : Validation de backups, avril 2008).

Architecture

Les nouveautés concernent essentiellement l'ordonnancement.

Sur le plan fonctionnel, les principes sont :
  • une sauvegarde complète quotidienne
  • une restauration complète quotidienne
  • des tests d'acceptabilité sur les données restaurées (par exemple, vérifier que les données qu'on a restaurées ne sont pas constituées de répertoires vides)
Sur le plan technique, les principes sont :
  • pour tout ce qui concerne les flux entre machines, l'ordonnancement est centralisé
  • les flux sont sécurisés (SSH)
  • les accès sont sécurisés de machine à machine (restrictions des adresses IP autorisées, etc.)
  • on limite les volumes des transferts réseaux (compression BZ2, regroupement des commandes envoyées par SSH…)
L'architecture se synthétise ainsi :


Légende :
  • ref — référentiels, « Repositories » : Ce sont les éléments à sauvegarder. En l'occurrence Subversion, un wiki…
  • m2 — ordonnanceur, « Scheduler »
  • bkp — espace de sauvegarde, « Archives »
  • tmp — espace temporaire pour restauration
  • ci — outil d'intégration continue, « Continuous Integration »
Les machines « ref » et « m2 » sont sur un même réseau local (LAN).
  • « ref » est visible sur internet, ports 22 et 80. Sur le LAN elle est dans une DMZ, et en particulier n'accède pas à « m2 ».
  • « m2 » n'est pas visible sur internet.
Les machines « bkp + tmp » et « ci » sont sur internet, avec leurs accès SSH limités à certaines adresses IP.

Les étapes du processus sont les suivantes :
  1. sauvegardes locales sur « ref », par l'outil « backup-manager »
  2. copie vers une machine distante qui stocke les archives, avec éventuellement une copie intermédiaire locale sur l'ordonnanceur lui-même
  3. décompression de la dernière sauvegarde présente dans les archives, restauration, contrôles et mesures
  4. copie des résultats des contrôles et mesures, vers une machine d'intégration continue
  5. analyse des résultats des contrôles et mesures, tests sur des critères d'acceptabilité
  6. [E] si une erreur est décelée, envoi d'un e-mail à l'administrateur
En gros, j'arrête de poser des crontabs partout comme je faisais il y a deux ans (voir article : Validation de backups) ; c'est un plat de spaghetti à administrer, et, quand une machine tombe ou est recyclée, les autres continuent leur semoule sans que ça serve à quoi que ce soit.

L'ordonnancement centralisé facilite les enchaînements.

En revanche, il ne facilite pas forcément la gestion de verrous.

Autre changement par rapport à il y a deux ans, j'écris mes scripts système en Bash et délaisse Ruby, qui n'apportait pas tant que ça.


Contrôles et mesures

Pour un repository Subversion, après restauration des données, les tests d'intégration continue se feront sur les mesures suivantes :
  • nom et taille du fichier de sauvegarde restauré => la date doit être celle du jour
  • dates de début et fin de la restauration => le traitement ne doit pas avoir été trop rapide
  • numéro de version (Revision) => ?
  • nombre total de fichiers après checkout => il doit être supérieur à telle valeur
  • nombre de fichiers « pom.xml » après checkout => il doit être supérieur à telle valeur
  • nombre de fichiers « build.xml » après checkout => il doit être supérieur à telle valeur
L'idée est de repérer les cas de sauvegardes vides, ainsi que les sauvegardes obsolètes.


Quelques idées rejetées

Il est bon de fonder ses choix aussi sur l'historique des choix qui n'ont pas été retenus ;-)


Idée rejetée : copie directe depuis la machine de référentiel vers la machine de backup.


Explication : pourquoi la machine de référentiel devrait-elle avoir connaissance de l'existence d'une machine de backup ? Et en être dépendante ? Et dépenser de la charge et être administrée pour ça ?
L'idée est que les processus de restauration d'archives doivent être vus par ailleurs. La machine à sauvegarder n'a pas elle-même à savoir qu'elle devrait envoyer un e-mail, etc.


Idée rejetée : accès direct depuis la machine de backup à la machine de référentiel.


Explication : on a choisi dans l'architecture de limiter les accès SSH à « ref» depuis l'internet à l'utilisation de Subversion. En particulier, pas d'accès aux utilitaires « ls » et « tar ».


Idée rejetée : stocker les archives sur la machine d'ordonnancement.


Explication : cette machine est dans les mêmes locaux que la machine référentiel. Or on veut évidemment que les données archivées soient physiquement découplées des données d'origine.


Idée rejetée : ordonnancement par cron de la restauration depuis l'espace temporaire.

C'est ce que faisait l'ancienne architecture.



Explication : l'espace temporaire n'a pas à être administré et donc n'a pas à contenir de scripts résiduels ni de crontab.


Idée rejetée : restauration sur la machine d'ordonnancement.


Explication : Il faut que la restauration ait lieu à partir d'un élément archivé (celui qu'on ira réellement chercher en cas de problème), et pas depuis une copie intermédiaire, qui par définition disparaîtra.
De plus, ni la mémoire ni le CPU ne sont suffisants sur cette machine pour une restauration.


Idée rejetée : envoi des résultats directement depuis l'espace temporaire vers la machine d'intégration continue.


Explication : l'espace temporaire n'a pas à connaître la machine d'intégration continue, à y avoir posé sa clef publique, etc.


Idée rejetée : récupération des résultats par la machine d'intégration continue auprès de l'espace temporaire.


Explication : la machine d'intégration continue n'a pas à savoir qu'il existe un espace temporaire (en plus, comment s'y connecter s'il n'est pas résiduel ?)


Variante possible

Une variante qui semble être digne d'intérêt, consiste à stocker les résultats des contrôles et mesures dans les référentiels, et que la machine d'intégration continue y accède.


Quelques avantages :

  • l'ordonnanceur n'a plus à connaître la topographie de la machine d'intégration continue
  • les résultats des contrôles et mesures sont archivés
  • on s'appuie sur le lien de l'ordonnanceur vers la machine référentiel, qui existe déjà
  • on s'appuie sur le lien de la machine d'intégration continue vers la machine référentiel, qui existe déjà
  • les tests sur les résultats peuvent être lancés depuis n'importe quel environnement qui a accès au référentiel, pas seulement depuis la machine d'intégration continue (donc, pratique pour développer ces tests)


Quelques inconvénients :
  • le référentiel augmente de façon automatique et ininterrompue, or ce référentiel est lui-même destiné à être sauvegardé et archivé, donnant lieu à des contrôles, qui seront injectés dans le référentiel, etc.
    Certes l'augmentation est d'1 Ko par jour, ce qui est dérisoire, mais techniquement, le principe du mécanisme croissant qui s'alimente lui-même n'est pas bon.
  • d'un point de vue fonctionnel ce serait mélanger production et décisionnel.
    Production = référentiel Subversion.
    Décisionnel = tests en intégration continue après extraction de mesures.

10 février 2010

Curriculum Vitæ à jour

Après la compta à jour et même le bilan d'Avantage Compris au clair, voici que je complète mon CV dans les temps !

CV en ligne : http://www.avantage-compris.com/team/dandriana/

J'en ai même fait une version anglaise, mais pour l'instant le site de ma boîte n'est qu'en français :-P

03 février 2010

IZI-collecte, plate-forme web pour les associations

D'après la page d'accueil, IZI-collecte (http://www.izi-collecte.com/) propose :
  • un paiement sécurisé pour les dons et les cotisations à un tarif bas et fixe, comprenant l’émission du e-reçu fiscal et des fonctionnalités communautaires
  • une plateforme d’envoi d’e-mailings et d’e-newsletters.
  • la gestion de base de données adhérents et donateurs