Tester ses commandes OSGi avec Karaf et PaxExam

Ce post a pour but de présenter **OSGi **et ses concepts dans un premier temps. Puis ensuite, de fournir un exemple basique de développement de composants OSGi à l’aide d’Apache Karaf 4. Enfin montrer comment peut-on écrire des tests d’intégration à l’aide de Pax Exam, afin de tester ses commandes.

Définition de OSGi

OSGi (Open Service Gateway Initiative) est une spécification permettant de concevoir une application basée sur une architecture modulaire. Chaque module, appelé bundle. Il possède une version et peut dépendre d’autres modules. OSGi gère différents classloader par bundle. Ceci permet une gestion dynamique des bundles, donc de déployer plusieurs versions d’un même composant. Le redéploiement partiel et à chaud de composants est donc ainsi possible, ce qui rend possible une mise à jour de versions applicatives sans coupure de service.

Présentation de Karaf

karaf-logoKaraf est un framework open source de la fondation Apache, basé sur OSGi. La dernière version (4.0.3) est téléchargeable sur le site ici. Il faut ensuite se positionner dans le répertoire bin/ et lancer la commande : karaf. La console shell de Karaf présente alors un prompt qui permet de lancer des commandes :

karaf-prompt

Karaf s’appuie sur Apache Aries pour fournir un mécanisme d’injection de dépendances pour OSGi. Un bundle est constitué d’un fichier Blueprint XML qui permet d’instancier des composants, d’injecter des dépendances entre ces composants, d’exposer des services à travers le Service Registry, d’injecter des références vers d’autres services d’autres bundles.

``` ... ```
Le plugin maven **maven-bundle-plugin** va permettre de packager le bundle et de générer le fichier MANIFEST.MF définissant un nom *Bundle-SymbolicName*, une version *Bundle-Version*, les packages exportés *Export-Package*, et importés *Import-Package*, ou encore les packages contenant les commandes *Karaf-Commands*.

Un bundle peut être installé manuellement. Par exemple :

`> bundle:install -s mvn:com.h2database/h2/1.4.190`
Mais, il peut être également installé par l’intermédiaire d’une *feature*. Une **feature** est constituée d’un fichier description XML qui liste les bundles que l’on souhaite installer au démarrage.
```
${project.description}
    <feature version="4.0.3">jdbc</feature>
    <feature version="4.3.6.Final">hibernate</feature>
    <feature version="2.2.0">jpa</feature>
    ...
    <bundle start-level="70">mvn:com.h2database/h2/1.4.190</bundle>
    ...
</feature>

```

Karaf vient avec un ensemble de **repositories de features** pré-configurées. Les commandes, ci-dessous, permettent respectivement de lister les repositories de features, lister les features, installées ou à disposition pour installation, installer une feature.
``` > feature:repo-list > feature:list > feature:install ```
Les bundles sont récupérés depuis internet et stockés dans le repository local. Les commandes, ci-dessous, permettent respectivement de démarrer / stopper un bundle, lister les bundles installés, de redéployer automatiquement un bundle compilé et installé dans le repository local.
``` > bundle:start > bundle:stop > bundle:list > bundle:watch ```
## Projet OSGi exemple

Le projet exemple* ippon-osgi-sample* montre comment développer des commandes OSGi. Il s’agit d’une application de type CRUD qui fournit la possibilité de lister les salariés de la société, d’ajouter et de supprimer un salarié d’une base de données. Cet exemple s’appuie sur un socle JPA/Hibernate et une base H2. Il est organisé en projet multi-modules maven :

``` +ippon-osgi-sample |-ippon-osgi-sample-ds |-ippon-osgi-sample-services |-ippon-osgi-sample-command |-ippon-osgi-sample-kar |-ippon-osgi-sample-ittests```
- **ippon-osgi-sample-ds** contient la datasource vers la base H2, packagée sous forme de bundle pour être déployé simplement. - **ippon-osgi-sample-services** contient les services et entités JPA. Ces services sont exposés en tant que services OSGi. - **ippon-osgi-sample-command** contient les commandes OSGi qui pourront être invoquées depuis le shell de Karaf. - **ippon-osgi-sample-kar** contient principalement le fichier *features.xml* qui permet de faire du provisionning et de packager notre application avec les bundles précedents pour déploiement dans Karaf. - **ippon-osgi-sample-ittests** contient les tests d’intégration d’exécution de nos commandes.

MindmapPour initialiser le projet, on peut utiliser différents archetypes maven. Le premier est un archetype maven destiné à créer des commandes. Le second permet d’initialiser un projet avec Blueprint XML. Le troisième est un archetype maven pour la création de feature, pour le provisionning et le packaging sous forme de fichier kar.

