Accueil > DynaResume, Eclipse RCP, OSGi, Spring DM > Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step13]

Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step13]


Dans le billet précédant [step12] nous avons initialisé le client RCP qui affiche une liste statique de Users.

Dans ce billet nous allons faire appel au service UserService pour afficher la liste des Users. Un bouton Refresh Users permettra de rappeler le service UserService pour rafraîchir la liste des users :

Pour récupérer le service UserService dans la View, nous utiliserons 2 techniques possibles :

Nous montrerons aussi comment créer des launch qui permettent de lancer :

  • l’application RCP en tant que Client lourd (les services sont dans le même conteneur OSGi que l’application RCP).
  • l’application RCP en tant que Client (Serveur) qui fait appel à un serveur pour utiliser les services.

Pré-requis

Dans ce billet nous partir des projets :

Mettez tous les launch du step11 dans le projet dynaresume-launch (projet de step12 qui contient le launch qui lance l’application RCP).

Vous pouvez téléchargez les projets expliqués dans cette section dans le zip org.dynaresume_step13-start.zip.

Si jamais vous partez d’un nouveau workspace où vous avez importé tous les projets, certains bundles ne compilent pas car la Target Platform utilisé est celle d’Eclipse (default) qui ne contient pas les bundles Spring requis. Pour utiliser la Target Platform de DynaResume, ouvrez le fichier target DynaResume Target Platform.target du projet spring-target-platform et cliquez sur le lien Set as Target Platform:

La Target Platform de votre workspace utilisera celle de DynaResume. Les projets du workspace se compilent à nouveau en utilisant la Target Platform de DynaResume qui corrige les erreurs de compilation.

UserService via ServiceTracker

Dans cette section nous allons dans la View de l’application RCP, récupérer le service UserService via ServiceTracker pour afficher la liste des Users en utilisant le service. Nous repartons des projets expliqués dans le pré-requis.

Vous pouvez téléchargez les projets expliqués dans cette section dans le zip org.dynaresume_step13-servicetracker.zip.

MANIFEST.MF

Pour récupérer le service UserService dans la View de l’application RCP nous devons utilisez dans la classe View l’interface UserService. Pour cela importez le package org.dynaresume.services dans notre application RCP.

View – ServiceTracker

Ici nous allons récupérer le service UserService dans la classe View via ServiceTracker. Pour cela nous devons dans la classe org.dynaresume.simplercpclient.View :

  1. ajouter un champs userServiceTracker de type ServiceTracker dans la classe :
    private ServiceTracker userServiceTracker = null;
  2. initialiser le ServiceTracker dans la méthode View#createPartControl(Composite parent), récuperer le service puis initialiser le viewer avec la liste de Users récupérée par UserService#findAllUsers :
    public void createPartControl(Composite parent) {
    	viewer = new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
    	viewer.setContentProvider(new ViewContentProvider());
    	viewer.setLabelProvider(new ViewLabelProvider());
    
    	// 1. Create tracker for UserService and open it.
    	userServiceTracker = new ServiceTracker(Activator.getDefault().getBundle().getBundleContext(), UserService.class.getName(),null);
    		userServiceTracker.open();
    
    	// 2. Get UserService with service tracker and refresh the viewer with
    	// list of users coming from service
    	UserService userService = (UserService) userServiceTracker.getService();
    	viewer.setInput(userService.findAllUsers());
    }
  3. fermer correctement le service tracker lorsque la View n’est plus utilisée en surchargeant sa méthode View#dispose() :
    public void dispose() {
    	super.dispose();
    	userServiceTracker.close();
    }

On peut remarquer que le code n’est pas sécurisé, car le service UserService récupéré par ServiceTracker peut être null et le code

viewer.setInput(userService.findAllUsers()); 

peut lancé une exception NullPointerException. mais ce potentiel bug est intentionnel et nous serviras dans la suite du billet.

Voici le code en entier de la classe View :

package org.dynaresume.simplercpclient;

import java.util.Collection;

public class View extends ViewPart {
	public static final String ID = "org.dynaresume.simplercpclient.view";

	private ServiceTracker userServiceTracker = null;
	private TableViewer viewer;

	class ViewContentProvider implements IStructuredContentProvider {
		public void inputChanged(Viewer v, Object oldInput, Object newInput) {
		}

		public void dispose() {
		}

		@SuppressWarnings("unchecked")
		public Object[] getElements(Object parent) {
			return ((Collection<User>) parent).toArray();
		}

	}

	class ViewLabelProvider extends LabelProvider implements
			ITableLabelProvider {
		public String getColumnText(Object obj, int index) {
			User user = (User) obj;
			switch (index) {
			case 0:
				return user.getLogin();
			}
			throw new IllegalArgumentException("bad index!");
		}

		public Image getColumnImage(Object obj, int index) {
			return getImage(obj);
		}

		public Image getImage(Object obj) {
			return PlatformUI.getWorkbench().getSharedImages().getImage(
					ISharedImages.IMG_OBJ_ELEMENT);
		}
	}

