[Update] 03/03/2009 : English version available !
Dans cet article je vous propose de regarder un exemple simple d’utilisation du framework d’intégration léger Apache Camel avec l’un des frameworks webs les plus intéressants, Apache Wicket. Je vous ai laissé une archive avec le code source, prêt à être compilé afin que vous puissiez tester tranquillement chez vous. Si vous souhaitez réutiliser ce code ou publier un article sur un journal papier, je vous remercie de me contacter avant de le faire.
Qu’est-ce qu’Apache Wicket ?
Wicket est un framework web Java open-source orienté composant. Contrairement à un framework orienté actions comme Struts 1, Wicket vous permet de définir en Java vos pages et vos composants, d’intégrer ensuite dans votre code HTML des tags légers, afin enfin de rendre le tout dans un serveur d’application léger. Lorsque je travaillais chez Reuters, j’étais en charge du framework Karma, dont l’architecture et la philosophie est vraiment très proche de Wicket. Pour la création d’application web, c’est donc une solution intéressante à envisager.
Dans l’exemple à suivre vous verrez comment créer votre première page, un composant particulier, et utiliser Spring avec Camel pour charger la configuration.
Ce qu’il faut retenir de Wicket c’est que le modèle d’événements du côté du serveur est semblable à Swing. Vous implémentez des listeners et sur la base du pattern Observer-Observable, le moteur de Wicket se charge de vous notifier selon les actions de l’utilisateur dans la page Web. Avec un support d’ajax très correct, c’est donc une solution intéressante à connaître.
Qu’est-ce qu’Apache Camel ?
Apache Camel est un framework basé sur Spring d’intégration. Il implémente les EIP (Enterprise Integration Patterns) en proposant d’une part l’utilisation du Xml pour la configuration (comme Mule) ou d’autre part un DSL en Java très simple, ce que nous verrons dans cet exemple.
Apache Camel est complémentaire de Mule. Mule est un framework léger d’intégration, Camel est une boîte à outils de patterns que vous pouvez d’ailleurs utiliser avec Mule, celui-ci se chargeant alors de la partie connexions, modèle de thread, là où Camel se charge du routage, de la transformation et de la médiation. Ici je n’utilise que Camel, afin simplement de vous montrer son fonctionnement.
Explication de la démonstration
J’avais préparé cette démonstration début décembre dans l’idée de la présenter lors de ma présentation à Devoxx. Vu la durée de la présentation, il n’était pas envisageable de se lancer finalement dans une démo, j’ai donc gardé ce code de côté… jusqu’à aujourd’hui !
… et pourquoi tu n’as pas pris Mule/Spring Integration/Apache ServiceMix/OpenESB/MachinTrucPouetPouet
J’aimerai bien montrer une autre solution basé sur Mule uniquement, afin que chacun se fasse sa propre idée. Je dois tester Spring Integration avant de pouvoir vous dire et vous montrer un peu de code, donc patience ! Pour le reste on verra.
Voici comment se présente l’application, une fois celle-ci déployée avec un « mvn jetty:run »
En tant que client, je mets mon nom dans le formulaire et un petit message afin de demander pourquoi ma superbe console PS3 n’est pas arrivée à Noël. Je clique sur envoyer et l’interface affiche « Thanks ».
Que se passe-t-il derrière ?
Lorsque je clique sur envoyer, Apache Camel reçoit de Wicket un message. Il va transformer ce message en fichier et le stocker dans un répertoire. Ensuite, on imagine qu’un autre processus de Camel va surveiller ce répertoire, se réveiller, et lire le message. Il va charger un template Velocity afin de générer un email, puis enfin envoyer le tout via SMTP vers une boîte email de test. J’ai utilisé MockMail comme nous verrons plus loin tout à l’heure.
Vue administrateur
Pour démontrer le fonctionnement, j’ai ensuite implémenté une page qui liste les emails présents dans la boîte aux lettres. Nous verrons comment cela fonctionne tout à l’heure.
Premiers pas avec Wicket
Pour la suite, vous pouvez récupérer l’archive que j’ai mis à la fin de cet article, cela sera plus simple pour suivre le code.
En premier lieu nous allons regarder la class InnoteriaWicketApplication. Il s’agit du point d’entrée de l’application Wicket. J’utilise Mock JavaMail pour déclarer une boîte aux lettres. La class ReportIncidentEndpoint est l’un de mes composants Apache Camel, il représente ici la porte vers Camel pour Wicket.
public class InnoteriaWicketApplication extends WebApplication { private ApplicationContext ctx; private ReportIncidentEndpoint endpoint; private Mailbox mailbox; // mock javamail private static ISpringContextLocator CTX_LOCATOR = new ISpringContextLocator() { public ApplicationContext getSpringContext() { return InnoteriaWicketApplication.get().ctx; } }; public InnoteriaWicketApplication() { } public Class getHomePage() { return HomePage.class; } public static InnoteriaWicketApplication get() { return (InnoteriaWicketApplication) Application.get(); } @Override protected void init() { ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Address address= null; try { address = new InternetAddress("incident@innoteria.fr"); } catch (AddressException e) { e.printStackTrace(); } mailbox = new Mailbox(address); } /** * This trick is used to decorate a specified template class with a SpringBeanLocator * so that I can use Spring to load my Apache Camel configuration. * @param clazz will be the endpoint to decorate * @param <T> template * @return a decorated class. */ private <T> T createProxy(Class<T> clazz){ return (T) LazyInitProxyFactory.createProxy(clazz, new SpringBeanLocator(clazz, CTX_LOCATOR)); } public ReportIncidentEndpoint getReportIncidentEndpoint(){ if(endpoint==null){ endpoint=createProxy(ReportIncidentEndpoint.class); } return endpoint; } }
Une fois votre application Wicket déclarée, il est temps de coder votre page d’accueil. La class HomePage étend WebPage. Nous voyons qu’un composant FeedbackPanel est déclaré avec 2 champs de type texte. Un formulaire est ensuite ajouté. La méthode onSubmit sera déclenchée lorsque le visiteur soumet ce formulaire dans le navigateur. On verra alors comment le service Apache Camel récupère l’instance de MyReportIncident pour la suite.
En quelques lignes, vous voyez qu’il est simple de coder un formulaire de saisie et de déclencher une action lorsque l’utilisateur soumet celui-ci.
public class HomePage extends WebPage { private static final long serialVersionUID = 1L; public HomePage(final PageParameters parameters) { // un de mes composants FeedbackPanel feedbackPanel = new FeedbackPanel("feedback"); add(feedbackPanel); final TextField yourName = new TextField("yourname", new Model()); final TextField yourMessage = new TextField("yourmessage", new Model()); yourMessage.setRequired(true) ; yourName.setRequired(true); Form form = new Form("formtest") { protected void onSubmit() { ReportIncidentEndpoint service = InnoteriaWicketApplication.get().getReportIncidentEndpoint(); service.reportIncident( new MyReportIncident( yourName.getModelObject().toString(), yourMessage.getModelObject().toString() )); info("Thanks!"); } }; form.add(yourName); form.add(yourMessage); // Add a form with an onSubmit implementation that sets a message add(form); /* java code */ add(new BookmarkablePageLink("link", CheckMailbox.class)); } }
Il faut ensuite aller voir du côté du code HTML pour comprendre comment Wicket fonctionne. Prenons cet extrait de HomePage.html
......
On retrouve les id des composants déclarés dans la class Java. L’avantage de Wicket est qu’il permet de réaliser sa mise en page en HTML et d’utiliser la fonction aperçu HTML d’IDEA IntelliJ. Finalement le code de la page reste lisible et ce n’est que du HTML.
Voilà pour cette mini introduction à Wicket, je vous laisse regarder le code tranquillement, et on reprend après la coupure pub.
Le côté Camel
Notre point d’entrée dans Camel est la class ReportIncidentEndpoint. La méthode reportIncident est notifiée par Wicket, elle publie sur une Queue en mémoire le message. La class ReportIncidentEndpoint est un bean spring qui est injecté un peu spécialement dans la class InnoteriaWicketApplication.
public class ReportIncidentEndpoint implements Serializable { private CamelContext context; private static final long serialVersionUID=2233L; public ReportIncidentEndpoint() throws Exception { // create the context context = new DefaultCamelContext(); // append the routes to the context context.addRoutes(new ReportIncidentRoutes()); // at the end start the camel context context.start(); } /** * Callback method executed from the HomePage when the user * clicks on submit. * @param message is a message created by Wicket * @return a status message that is shown on HomePage. */ public String reportIncident(MyReportIncident message) { // create the producer template to use for sending messages ProducerTemplate producer = context.createProducerTemplate(); // Send the message to an in-memory queue and return // See GenerateEmailStoreToFilePollFolderAndSendFile // it will read the message from direct:start in-memory queue Object mailBody = producer.sendBody("direct:start", message); System.out.println("Body:" + mailBody); return "OK"; } }
Voyons maintenant deux exemples d’intégration avec Apache Camel. J’ai déclaré explicitement 2 routes, pour vous montrer l’un des principes de Camel. Regardez la class ReportIncidentRoutes. Celle-ci étend la class RouteBuilder de Camel qui vous permet de définir pour votre contexte le chemin de traitement. Notez qu’il est aussi possible de faire appel à du XML, mais ici je trouve élégant la définition de votre route en java, surtout lorsque finalement votre architecture ne changera plus.
La première route lit la queue « direct:start », appelle ensuite un template Velocity pour générer un email, ajoute le nom d’un fichier et enfin, enregistre dans le fichier errorEmail.txt le message de l’utilisateur. Notez que je pourrais tout aussi bien envoyer vers une Queue JMS, peu importe. Ici l’idée est de vous montrer le fonctionnement de Camel.
public class ReportIncidentRoutes extends RouteBuilder { public void configure() throws Exception { from("direct:start") .to("velocity:MailBody.vm") .setHeader(FileComponent.HEADER_FILE_NAME, "errorEmail.txt") .to("file:///./target/demoCamel?append=false"); ...
La deuxième route surveille ce répertoire, prend le premier fichier déposé, ajoute un header qui sera le sujet de l’email et enfin expédie le tout à mon serveur SMTP. Souvenez-vous, j’ai lancé dans ma class InnoteriaWicketApplication une instance de Mailbox (mock mail) et j’ai donc bien un serveur de test qui écoute sur le port 25, ce que vous pouvez vérifier avec un telnet localhost 25.
L’email est donc réellement expédié pour Camel.
from("file:///./target/demoCamel") .setHeader("subject", constant("New incident reported")) .to("smtp://nicolas@localhost?password=secret&to=incident@innoteria.fr");
Enfin voici un autre exemple un peu plus avancé de Camel qui vous montre sa puissance :
// exemple 1 si la cle foo de l'entete est egale a bar alors fait suivre a queue b from("queue:a").filter(header("foo").isEqualTo("bar")).to("queue:b"); // exemple 2 de gestion de queue d'erreur si rien ne correspond from("queue:c").choice() .when(header("foo").isEqualTo("bar")).to("queue:d") .when(header("foo").isEqualTo("cheese")).to("queue:e") .otherwise().to("queue:errorQueue");
Camel est donc intéressant car il vise à réduire la quantité de code technique habituellement nécessaire pour envoyer un email, lire un fichier, le transformer, envoyer un message vers une Topic JMS etc. L’idée de ce framework d’intégration est de faciliter les échanges avec les systèmes externes de votre application. En prenant l’exemple de l’envoi de mail je sais que certains vont me dire qu’il est possible de le faire autrement. Mais cet exemple est plus facile à écrire qu’un exemple avec du JMS par exemple. Donc si le sujet vous intéresse, allez voir sur le site d’Apache Camel qui contient pas mal d’exemples complets. Ce framework est facile à apprendre et il peut vous rendre service pour des cas d’utilisation relativement courant.
Comment lire le contenu de la boîte email et afficher les messages ?
Revenons à Wicket. Comment maintenant lire les messages en attente dans la Mailbox et les afficher à l’utilisateur ?
Si vous cliquez sur le lien « Check Incident Mailbox… » sur la page d’accueil, vous verrez alors cet écran :
La class CheckMailbox est une page Web wicket qui retrouve la liste des courriers électroniques envoyés à l’adresse « incident@innoteria.fr ». Nous passons ensuite notre iterator à un composant fait maison qui liste les messages et les affiche. J’ai adapté une class de l’api Mail car celle-ci n’étant pas sérialisable, elle ne peut pas être utilisé par Wicket directement. Hop on a ici le pattern Iterator et le pattern Adapter. Elle est bas belle la vie ?
public class CheckMailbox extends WebPage { private static final long serialVersionUID = 1L; public CheckMailbox(final PageParameters parameters) { Address address = null; try { address = new InternetAddress("incident@innoteria.fr"); } catch (AddressException e) { e.printStackTrace(); } Mailbox mailbox = Mailbox.get(address); // javax.mail.message subclasses are not serializable // thus we need this little trick to adapt our collection. // I don't wan use collections-utils and folks, it's too simple. InnoteriaMessageIterator imi=new InnoteriaMessageIterator(mailbox.iterator()); MessagePanel rv = new MessagePanel("repeatingPanel",imi); add(rv); } }
My first composant
La class MessagePanel est un composant Wicket, définit avec son propre code HTML. DisplayMessagePanel est un Panel contenu dans MessagePanel. Lorsque j’ai écrit ce code, je pensais ajouter d’autres widgets Wicket dans MessagePanel. Bref passons.
J’aime beaucoup l’élégance de wicket dans cette class. Sans chercher à être hyper générique, c’est simple, brutale et efficace. A l’exécution vous vous en doutez, ce code liste les messages et affiche donc la liste des réclamations des clients.
public class DisplayMessagePanel extends Panel { public DisplayMessagePanel(String id, final InnoteriaMessageIterator listOfMessages) { super(id); add(new RefreshingView("repeatingView") { @Override protected Iterator getItemModels() { return new ModelIteratorAdapter(listOfMessages.iterator()) { @Override protected IModel model(Object o) { return new CompoundPropertyModel((InnoteriaMessage) o); } }; } @Override protected void populateItem(Item item) { // Those 3 properties are read directly on InnoteriaMessage POJO // See CompoundPropertyModel javadoc for more details item.add(new Label("subject")); item.add(new Label("from")); item.add(new Label("content")); } }); } }
La conclusion de la fin pour terminer enfin
Voilà, j’ai passé pas mal de temps à écrire l’exemple complet. L’idée était de vous présenter un exemple complet basé sur Wicket d’une part et sur Apache Camel d’autre part. Il sera intéressant de faire le même code avec par exemple Spring Integration, Spring MVC afin de se faire son idée.
Avis aux amateurs !!!
Code source complet de cet article :
touilleur_wicket_camel_1.0.tar.gz