``` mvn archetype:generate -DarchetypeGroupId=org.apache.karaf.archetypes -DarchetypeArtifactId=karaf-command-archetype -DarchetypeVersion=4.0.0 -DgroupId=fr.ippon.osgi.sample -DartifactId=ippon-osgi-sample-command -Dversion=1.0-SNAPSHOT -Dpackage=fr.ippon.osgi.sample.command

mvn archetype:generate -DarchetypeGroupId=org.apache.karaf.archetypes -DarchetypeArtifactId=karaf-blueprint-archetype -DarchetypeVersion=4.0.0 -DgroupId=fr.ippon.osgi.sample -DartifactId=ippon-osgi-sample-services -Dpackage=fr.ippon.osgi.sample.services -Dversion=1.0-SNAPSHOT

mvn archetype:generate -DarchetypeGroupId=org.apache.karaf.archetypes -DarchetypeArtifactId=karaf-feature-archetype -DarchetypeVersion=4.0.0 -DgroupId=fr.ippon.osgi.sample -DartifactId=ippon-osgi-sample-feature -Dversion=1.0-SNAPSHOT -Dpackage=fr.ippon.osgi.sample.feature```

Une feature définit les différentes ressources via URLs (instances, bundles, fichiers de configuration). *features.xml *recense d’autres features que l’on souhaite installer et activer par défaut au démarrage du serveur. Voici les principaux dans notre cas : **jdbc**, **hibernate**, **transaction**, **jpa**, etc.** **Dans ce même fichier peuvent être ajouter nos propres bundles, comme par exemple, l’ajout du driver H2 ou des librairies Apache Commons. La commande *feature:info hibernate* liste l’ensemble des bundles dépendant de cette feature. Karaf peut demander le téléchargement d’artefact à partir de dépôts distants présent dans sa liste de repositories.

Un **kar **est donc un package sous forme d’archive zip, qui contient toutes les ressources décrites dans le fichier features XML. Il peut être déployé sans connexion internet. il est constitué d’un répertoire repository contenant une liste de features XML et l’ensemble des artefacts Maven. Voici les commandes pour installer / désinstaller un kar :

``` karaf@root()> kar:uninstall ippon-osgi-sample-kar-1.0-SNAPSHOT karaf@root()> kar:install file:/D:/Java/workspace-nb/ippon-osgi-sample/ippon-osgi-sample-kar/target/ippon-osgi-sample-kar-1.0-SNAPSHOT.kar```
### Service JNDI pour la Datasource

Le projet ippon-osgi-sample-ds produit à la compilation un bundle OSGi qui va enregistrer dans le registre de services notre datasource vers la base H2. Ce service est accessible à partir de son nom JNDI (jdbc/ippon-osgi)

```

```

### Services et entités JPA

Le bundle ippon-osgi-sample-services contient les entités JPA et les services d’accès à la base de données. On définit le fichier persistence.xml en précisant bien l’URL vers le service qui expose la datasource.

``` org.hibernate.jpa.HibernatePersistenceProvider fr.ippon.osgi.sample.model.Employee true osgi:service/javax.sql.DataSource/(osgi.jndi.service.name=jdbc/ippon-osgi) ... ```
Dans le fichier Blueprint XML, JPA est activé. Les beans de services sont déclarés et exposés comme services OSGi, afin de les utiliser dans d’autres bundles.
```
<jpa:enable />

<bean id="employeeBean" class="fr.ippon.osgi.sample.services.EmployeeServiceImpl">
    <tx:transaction method="*"/>
</bean>
<service ref="employeeBean" interface="fr.ippon.osgi.sample.services.EmployeeService"/>

```

Il ne faut pas oublier aussi de préciser au niveau du plugin *maven-bundle-plugin* le chemin vers le fichier de configuration JPA *META-INF/persistence.xml *et d’exporter les packages contenant les services *fr.ippon.osgi.sample.services*.*

La configuration JPA est classique. Une entityManager est récupérée grâce à l’annotation @PersistenceContext(unitName = “ippon-pu”) et l’API Criteria JPA est utilisée pour construire les requêtes. (cf. EmployeeServiceImpl.java)

Développement de commandes Karaf

Karaf offre la possibilité d’étendre ses commandes shell de base. Nous allons créer nos propres commandes faisant référence à nos services Blueprint développés précédemment dans le bundle ippon-osgi-sample-services. Une commande est définie par un scope et un nom. Elle peut avoir en paramètre des options ou des arguments ; la complétion peut être activée.

Voici un exemple de commande faisant appel au service qui liste les salariés de la société.

``` @Command(scope = "ippon", name = "list-employees", description = "Liste les employees de la societe") @Service public class ListEmployees implements Action {
@Option(name = "-j", aliases = {"--job"}, description = "Liste de jobs", required = false, multiValued = false)
@Completion(JobCompleter.class)
private String jobsParam;

@Reference
private EmployeeService employeeService;

@Override
public Object execute() throws Exception {
        System.out.println("Liste des employes :");
        employeeService.getAllEmployees();
            ...
        }

}```