	public void createPartControl(Composite parent) {
		viewer = new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL
				| SWT.V_SCROLL);
		viewer.setContentProvider(new ViewContentProvider());
		viewer.setLabelProvider(new ViewLabelProvider());

		// 1. Create tracker for UserService and open it.
		userServiceTracker = new ServiceTracker(Activator.getDefault()
				.getBundle().getBundleContext(), UserService.class.getName(),
				null);
		userServiceTracker.open();

		// 2. Get UserService with service tracker and refresh the viewer with
		// list of users coming from service
		UserService userService = (UserService) userServiceTracker.getService();
		viewer.setInput(userService.findAllUsers());

	}

	@Override
	public void dispose() {
		super.dispose();
		userServiceTracker.close();
	}

	public void setFocus() {
		viewer.getControl().setFocus();
	}
}

Run – ServiceTracker

Règles à respecter dans un launch de type application RCP

Un launch de type application RCP qui utilise Spring DM doit IMPERATIVEMENT respecter plusieurs règles pour son bon fonctionnement. Ces règles vont être démontrées pas à pas dans la suite de la section. Les voici :

  • la case à cocher Clear the configuration area before launching (onglet Configuration du launch) doit être sélectionnée pour ne pas avoir de mauvaises surprises lorsque la configuration du launch change:
  • par défaut la propriété Default Start est à false dans un launch de type Eclipse Application alors qu’elle est true dans un launch de type OSGi Framework. Ceci implique que :
    • la propriété Auto Start du bundle Spring Extender doit être forcée à true (et par prudence, il doit ête démarré avant tous les autres bundles) :
    • la propriété Auto Start des bundles d’implémentation des services (dans notre cas org.dynaresume.services.impl OU org.dynaresume.remoting.importer.http) doit être forcée à true:

OSGi DynaResume – RCP Full Client