Les annotations **@Command****@Service** servent à déclarer cette classe comme commande Karaf. Cette commande est exposée comme un service OSGi. Il faut également dans le *maven-bundle-plugin* penser à définir les packages contenant les commandes Karaf* fr.ippon.osgi.sample.command** dans les instructions.
``` org.apache.felix maven-bundle-plugin 2.5.4 true ${project.artifactId} fr.ippon.osgi.sample.command*;-noimport:=true fr.ippon.osgi.sample.command* ```
L’annotation** @Option** permet de définir des paramètres pour la commande. **@Argument** permet de définir les arguments. **@Completion** permet de faire de la complétion et peut-être associée à une option ou un argument de commande.

L’annotation @Reference permet de récupérer les services OSGi exposés par le projet ippon-osgi-services. L’usage de ces annotations simplifie grandement la configuration, réalisée auparavant en XML dans les versions antérieures de Karaf (dans le fichier Blueprint XML du bundle). La méthode execute() contient le cœur d’exécution de la commande qui renvoie la liste des salariés.

Retourner tous les salariés de la société : ippon:list-employees

list-employees-1

Retourner uniquement les architectes de la société : ippon:list-employees -j ARCHITECT

list-employees-2

Retourner uniquement les architectes de la société dont le nom contient ‘Employee 3’: ippon:list-employees -j ARCHITECT -n ‘Employee 3’

list-employees-3

D’autres commandes d’ajout et de suppression de salarié sont disponibles sur GitHub ici.
**

``` > ippon:add-employee DEV 'New Employee' 'New Employee' '01-01-1990' > ippon:remove-employee 2```
### Packaging kar

Voici le feature du projet, il contient les features Karaf à activer, les bundles de librairies tierces et les bundles du projet (ippon-osgi-sample-*). Le plugin maven karaf-maven-plugin va génèrer le kar (cf. pom.xml).

```
${project.description}
    <feature version="4.0.3">jdbc</feature>
    <feature version="4.3.6.Final">hibernate</feature>
    <feature version="2.2.0">jpa</feature>
    <feature version="1.3.0">transaction</feature>
    <feature version="4.0.3">jndi</feature>
    
    <bundle start-level="70">mvn:com.h2database/h2/1.4.190</bundle>
    <bundle start-level="80">mvn:commons-lang/commons-lang/2.6</bundle>
    <bundle start-level="80">mvn:commons-logging/commons-logging/1.2</bundle>
    <bundle start-level="80">mvn:commons-io/commons-io/2.4</bundle>
    
    <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-ds/1.0-SNAPSHOT</bundle>
    <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-services/1.0-SNAPSHOT</bundle>
    <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-command/1.0-SNAPSHOT</bundle>
</feature>

```

### Tests d’intégration des commandes avec PaxExam

paxexam-logoPaxExam est un framework pouvant réaliser des tests d’intégration dans le cadre d’un environnement OSGi. Il est capable de démarrer un Karaf, de déployer des features, des bundles, de surcharger les propriétés de configuration et de lancer de commandes. Différentes stratégies permettent d’utiliser ou non la même configuration pour chaque tests unitaires.

Pour l’intégrer au projet Maven ippon-osgi-sample-ittests, il faut ajouter les dépendances suivantes :