Ici nous allons nous occuper du launch de type Full Client RCP (pas Client (Serveur). Pour cela, renommez le launch DynaResume – org.dynaresume.simplercpclient.application.launch en OSGi DynaResume – RCP Full Client.

Cochez les mêmes bundles que le launch du client simple OSGi OSGi DynaResume – Full Client.launch :

  • org.dynaresume.config.log4j
  • org.dynaresume.domain
  • org.dynaresume.services
  • org.dynaresume.services.impl
  • org.dynaresume.simplercpclient (déja coché)

Et ceux de la Target Platform (ceux de Spring) :

  • com.springsource.org.aopalliance
  • com.springsource.sl4j.api
  • com.springsource.sl4j.log4j
  • com.springsource.sl4j.org.apache.commons.logging
  • org.springframework.aop
  • org.springframework.beans
  • org.springframework.context
  • org.springframework.core
  • org.springframework.osgi.core
  • org.springframework.osgi.extender
  • org.springframework.osgi.io
  • org.springframework.osgi.log4j.osgi

Lancez (via le bouton Run) le launch et la console OSGi affiche l’erreur suivante:

!MESSAGE Unable to create view ID org.dynaresume.simplercpclient.view: An unexpected exception was thrown.
!STACK 0
java.lang.NullPointerException
at org.dynaresume.simplercpclient.View.createPartControl(View.java:88)
...

Cette erreur de NullPointerException vient de la ligne de code

viewer.setInput(userService.findAllUsers()); 

car le service userService est null. Le service n’a pas pu être récupéré car le bundle org.dynaresume.services.impl qui a pour rôle d’enregistrer dans le service OSGi le service, n’est pas démarré. Pour s’en rendre compte, tappez ss + entrée. la console OSGi affiche le statut des bundles de la Target Platform:

ss

Framework is launched.

id State Bundle
...
13 RESOLVED org.springframework.osgi.extender_1.2.0
...
15 ACTIVE org.dynaresume.simplercpclient_1.0.0.qualifier
...
24 RESOLVED org.dynaresume.services.impl_1.0.0.qualifier
...

On peut remarquer que le bundle de l’application RCP est lancé (état ACTIVE) mais pas ceux de Spring Extender et de l’implémentation du service (état RESOLVED).

Lorsque nous avons lancé le launch du client simple OSGi OSGi DynaResume – Full Client.launch, le bundle client simple OSGi arrivait à récupérer le service. Pourquoi le lancement d’une application RCP diffère de celui d’un bundle OSGi? La réponse est toute bête (mais qui m’a fait perdre pas mal de temps). La création d’un launch de type OSGi Framework positionne la propriété Auto-Start à true alors que celle d’un launch de type Eclipse Application positionne la propriété Auto-Start à false.

Positionnez la propriété Default Auto-Start du launch OSGi DynaResume – Full Client.launch à true et relancez.

L’application RCP affiche la liste des Users :

Cependant ce procédé n’est pas très optimisé car tous les bundles de la Target Platform sont lancés même si ils ne sont pas utilisés.

Repositionnez la propriété Default Auto-Start à false et cliquez sur le bouton Run.

On s’attend à se retrouver avec l’erreur de NullPointeurException, mais l’application RCP s’affiche bien? La configuration précédante a été mise en cahe et n’a pas été rafraîchie. Pour éviter ce problème (qui m’a fait perdre un temps fou), il faut sélectionner la case à cocher Clear the configuration area before launching (onglet Configuration du launch) :

Relancez et nous nous retrouvons à nouveau avec l’erreur NullPointeurException.

Pour corriger l’erreur de NullPointeurException, il faut que les bundles Spring Extender (qui s’occupe de charger les fichier XML Spring contenus dans les bundles) et Implémentation de services (org.dynaresume.services.impl) soient démarrés, autrement dit leur propriété Auto-Start doit être mises à true.

Pour cela,

  • positionnez la propriété Auto-Start à true du bundle Spring Extender:
  • positionnez la propriété Auto-Start à true du bundle Implémentation de services (org.dynaresume.services.impl):

Relancez et vous pourrez constater que l’application RCP affiche correctement la liste des Users.

Spring DM <osgi:reference (SpringExtensionFactory)

Dans cette section nous allons utiliser Spring DM pour récupérer le service UserService via <osgi:reference comme nous l’avons effectué dans le billet [step8] dans le bundle OSGi client org.dynaresume.simpleosgiclient. Pour rappel ce dernier lance une Thread FindAllUsersThread qui fait appel au service UserService et affiche dans la console toutes les 5 secondes la liste des Users. Cette Thread est instanciée et lancée par Spring en la déclarant en tant que bean :

<bean id="FindAllUsersThread" class="org.dynaresume.simpleosgiclient.internal.FindAllUsersThread"
	init-method="start" destroy-method="interrupt">
	<property name="userService" ref="userService"></property>
</bean>

ce qui permet de récupérer le service UserService par injection de dépendance :

public class FindAllUsersThread extends Thread {
...
	private UserService userService;

	public void setUserService(UserService userService) {
		this.userService = userService;
	}
         ...
}

Dans le cas de notre application RCP la classe org.dynaresume.simpleosgiclient.View est instancié par le framework Eclipse RCP en la déclarant dans le fichier plugin.xml :

   <extension
         point="org.eclipse.ui.views">
      <view
            name="View"
            class="org.dynaresume.simplercpclient.View"
            id="org.dynaresume.simplercpclient.view">
      </view>
   </extension>

Nous souhaitons récupérer le service par injection de dépendance dans la classe View :

public class View extends ViewPart {
        ...	
	private UserService userService;
	
	public void setUserService(UserService userService) {
		this.userService = userService;
	}
        ...
}

Ce qui implique que cette classe doit être instancié par le conteneur Spring et plus par le framework Eclipse RCP. Nous allons expliquer dans cette section comment effectuer ceci en utilisant la classe org.eclipse.springframework.util.SpringExtensionFactory créé par Martin Lippert.

Pour démarrer cette section, nous repartons des projets expliqués dans le pré-requis. Vous pouvez téléchargez l’ensemble des projets expliqués dans cette section dans le zip org.dynaresume_step13-springextensionfactory.zip.

IExtensionFactory

Avant d’expliquer comment utiliser org.eclipse.springframework.util.SpringExtensionFactory, nous allons voir comment il est possible en Eclipse RCP d’utiliserdans les déclaration des point d’extension du fichier plugin.xml, sa propre factory de classes à l’aide de l’interface org.eclipse.core.runtime.IExecutableExtensionFactory au lieu de déclarer directement la classe View.

Autrement dit ici nous allons créer une factory org.dynaresume.simplercpclient.internal.MyExtensionFactory qui retourne une instance View et la déclarer dans le fichier plugin.xml comme suit:

<extension
         point="org.eclipse.ui.views">
      <view
            name="View"
            class="org.dynaresume.simplercpclient.internal.MyExtensionFactory"
            id="org.dynaresume.simplercpclient.view">
      </view>
   </extension>

Cette explication permettra de bien comprendre ensuite comment fonctionne en interne org.eclipse.springframework.util.SpringExtensionFactory. Pour démarrer cette section nous allons repartir des projets contenus dans le zip org.dynaresume_step12.zip.

Vous pouvez téléchargez les projets expliqués dans cette section dans le zip org.dynaresume_step13-myextensionfactory.zip.

MyExtensionFactory – IExecutableExtensionFactory

Le framework Eclipse fournit une interface org.eclipse.core.runtime.IExecutableExtensionFactory:

package org.eclipse.core.runtime;

 import org.eclipse.core.runtime.CoreException;

public interface IExecutableExtensionFactory {
     /**
      * Creates and returns a new instance.
      *
      * @exception CoreException if an instance of the executable extension
      * could not be created for any reason
      */
     Object create() throws CoreException;
}

Cette interface impose d’implémenter la méthode IExecutableExtensionFactory#create() qui doit retourner une instance d’un objet. Dans notre cas nous souhaitons retourner une instance de la classe org.dynaresume.simplercpclient.View.

Pour cela créér dans le projet org.dynaresume.simplercpclient la classe org.dynaresume.simplercpclient.internal.MyExtensionFactory comme suit:

package org.dynaresume.simplercpclient.internal;

import org.dynaresume.simplercpclient.View;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtensionFactory;

public class MyExtensionFactory implements IExecutableExtensionFactory {

	public Object create() throws CoreException {
		return new View();
	}

}

Modifiez la déclaration XML du point d’extension de la View dans le fichier plugin.xml comme suit:

<extension
         point="org.eclipse.ui.views">
      <view
            name="View"
            class="org.dynaresume.simplercpclient.internal.MyExtensionFactory"
            id="org.dynaresume.simplercpclient.view">
      </view>
   </extension>

Vous pouvez relancez l’application et vérifiez que la View s’instancie correctement.

MyExtensionFactory – IExecutableExtension

Il est possible de récupérer dans notre factory les informations de la déclaration XML de notre View (attribut XML name, class, id…) en implémentant l’interface org.eclipse.core.runtime.IExecutableExtension:

package org.eclipse.core.runtime;

import org.eclipse.core.runtime.CoreException;

public interface IExecutableExtension {

	public void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException;

}

Cette interface permet de récupérer l’interface org.eclipse.core.runtime.IConfigurationElement qui à une méthode IConfigurationElement#getAttribute(String name) qui retourne la valeur de l’attribut name. Nous pouvons par conséquent imaginer de récupérer l’attribut XML « id » de la View et instancier la bonne classe en fonction de celui-ci. Pour cela modifiez la classe
org.dynaresume.simplercpclient.internal.MyExtensionFactory comme suit:

package org.dynaresume.simplercpclient.internal;

import org.dynaresume.simplercpclient.View;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IExecutableExtensionFactory;

public class MyExtensionFactory implements IExecutableExtension,
		IExecutableExtensionFactory {

	private Object bean = null;

	public Object create() throws CoreException {
		return bean;
	}

	public void setInitializationData(IConfigurationElement config,
			String propertyName, Object data) throws CoreException {
		String id = config.getAttribute("id");
		if ("org.dynaresume.simplercpclient.view".equals(id)) {
			bean = new View();
		}
	}

}

Dans cet exemple nous avons instancié la classe View si l’attribut id à la valeur « org.dynaresume.simplercpclient.view ».

Imaginez que votre factory ait accès à l’ApplicationContext Spring qui contient des bean déclarés, cet id peut être l’id d’un bean. C’est d’ailleurs comme cela que org.eclipse.springframework.util.SpringExtensionFactory fonctionne. Cette classe implémente les 2 interfaces décrites ci-dessus et à utilise l’ApplicationContext du bundle (qui est enregistré en tant que service OSGi).

SpringExtensionFactory

Dans cette section nous allons mettre en place l’injection de dépendance du service UserService dans la View avec SpringExtensionFactory.

Pour démarrer cette section nous allons repartir des projets contenus dans le zip org.dynaresume_step12.zip.

Vous pouvez téléchargez les projets expliqués dans cette section dans le zip org.dynaresume_step13-springextensionfactory.zip.

SpringExtensionFactory, ca marche comment?

SpringExtensionFactory est un projet développé par Martin Lippert qui permet de gérer les instantiations des ViewPart, EditorPart Eclipse RCP dans un conteneur Spring, ce qui permet de bénéficier ainsi de l’injection de dépendances de Spring dans les ViewPart-EditorPart. Cette classe suit l’idée que j’ai décrite dans la section précédante « IExtensionFactory » . Autrement dit nous pourrons bénéficier de l’injection de dépendance dans la classe View pour récupérer le service UserService:

public class View extends ViewPart {
...
	private UserService userService;
	
	public void setUserService(UserService userService) {
		this.userService = userService;
	}
...
}

En déclarant dans le point d’extension de la View du fichier XML plugin.xml comme ceci :

...
      <view
            name="View"
            class="org.eclipse.springframework.util.SpringExtensionFactory"
            id="org.dynaresume.simplercpclient.view">
      </view>
...

Autrement dit l’attribut class n’est plus la classe org.dynaresume.simplercpclient.View mais org.eclipse.springframework.util.SpringExtensionFactory. L’id de la View peut ensuite être utilisé dans la déclaration XML bean dans le fichier module-context.xml pour indiquer la classe View à instancier par Spring :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
	<bean id="org.dynaresume.simplercpclient.view"
		class="org.dynaresume.simplercpclient.View"
		scope="prototype">
		<property name="userService" ref="userService"></property>
	</bean>
</beans>

Ici nous pouvons voir que nous injectons le service UserService dans la classe View en faisant référence à un bean d’id userService qui est déclaré dans le fichier XML Spring module-osgi-context.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi"
	xsi:schemaLocation="http://www.springframework.org/schema/osgi  
       http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd
       http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

	<osgi:reference id="userService" interface="org.dynaresume.services.UserService"
		timeout="1000" />

</beans> 

Ce bean récupère dans le registre de services OSGi le service UserService.

Pour rédiger ce billet je me suis appué sur l’article Tutorial: Spring OSGi + Eclipse RCP qui explique comment utiliser SpringExtensionfactory et qui liste d’autres manières d’utiliser SpringExtensionfactory (ex : SpringExtensionfactory:myview).

SpringExtensionfactory – Target Platform

Le projet de Marin Lippert peut se télécharger dans le zip org.eclipse.springframework.util_1.0.3.zip dans son billet New version of Spring Extension Factory available. Ce zip contient dans le répertoire plugin un JAR org.eclipse.springframework.util_1.0.3.jar qui est une bundle OSGi que l’on peut utiliser tel quel et qui contient aussi les sources de son projet.

Téléchargez et récupérez le JAR et stocker le dans le projet Target Platform spring-target-platform dans le répertoire client:

Ajoutez à la Target Platform le répertoire client comme expliqué ici.

Dans la page Add Content du wizard, ajoutez la valeur ${workspace_loc}/spring-target-platform/client pour faire référence au JARs du répértoire client du projet du workspace. Cliquez sur OK :

Il faudra bien penser à sélectionner le bundle org.eclipse.springframework.util dans les launch.

MANIFEST.MF

Importez les packages suivants :

  • org.dynaresume.services. En effet pour récupérer le service UserService dans la View de l’application RCP nous devons utiliser dans la classe View l’interface UserService.
  • org.eclipse.springframework.util. En effet l’application RCP dépendra de la classe org.eclipse.springframework.util.SpringExtensionFactory utilisé dans la déclaration de la View du plugin.xml. L’import de ce package permet d’éviter une erreur de type ClassNotFoundException sur cette classe:
    !ENTRY org.eclipse.equinox.registry 4 1 2009-12-28 20:09:04.484
    !MESSAGE Unable to create view ID org.dynaresume.simplercpclient.view: Plug-in org.dynaresume.simplercpclient was unable to load class org.eclipse.springframework.util.SpringExtensionFactory.
    !STACK 0
    java.lang.ClassNotFoundException: org.eclipse.springframework.util.SpringExtensionFactory
    at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:489)
    ...