``` org.ops4j.pax.exam pax-exam-container-karaf ${pax-exam.version} test org.ops4j.pax.exam pax-exam-junit4 ${pax-exam.version} test org.ops4j.pax.exam pax-exam-inject ${pax-exam.version} test org.apache.geronimo.specs geronimo-atinject_1.0_spec 1.0 test org.apache.karaf apache-karaf 4.0.3 tar.gz test org.apache.karaf org.apache.karaf.client ```
Puis ajouter le plugin suivant, pour activer les fonctionnalités de versionAsInProject() sur les features (voir plus loin)
``` org.apache.servicemix.tooling depends-maven-plugin 1.2 generate-depends-file generate-depends-file ```
La classe de tests d’intégration des commandes Karaf doit posséder les annotations suivantes : **@RunWith(PaxExam.class)** pour activer PaxExam et **@ExamReactorStrategy(PerClass.class)** pour activer la configuration une et une seule fois pour tous les tests de cette même classe. La classe étendue **KarafTestSupport** fournit des fonctions permettant d’exécuter des commandes, vérifier l’installation d’un bundle ou d’une feature**. **Elle est fortement inspirée de celle des tests d’intégration du projet Karaf lui-même. [https://github.com/apache/karaf/blob/master/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java](https://github.com/apache/karaf/blob/master/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java)
``` @RunWith(PaxExam.class) @ExamReactorStrategy(PerClass.class) public class IpponOSGIPaxExamTest extends KarafTestSupport {
@ProbeBuilder
public TestProbeBuilder probeConfiguration(TestProbeBuilder probe) {
    return probe.setHeader(Constants.DYNAMICIMPORT_PACKAGE, "*,org.apache.felix.service.*;status=provisional");
}

@Configuration
public static Option[] configure() throws Exception {
    return new Option[]{
        karafDistributionConfiguration()
        .frameworkUrl("mvn:org.apache.karaf/apache-karaf/4.0.3/tar.gz")
        .karafVersion("4.0.3")
        .useDeployFolder(false)
        .unpackDirectory(new File("target/paxexam/unpack")),
        logLevel(LogLevelOption.LogLevel.WARN),

        // install features

        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jdbc"),
        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "hibernate"),
        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jpa"),
        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "transaction"),
        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jndi"),
        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "pax-jdbc-pool-dbcp2"),
        features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "aries-annotation"),

        // Change ssh port

        editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiRegistryPort", RMI_REG_PORT),
        editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiServerPort", RMI_SERVER_PORT),

        keepRuntimeFolder(),

        // install bundles

        mavenBundle().groupId("com.h2database").artifactId("h2").version("1.4.190"),
        mavenBundle().groupId("commons-lang").artifactId("commons-lang").version("2.6"),
        mavenBundle().groupId("commons-logging").artifactId("commons-logging").version("1.2"),
        mavenBundle().groupId("commons-io").artifactId("commons-io").version("2.4"),

        // install bundle datasource h2 for test

        streamBundle(bundle().add("OSGI-INF/blueprint/datasource-h2-test.xml",
                new File("src/test/resources/OSGI-INF/blueprint/datasource-h2-test.xml").toURL())
                .set(Constants.BUNDLE_NAME, "Apache Karaf :: Ippon OSGI Datasource Test")
                .set(Constants.BUNDLE_SYMBOLICNAME, "ippon-osgi-sample-ds")
                .set("Bundle-ManifestVersion", "2")
                .set(Constants.DYNAMICIMPORT_PACKAGE, "*").build()).start(),

        // install ippon bundles

        mavenBundle().groupId("fr.ippon.osgi.sample").artifactId("ippon-osgi-sample-services").version("1.0-SNAPSHOT"),
        mavenBundle().groupId("fr.ippon.osgi.sample").artifactId("ippon-osgi-sample-command").version("1.0-SNAPSHOT"),

    };
}

...

}```

La configuration dans la méthode *probeConfiguration()* autorise l’import dynamique de tous les packages de type provisional dans le *features.xml* du projet.

La configuration de la méthode configure() définit la distribution Karaf karafDistributionConfiguration() à déployer lors des lancements des tests, les features features() et bundles mavenBundle() à installer. streamBundle() permet de créer un bundle à la volée à partir d’une autre datasource de test datasource-h2-test.xml. editConfigurationFilePut() permet de surcharger les fichiers de configuration du serveur.

Il est alors possible de tester l’installation des features et bundles.

``` @Test public void testProvisioning() throws Exception { // Check that the features are installed
    assertFeatureInstalled("jdbc", "4.0.3");
    assertFeatureInstalled("hibernate", "4.3.6.Final");
    assertFeatureInstalled("jpa", "2.2.0");

    // Check that the bundles are installed

    assertBundleInstalled("ippon-osgi-sample-services");
    assertBundleInstalled("ippon-osgi-sample-command");
}```
De tester l’exécution de nos commandes Karaf. Exemple :
``` @Test public void testListEmployeesWithOptionsCommand() { Assert.assertNotNull(bundleContext);
    String result = executeCommand("ippon:list-employees -j ARCHITECT -n 'Employee 3'");
    System.out.println("result : " + result);
    Assert.assertNotNull(result);
}```
## Conclusion

Nous avons vu comment écrire nos propres commandes Karaf, comment les tester avec PaxExam. J’espère que ce tutoriel peut constituer une première approche pour qui souhaite débuter avec les concepts d’OSGi.

Le code source de l’application est disponible sur GitHub : https://github.com/sfoubert/ippon-osgi-sample

Références

https://www.osgi.org/developer/specifications/

https://ops4j1.jira.com/wiki/display/PAXEXAM4
https://ops4j1.jira.com/wiki/display/PAXEXAM4/Karaf+Test+Container+Reference

https://karaf.apache.org/manual/latest/developers-guide/extending.html
http://iocanel.blogspot.fr/2012/01/advanced-integration-testing-with-pax.html

https://www.packtpub.com/application-development/apache-karaf-cookbook
https://github.com/jgoodyear/ApacheKarafCookbook