View ( UserService DI)

Nous allons modifier la classe View du step12 pour utiliser le service UserService récupéré par le mécanisme d’injection de dépendance. Pour cela modifiez la classe org.dynaresume.simplercpclient.View comme suit :

  • mise en place de la dépendance d’injection en définissant un setter sur le service UserService:
    public class View extends ViewPart {
    ...
    	private UserService userService;
    	
    	public void setUserService(UserService userService) {
    		this.userService = userService;
    	}
    ...
    }
  • appel du service pour récupérer la liste des Users et la renseigner au viewer :
    public void createPartControl(Composite parent) {
    ...
    	viewer.setInput(userService.findAllUsers());
    	}

A premier abord, on peut se dire que le code userService.findAllUsers() peut lancer un NullPointerException dans le cas ou l’instance userService est null. Vous pourrez vérifiez en debug que cette instance n’est jamais null, même si le service n’existe pas (mécanisme de proxy Spring).

module-context.xml

Créez le fichier module-context.xml dans le répertoire META-INF/spring du bundle comme suit :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
	<bean id="org.dynaresume.simplercpclient.view"
		class="org.dynaresume.simplercpclient.View"
		scope="prototype">
		<property name="userService" ref="userService"></property>
	</bean>
</beans>

Modifiez la déclaration de la View dans le plugin.xml comme suit:

...
      <view
            name="View"
            class="org.eclipse.springframework.util.SpringExtensionFactory"
            id="org.dynaresume.simplercpclient.view">
      </view>
...

module-osgi-context.xml

Créez le fichier module-osgi-context.xml dans le répertoire META-INF/spring du bundle comme suit :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi"
	xsi:schemaLocation="http://www.springframework.org/schema/osgi  
       http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd
       http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

	<osgi:reference id="userService" interface="org.dynaresume.services.UserService"
		timeout="1000" />

</beans> 

Run Full – RCP Full Client

Ici nous allons nous occuper du launch de type Full Client RCP (pas Client (Serveur). Pour cela, dupliquez (car nous repartirons de celui-ci pour le launch client serveur) le launch DynaResume – org.dynaresume.simplercpclient.application.launch et renommez le en OSGi DynaResume – RCP Full Client.

Cochez les mêmes bundles que le launch du client simple OSGi OSGi DynaResume – Full Client.launch, sans oublier de sélectionner org.eclipse.springframework.util :

  • org.dynaresume.config.log4j
  • org.dynaresume.domain
  • org.dynaresume.services
  • org.dynaresume.services.impl
  • org.dynaresume.simplercpclient (déja coché)

Et ceux de la Target Platform (ceux de Spring) :

  • com.springsource.org.aopalliance
  • com.springsource.sl4j.api
  • com.springsource.sl4j.log4j
  • com.springsource.sl4j.org.apache.commons.logging
  • org.springframework.aop
  • org.springframework.beans
  • org.springframework.context
  • org.springframework.core
  • org.springframework.osgi.core
  • org.springframework.osgi.extender
  • org.springframework.osgi.io
  • org.springframework.osgi.log4j.osgi

et celui de SpringExtensionFactory :

  • org.eclipse.springframework.util

Appliquez les 3 règles à suivre dans un launch de type RCP avec Spring DM, autrement dit:

  • cochez la case Clear the configuration area before launching
  • forcez à true la propriété Auto Start du bundle Spring Extender org.springframework.osgi.extender et par précaution assurez vous que ce bundle démarre avant les autres bundles qui ont des fichiers de configuration en rensignant le niveau Start Level.
  • forcez à true la propriété Auto Start du bundle d’implémentation des services org.dynaresume.services.impl

Lancez (via le bouton Run) le launch et l’application RCP doit s’afficher avec la liste des Users récupérés du service UserService.

OSGi DynaResume – RCP Client (Server)

Ici nous allons nous occuper du launch de type Client (Server) RCP. Dans ce launch le bundle qui joue le rôle d’implémentation de services est celui du remoting org.dynaresume.remoting.importer.http. Pour cela, renommez le launch DynaResume – org.dynaresume.simplercpclient.application.launch en OSGi DynaResume – RCP Client (Server).

Cochez les mêmes bundles que le launch du client simple OSGi OSGi DynaResume – Client (Server).launch, sans oublier de sélectionner org.eclipse.springframework.util :

  • org.dynaresume.config.log4j
  • org.dynaresume.config.remoting.importer.http
  • org.dynaresume.domain
  • org.dynaresume.services
  • org.dynaresume.remoting.importer.http
  • org.dynaresume.simplercpclient (déja coché)

Et ceux de la Target Platform (ceux de Spring) :

  • com.springsource.javax.servlet (2.4.0 ou 2.5.0)
  • com.springsource.org.aopalliance
  • com.springsource.sl4j.api
  • com.springsource.sl4j.log4j
  • com.springsource.sl4j.org.apache.commons.logging
  • org.springframework.aop
  • org.springframework.beans
  • org.springframework.context
  • org.springframework.core
  • org.springframework.osgi.core
  • org.springframework.osgi.extender
  • org.springframework.osgi.io
  • org.springframework.osgi.log4j.osgi
  • org.springframework.web

et celui de SpringExtensionFactory :

  • org.eclipse.springframework.util

Appliquez les 3 règles à suivre dans un launch de type RCP avec Spring DM, autrement dit:

  • cochez la case Clear the configuration area before launching
  • forcez à true la propriété Auto Start du bundle Spring Extender org.springframework.osgi.extender et par précaution modifier assurez vous que ce bundle démarre avant les autres bundles qui ont des fichiers de configuration en rensignant le niveau Start Level.
  • forcez à true la propriété Auto Start du bundle d’implémentation des services org.dynaresume.remoting.importer.http
Lancement Client & Serveur

Pour tester notre launch Client (Server), nous devons lancer le serveur via les launch OSGi DynaResume – Server Tomcat 5.5 ou OSGi DynaResume – Server Jetty 6.1.9. Avant de lancer l’un ou l’autre il faut s’assurer que le bundle org.dynaresume.simplercpclient n’est pas sélectionné dans ces 2 launch.

Lancez (via le bouton Run) le launch OSGi DynaResume – Server Tomcat 5.5, le serveur est démarré.

Lancez (via le bouton Run) le launch OSGi DynaResume – RCP Client (Server) et l’application RCP doit s’afficher avec la liste des Users récupérés du service UserService.

Gestion d’erreurs

Dans une architecture de type Client/Serveur, le client qui fait appel au service UserService, peut être confronté à 2 types d’erreurs :

  • RemoteConnectFailureException : le serveur n’est pas lancé et le bundle org.dynaresume.remoting.importer.http qui tente d’appeler le service lance une erreur de Connection.
  • ServiceUnavailableException : le service UserService n’est pas disponible dans le registre de services OSGi.

Des lors qu’il y a une exception lancée à l’appel du servive UserService (erreur de type « ServiceUnavailableException » ou « RemoteConnectFailureException »), la View avec la liste des Users ne s’affichent pas. Le message de l’erreur s’affiche avec un bouton Détails>>>.

Nous souhaitons maintenant afficher un bouton  » Refresh Users » qui appelle le service UserService pour rafraichir ensuite la Table SWT qui affiche la liste des Users. Qu’il y ait un erreur ou non, la View affichera tout le temps la Table SWT et son bouton  » Refresh Users »:

REMARQUE : le bouton est énorme et pas bien placé, mais je ne voulais pas parler des Layout SWT dans ce billet. Ceci est un autre sujet.

Règles à respecter pour gérer correctement les erreurs de type service OSGi géré par Spring DM

Les 2 règles que nous allons expliqué dans la suite de billet pour « ignorer » (sans afficher l’erreur à la place de la View) les 2 types d’erreur sont :

  • catcher l’appel des méthodes services OSGi. Ceci permet de gérer les erreurs de type RemoteConnectFailureException. Pour cela nous allons catcher les appels des méthodes de UserService:
    try {
    	viewer.setInput(userService.findAllUsers());
    } catch (Exception e) {
    	e.printStackTrace();
    }
  • modifier la cardinalité à « 0..1 » dans la déclaration du service OSGi UserService. Ceci permet de gérer les erreurs de type ServiceUnavailableException. Dans notre cas nous allons effectuer :
    <osgi:reference id="userService" interface="org.dynaresume.services.UserService"
    		cardinality="0..1"
    		timeout="1000" />

View – Refresh Button

Ici nous allons modifier la classe View pour ajouter un bouton « Refresh Users » qui rafraîchit la liste des Users en faisant appel au service UserService. Pour cela modifiez la classe

  • en ajoutant la méthode privée refresh qui fait appel au service UserService :
    public void refresh() {
    		viewer.setInput(userService.findAllUsers());
    	}

    A ce stade nous ne catchons pas l’utilisation du service pour montrer qu’il est important de catcher les erreurs de type RemoteConnectFailureException.

  • en créant un Button SWT où l’on branche un listener sur le click du bouton pour faire appel à la méthode refresh. Cette méthode est aussi appelée à la creation de la View pour rafraîchir la liste de Users :
    public void createPartControl(Composite parent) {
    	viewer = new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
    	viewer.setContentProvider(new ViewContentProvider());
    	viewer.setLabelProvider(new ViewLabelProvider());
    
    	// Create Refresh Button
    	Button refreshButton = new Button(parent, SWT.NONE);
    	refreshButton.setText("Refresh Users");
    	refreshButton.addSelectionListener(new SelectionAdapter() {
    		@Override
    		public void widgetSelected(SelectionEvent e) {
    			refresh();
    		}
    	});
    	// Refresh Users list
    	refresh();
    }

Voici le code complet de la classe View :

package org.dynaresume.simplercpclient;

import java.util.Collection;

import org.dynaresume.domain.User;
import org.dynaresume.services.UserService;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.ViewPart;

public class View extends ViewPart {
	public static final String ID = "org.dynaresume.simplercpclient.view";

	private TableViewer viewer;

	private UserService userService;

	public void setUserService(UserService userService) {
		this.userService = userService;
	}

	class ViewContentProvider implements IStructuredContentProvider {
		public void inputChanged(Viewer v, Object oldInput, Object newInput) {
		}

		public void dispose() {
		}

		@SuppressWarnings("unchecked")
		public Object[] getElements(Object parent) {
			return ((Collection<User>) parent).toArray();
		}

	}

	class ViewLabelProvider extends LabelProvider implements
			ITableLabelProvider {
		public String getColumnText(Object obj, int index) {
			User user = (User) obj;
			switch (index) {
			case 0:
				return user.getLogin();
			}
			throw new IllegalArgumentException("bad index!");
		}

		public Image getColumnImage(Object obj, int index) {
			return getImage(obj);
		}

		public Image getImage(Object obj) {
			return PlatformUI.getWorkbench().getSharedImages().getImage(
					ISharedImages.IMG_OBJ_ELEMENT);
		}
	}

	public void createPartControl(Composite parent) {
		viewer = new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL
				| SWT.V_SCROLL);
		viewer.setContentProvider(new ViewContentProvider());
		viewer.setLabelProvider(new ViewLabelProvider());

		// Create Refresh Button
		Button refreshButton = new Button(parent, SWT.NONE);
		refreshButton.setText("Refresh Users");
		refreshButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				refresh();
			}
		});
		// Refresh Users list
		refresh();

	}

	public void refresh() {
		viewer.setInput(userService.findAllUsers());
	}

	public void setFocus() {
		viewer.getControl().setFocus();
	}
}

RemoteConnectFailureException – Serveur non lancé

Le client lance des erreurs de type RemoteConnectFailureException, lorsque le serveur n’est pas lancé et le bundle org.dynaresume.remoting.importer.http qui tente d’appeler le service lance une erreur de Connection.

Pour simuler cette erreur, il faut que la propriété Auto Start de org.dynaresume.remoting.importer.http soit à true.Aarrêtez le serveur et relancez le launch OSGi DynaResume – RCP Client (Server). L’application RCP affiche une erreur à la place de la View :

La console OSGi affiche l’erreur suivante :

org.springframework.remoting.RemoteConnectFailureException: Could not connect to HTTP invoker remote service at [http://localhost:8080/dynaresume-server/remoting/UserService
]; nested exception is java.net.ConnectException: Connection refused: connect
...

Pour éviter cette erreur, nous allons catcher l’erreur lors des appels du méthodes du service, autrement dit modifier la méthode refresh comme suit:

public void refresh() {
	try {
		viewer.setInput(userService.findAllUsers());
	} catch (Exception e) {
		e.printStackTrace();
	}
}

Relancez le launch OSGi DynaResume – RCP Client (Server), l’application RCP affiche la liste des users vide et la console OSGi affiche l’erreur RemoteConnectFailureException.

Relancez le serveur via le launch OSGi DynaResume – Server Tomcat 5.5 puis cliquez sur le bouton Refresh Users, la liste des Users doit se rafraîchir.

ServiceUnavailableException – Services impl non lancé

le service UserService n’est pas disponible dans le registre de services OSGi. Ce cas arrive lorsque le bundle org.dynaresume.remoting.importer.http qui enregistre le service UserService dans le registre de services OSGi n’est pas démarré. Pour simuler cette erreur, forcez à default (qui vaut false) la propriété Auto Start du bundle des services org.dynaresume.remoting.importer.http. Si vous relancez le launch OSGi DynaResume – RCP Client (Server), l’application RCP affiche une erreur à la place de la View :

La console OSGi affiche l’erreur suivante :

!MESSAGE Unable to create view ID org.dynaresume.simplercpclient.view: Plug-in "org.dynaresume.simplercpclient" was unable to execute setInitializationData on an instance of "org.eclipse.springframework.util.SpringExtensionFactory".
!STACK 0
java.lang.RuntimeException: application context for bundle org.dynaresume.simplercpclient not found
at org.eclipse.springframework.util.SpringExtensionFactory.setInitializationData(SpringExtensionFactory.java:68)
...

Le catch dans la méthode refresh ne suffit pas. Si vous mettez un point d’arrêt dans la méthode View#createPartControl(Composite parent) vous remarquerez qu’elle n’est jamais appelée. Ceci signifie que le bean View n’a pas pu s’instancier. Ce problème est du au fait que le service OSGi UserService est obligatoire et dans le cas où le bundle org.dynaresume.remoting.importer.http n’est pas lancé, aucun bundle n’enregistre de services UserService dans le registre de services OSGi.

Pour indiquer qu’un service est obligatoire ou non, ceci s’effectue via l’attribut cardinality. Pour plus d’informations je vous conseille de lire The cardinality Attribute. En effet notre déclaration du service s’effectue comme ceci :

<osgi:reference id="userService" interface="org.dynaresume.services.UserService"
		timeout="1000" />

Ici nous n’avons pas indiqué l’attribut cardinality qui par défaut prend la valeur 1..1 qui signifie que le service est obligatoire. Pour indiquer que le service OSGi n’est pas obligatoire, il faut indiquer l’attribut cardinality avec la valeur 0..1. Modifiez le fichier XML module-osgi-context.xml comme suit:

<osgi:reference id="userService" interface="org.dynaresume.services.UserService"
		cardinality="0..1"
		timeout="1000" />

Relancez le launch OSGi DynaResume – RCP Client (Server), l’application RCP affiche la liste des users vide et la console OSGi affiche l’erreur suivante :

org.springframework.osgi.service.ServiceUnavailableException: service matching filter=[(objectClass=org.dynaresume.services.UserService)] unavailable
at org.springframework.osgi.service.importer.support.internal.aop.ServiceDynamicInterceptor.getTarget(ServiceDynamicInterceptor.java:419)
...

Tappez ss + entrée

id State Bundle
...
24 RESOLVED org.dynaresume.remoting.importer.http_1.0.0.qualifier
...

On peut constater que le bundle org.dynaresume.config.remoting.importer.http n’est pas à l’état ACTIVE.

Tappez start 24 (dans mon cas) pour lancer le bundle org.dynaresume.config.remoting.importer.http puis cliquez sur le bouton Refresh Users, la liste des Users doit se rafraîchir.

Conclusion

Nous avons vu dans ce billet comment consommer un service qui a été déclaré via Spring DM <osgi:reference dans une ViewPart RCP. La classe SpringExtensionFactory permet d'injecter les services déclarés via Spring DM dans des ViewPart ou autres (EditorPart par exemple). Nous avons aussi qu'il est important de :

Dans les prochains billets nous mettrons en place une base de donnée qui contiendra la liste des Users. Le service UserService fera appel à une couche DAO (Data Access Object, encore appelé Repository) pour récupérer les données de la base de données. Nous verrons encore la puissance de Spring qui permet de gérer la problématique de gestion des connections à la base via AOP.

Vous pouvez lire le billet suivant [step14].

  1. janvier 8, 2010 à 8:20

    Salut Angelo,

    Très impressionnant … une mine d’or. Tu devrais penser à écrire un livre

    Mickael

  2. janvier 8, 2010 à 9:18

    Salut Mickael,

    Wow que d’éloge!

    Merci beaucoup encore à toi pour tes encouragements.

    Angelo

  1. janvier 8, 2010 à 6:16
  2. janvier 29, 2010 à 3:57
  3. avril 27, 2010 à 4:08

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :