Webapp REST/XSLT avec Maven Tutoriel

Inria

Mavenisez une application Web.

Apprenez les bases de REST.

  • Comprendre le fonctionnement de base de Maven
  • Créer un projet Web et déboguer les problèmes
  • Importer une implémentation de REST avec Maven
  • Utiliser les annotations REST
  • Mapper une transformation XSLT sur une URL

Premiers pas avec Maven

Maven est :

  • une suite d'outils de gestion et de construction d'applications,
  • une réponse à la problématique d'industrialisation des développements,
  • et un écosystème majeur dans l'ingénierie logicielle.

Maven décrit un projet informatique, ses ressources, son packaging, sa documentation, etc, dans un document XML appelé POM (Project Object Model), et favorise l'usage de conventions plutôt que de la configuration explicite. C'est à dire qu'en spécifiant à minima un projet Maven, les sources seront trouvés à un emplacement convenu, les classes seront compilées également dans un emplacement convenu, etc.

Maven s'utilise en ligne de commande mais s'intègre bien dans les IDE tels qu'Eclipse.

Ce tutorial se propose de créer un projet Web avec Maven dans lequel une transformation XSLT pourra être invoquée avec un service REST.

Vérifiez en premier lieu que Maven est installé, sinon installez-le et ajoutez la commande mvn dans le path de votre interpréteur de commande :
(référez-vous à la documentation de Maven en cas de besoin)

$ mvn -v
Apache Maven 3.0.4 (r1232337; 2012-01-17 09:44:56+0100)
Maven home: /usr/share/maven
Java version: 1.7.0_12-ea, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.7.0_12.jdk/Contents/Home/jre
Default locale: fr_FR, platform encoding: UTF-8
OS name: "mac os x", version: "10.7.5", arch: "x86_64", family: "mac"

Création du projet

Maven en définitive ne fait pas grand chose, à part lire un fichier POM, et, grâce à son contenu, exécute la tâche demandée par l'utilisateur. Les commandes prennent corps dans des plugins que soit Maven "connait", soit qu'il est possible d'ajouter au POM.

Le plugin Maven Archetype permet de créer le squelette d'une application, et s'exécute avec le goal archetype:generate. Un goal est une tâche à accomplir qui accepte des arguments. La syntaxe d'exécution est : mvn pluginId:goalId -Dparam=value.

Créez votre projet (vous pouvez utiliser vos propres groupId, artifactId et packageName) en choisissant comme archetype maven-archetype-webapp :

$ mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp \
                         -DgroupId=org.inria.ns.tp \
                         -DartifactId=tp-web-rest \
                         -DpackageName=org.inria.ns.tp.rest \
                         -Dversion=1.0-SNAPSHOT
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom <<<
[INFO] 
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/archetypes/maven-archetype-webapp/1.0/maven-archetype-webapp-1.0.jar
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/archetypes/maven-archetype-webapp/1.0/maven-archetype-webapp-1.0.jar (4 KB at 33.2 KB/sec)
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/archetypes/maven-archetype-webapp/1.0/maven-archetype-webapp-1.0.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/archetypes/maven-archetype-webapp/1.0/maven-archetype-webapp-1.0.pom (533 B at 9.1 KB/sec)
[INFO] Using property: groupId = org.inria.ns.tp
[INFO] Using property: artifactId = tp-web-rest
[INFO] Using property: version = 1.0-SNAPSHOT
[INFO] Using property: package = org.inria.ns.tp.rest
Confirm properties configuration:
groupId: org.inria.ns.tp
artifactId: tp-web-rest
version: 1.0-SNAPSHOT
package: org.inria.ns.tp.rest
 Y: : y
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Old (1.x) Archetype: maven-archetype-webapp:1.0
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.inria.ns.tp
[INFO] Parameter: packageName, Value: org.inria.ns.tp.rest
[INFO] Parameter: package, Value: org.inria.ns.tp.rest
[INFO] Parameter: artifactId, Value: tp-web-rest
[INFO] Parameter: basedir, Value: /Users/myhomedir/workspace
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] project created from Old (1.x) Archetype in dir: /Users/myhomedir/workspace/tp-web-rest
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.185s
[INFO] Finished at: Tue Oct 15 14:51:46 CEST 2013
[INFO] Final Memory: 16M/125M
[INFO] ------------------------------------------------------------------------

Vérifiez que vous obtenez l'arborescence suivante, typique d'une application Web :

Info Commandes Maven

Désormais, toutes les commandes Maven seront lancées depuis le répertoire racine du projet, celui contenant le POM. Maven utilisera systématiquement ce POM, donc votre projet.

Consultez le contenu des 3 fichiers obtenus.

Editez le pom.xml et insérez le plugin Jetty (Jetty est un serveur Web similaire à Tomcat) dans la balise <build> :

    <plugins>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
      </plugin>      
    </plugins>

Lancez le serveur Web :

$ mvn jetty:run
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for org.inria.ns.tp:tp-web-rest:war:1.0-SNAPSHOT
[WARNING] 'build.plugins.plugin.version' for org.mortbay.jetty:maven-jetty-plugin is missing. @ line 21, column 15
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building tp-web-rest Maven Webapp 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-jetty-plugin:6.1.26:run (default-cli) @ tp-web-rest >>>
[INFO] 
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ tp-web-rest ---
[debug] execute contextualize
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ tp-web-rest ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-resources-plugin:2.5:testResources (default-testResources) @ tp-web-rest ---
[debug] execute contextualize
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /Users/myhomedir/workspace/tp-web-rest/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ tp-web-rest ---
[INFO] No sources to compile
[INFO] 
[INFO] <<< maven-jetty-plugin:6.1.26:run (default-cli) @ tp-web-rest <<<
[INFO] 
[INFO] --- maven-jetty-plugin:6.1.26:run (default-cli) @ tp-web-rest ---
[INFO] Configuring Jetty for project: tp-web-rest Maven Webapp
[INFO] Webapp source directory = /Users/myhomedir/workspace/tp-web-rest/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] Classes = /Users/myhomedir/workspace/tp-web-rest/target/classes
2013-10-15 18:04:01.215:INFO::Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /tp-web-rest
[INFO] Tmp directory =  determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides =  none
[INFO] web.xml file = /Users/myhomedir/workspace/tp-web-rest/src/main/webapp/WEB-INF/web.xml
[INFO] Webapp directory = /Users/myhomedir/workspace/tp-web-rest/src/main/webapp
[INFO] Starting jetty 6.1.26 ...
2013-10-15 18:04:01.323:INFO::jetty-6.1.26
2013-10-15 18:04:01.429:INFO::No Transaction manager found - if your webapp requires one, please configure one.
2013-10-15 18:04:01.676:INFO::Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server

Lancez un navigateur Web : http://localhost:8080/tp-web-rest/

D'où provient l'affichage obtenu ?

Le fichier web.xml indique quel fichier est utilisé par défaut :
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

Info Arrêter Jetty

Pour arrêter Jetty, utilisez simplement CTRL+C.

Regardez les traces d'exécution dans la console

  1. Dans quel répertoire Jetty va-t-il chercher les classes ?
  2. Dans quel répertoire standard les classes propres à une application Web devraient se trouver ?
  1. Jetty indique les classes à utiliser dans la ligne de log : [INFO] Classes = /Users/myhomedir/workspace/tp-web-rest/target/classes
  2. Les répertoires standards dans lesquels une application Web peut trouver ses propres classes sont WEB-INF/classes et WEB-INF/lib pour les librairies.

Nous apprenons que Maven est susceptible de compiler ses classes dans le répertoire target, mais que Jetty utilise src/main/webapp (vérifiez dans les traces d'exécution).

Indiquez à Maven de compiler vers une arborescence conforme à une application Web, en ajoutant après la section <plugins> la directive suivante :

    <!-- target/tp-web-rest/WEB-INF/classes -->
    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</outputDirectory>

Info Variables du projet

Notez comment certaines variables du projet peuvent s'utiliser dans le POM.

Relancez Jetty. Qu'indiquent les logs ?

Le path est conforme à nos attentes : [INFO] Classes = /Users/myhomedir/workspace/tp-web-rest/target/tp-web-rest/WEB-INF/classes

Examinez l'arborescence du répertoire target. Que constatez-vous ?

  1. Il y a deux répertoires classes : target/classes et target/tp-web-rest/WEB-INF/classes
  2. Le descripteur de déploiement est manquant : target/tp-web-rest/WEB-INF/web.xml
  1. Le premier problème vient du fait que la place n'est pas nette lorsqu'on applique les changements faits dans le POM. Ces changements produisent de nouveaux résultats, mais les anciens résultats sont encore là.

    Tapez mvn clean et examinez à nouveau l'arborescence

  2. Le second est que la tâche de construction d'une application Web n'a pas été lancée.

    Tapez mvn war:war et vérifiez le résultat.

Info Commandes Maven

Plusieurs commandes Maven peuvent être soumises en une seule fois : mvn clean war:war.

Cyce de vie de Maven

Il y a une différence entre mvn clean et mvn war:war. La première forme n'invoke pas le goal d'un plugin (ce qui est le cas de la deuxième forme), mais une phase du cycle de vie de Maven.

Le cycle de vie de l'élaboration d'un projet est une suite ordonnée de phases aboutissant à sa construction. Plusieurs cycles de vie peuvent être considérés, mais le plus commun (le cycle de vie par défaut) commence par une phase de validation de l'intégrité du projet et se termine par le déploiement du projet. Les phases sont laissées vagues intentionnellement (validation, test, déploiement, etc) de sorte à pouvoir leur faire endosser un sens particulier selon le type de projet. Par exemple, pour un projet qui produit une archive Java, la phase package va construire un Jar ; pour un projet qui produit une application Web, elle produira un fichier War.

Regardez dans le POM, quel est le <packaging> du projet ?

<packaging>war</packaging>

Ainsi, en soumettant la commande mvn package, Maven identifie grâce au type de projet que la phase "package" appartient au cycle de vie par défaut, et applique une à une les phases de ce cycle jusqu'à la phase "package" ; chaque phase est associée par défaut à une ou plusieurs tâches (goal d'un plugin).

En consultant les tableaux ci-contre, identifiez les tâches exécutées par la commande mvn package

  1. resources:resources
  2. compiler:compile
  3. resources:testResources
  4. compiler:testCompile
  5. surefire:test
  6. war:war

Vérifiez dans les traces d'exécution en lançant la commande mvn package que les tâches ont bien été exécutées.

Phases du cycle de vie de nettoyage
Phase Goal
pre-clean
clean clean:clean
post-clean

Info Phase d'exécution des goals

Nous verrons qu'un plugin peut se configurer pour être associé à une phase arbitraire.

Phases du cycle de vie par défaut

(voir la référence Maven)

Phase Goal
Packaging jar Packaging war
validate
initialize
generate-sources
process-sources
generate-resources
process-resources resources:resources
compile compiler:compile
process-classes
generate-test-sources
process-test-sources resources:testResources
generate-test-resources
process-test-resources
test-compile compiler:testCompile
process-test-classes
test surefire:test
prepare-package
package jar:jar war:war
pre-integration-test
integration-test
post-integration-test
verify
install install:install
deploy deploy:deploy

Correction du POM

Dans les traces d'exécution peuvent se trouver quelques avertissements concernant l'encodage. Avant de nous occuper des librairies nécessaires au projet, complétons le POM avec quelques propriétés.

Ajoutez avant la section <dependencies>

  <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <maven.compiler.source>1.7</maven.compiler.source>
      <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

Les logs indiquent également que la version du plugin Jetty est manquante, et mentionne celle qui est utilisée.

Corrigez la version du plugin Jetty.

    <version>6.1.26</version>

Vérifiez les logs en lançant la commande mvn clean package

Il nous reste à ajouter les dépendances. Mais où trouver les librairies utiles ?

Ajout de dépendances

Allez à http://search.maven.org/ et cherchez la librairie RESTEasy. Il y en a un peu trop... par tâtonnement on peut inférer qu'une version récente est la "3.0.4.Final" de chez "org.jboss.resteasy". Faites une recherche avancée, et saisissez le groupe, la version et le packaging "jar" ; la requête devrait montrer : g:"org.jboss.resteasy" AND v:"3.0.4.Final" AND p:"jar". En vous réferrant à la documentation de RESTEasy vous devriez identifier quel artéfact utiliser pour le projet.

Insérez la dépendance RESTEasy appropriée dans le POM :

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxrs</artifactId>
        <version>3.0.4.Final</version>
    </dependency>

Dans une application Web, on utilise des classes du package javax.servlet.

Utilisez http://search.maven.org/ pour trouver l'artéfact et ajoutez-le aux dépendances du POM.

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>

Info Portée des dépendances

Notez le <scope> assigné aux dépendances : pour les tests la valeur est "test", ce qui signifie que la librairie ne servira que pour compiler et exécuter les tests mais ne fera pas partie de l'application. La valeur "provided" indique que la librairie est utile à la compilation, mais ne doit pas être packagée dans l'application ; en effet, les classes du package javax.servlet sont fournies par le conteneur de servlet (Tomcat, Jetty, ou autre).

Ecriture du programme

Créez votre classe org.inria.ns.tp.rest.ZooService.java et placez-là dans le répertoire src/main/java. Quelle commande Maven permet de compiler ?

mvn compile

Ecrivez le corps de votre classe en utilisant des annotations JAX-RS (voir JAX-RS Javadoc) afin :

  • de mapper votre classe sur le path "welcome.txt"
    Utilisez l'annotation @javax.ws.rs.Path
  • de créer une méthode qui réponde au GET HTTP
    Utilisez l'annotation @javax.ws.rs.GET
  • que cette méthode retourne une réponse HTTP dont le type MIME soit text/plain
    Utilisez l'annotation @javax.ws.rs.Produces
package org.inria.ns.tp.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/welcome.txt")
public class ZooService {

    @GET
    @Produces("text/plain")
    public String welcome() {
        return "Bienvenue au Zoo !";
    }

}

Une fois que votre code compile sans erreur, faites en un paquet.

Quelle commande Maven permet de faire un paquet ?

mvn package

Examinez l'arborescence en particulier le répertoire WEB-INF de la cible, que constatez-vous ?

On voit dans WEB-INF/lib la librairie RESTEasy mentionnée dans le POM, mais également toutes les librairies dépendantes, dont celles propres à JAXRS.

Info Entrepots Maven

Des milliers de librairies son accessibles depuis l'entrepot central de Maven, qui sont téléchargées en fonction des besoins en local dans le répertoire ~/.m2/repository qui constitue l'entrepot local.

Il est possible d'ajouter d'autres entrepots au POM.

La phase install permet d'installer les artéfacts du projet (jar ou war, doc, sources, etc) dans l'entrepot local.

Lancez la commande mvn dependency:tree et examinez la hiérarchie des dépendances.
Que constatez-vous ?

Toutes les librairies sont reliées aux trois définies dans le POM (JUnit, servlet, RESTEasy).
$ mvn dependency:tree
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building tp-web-rest Maven Webapp 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ tp-web-rest ---
[INFO] org.inria.ns.tp:tp-web-rest:war:1.0-SNAPSHOT
[INFO] +- junit:junit:jar:3.8.1:test
[INFO] +- javax.servlet:javax.servlet-api:jar:3.0.1:provided
[INFO] \- org.jboss.resteasy:resteasy-jaxrs:jar:3.0.4.Final:compile
[INFO]    +- org.jboss.resteasy:jaxrs-api:jar:3.0.4.Final:compile
[INFO]    +- org.slf4j:slf4j-simple:jar:1.5.8:runtime
[INFO]    |  \- org.slf4j:slf4j-api:jar:1.5.8:runtime
[INFO]    +- org.scannotation:scannotation:jar:1.0.3:compile
[INFO]    |  \- javassist:javassist:jar:3.12.1.GA:compile
[INFO]    +- org.jboss.spec.javax.annotation:jboss-annotations-api_1.1_spec:jar:1.0.1.Final:compile
[INFO]    +- javax.activation:activation:jar:1.1:compile
[INFO]    +- org.apache.httpcomponents:httpclient:jar:4.2.1:compile
[INFO]    |  +- org.apache.httpcomponents:httpcore:jar:4.2.1:compile
[INFO]    |  +- commons-logging:commons-logging:jar:1.1.1:compile
[INFO]    |  \- commons-codec:commons-codec:jar:1.6:compile
[INFO]    +- commons-io:commons-io:jar:2.1:compile
[INFO]    \- net.jcip:jcip-annotations:jar:1.0:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.525s
[INFO] Finished at: Sun Oct 20 09:51:02 CEST 2013
[INFO] Final Memory: 9M/151M
[INFO] ------------------------------------------------------------------------

Configuration de RESTEasy et de Jetty

Notre programme compile mais ne s'exécute pas encore... Il manque l'intervention de RESTEasy pour traiter les requêtes HTTP et les router vers notre classe ZooService.

Editez src/main/webapp/WEB-INF/web.xml et déclarez la servlet RESTEasy et les paramètres utiles en vous inspirant de la documentation RESTEasy.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">

    <!-- Auto scan REST service -->
    <context-param>
        <param-name>resteasy.scan</param-name>
        <param-value>true</param-value>
    </context-param> 
    <!-- this need same with resteasy servlet url-pattern -->
    <context-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/zoo</param-value>
    </context-param>

    <listener>
        <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
    </listener>

    <!-- Handles org.inria.ns.tp.rest.ZooService -->
    <servlet>
        <servlet-name>ResteasyServlet</servlet-name>
        <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>ResteasyServlet</servlet-name>
        <url-pattern>/zoo/*</url-pattern>
    </servlet-mapping>

    <!-- Default page to serve -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

</web-app>

Lancez Jetty. Quelle est l'URL d'accès à la classe ZooService ?

  1. Les logs indiquent dans quel path est déployée la Webapp : [INFO] Context path = /tp-web-rest
  2. web.xml spécifie sur quel path est mappé la servlet RESTEasy : <url-pattern>/zoo/*</url-pattern> ; le paramètre <param-name>resteasy.servlet.mapping.prefix</param-name> mentionne que la partie utile du path est après la valeur <param-value>/zoo</param-value>
  3. ZooService spécifie sur quel path est mappé la classe : @Path("/welcome.txt")

L'URL obtenue est http://localhost:8080/tp-web-rest/zoo/welcome.txt

Mais cette URL provoque une erreur :

1063157 [1101161875@qtp-296187438-0] ERROR org.jboss.resteasy.core.ExceptionHandler - failed to execute
javax.ws.rs.NotFoundException: Could not find resource for full path: http://localhost:8080/tp-web-rest/zoo/welcome.txt
    at org.jboss.resteasy.core.registry.ClassNode.match(ClassNode.java:73)
    at org.jboss.resteasy.core.registry.RootClassNode.match(RootClassNode.java:48)
    at org.jboss.resteasy.core.ResourceMethodRegistry.getResourceInvoker(ResourceMethodRegistry.java:444)
    at org.jboss.resteasy.core.SynchronousDispatcher.getInvoker(SynchronousDispatcher.java:234)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:171)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:220)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
    at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:401)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:766)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:450)
    at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230)
    at org.mortbay.jetty.handler.HandlerCollection.handle(HandlerCollection.java:114)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.Server.handle(Server.java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
    at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:928)
    at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:549)
    at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:212)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
    at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:410)
    at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)

Cette trace nous indique que la servlet org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher est bien sollicitée, donc le web.xml est correct. Mais RESTEasy ne trouve de mapping vers notre classe ZooServlet.
Pourtant dans web.xml le paramètre <param-name>resteasy.scan</param-name> est bien positionné à <param-value>true</param-value>

Examinez dans les logs avant la stacktrace les paramètres concernant la configuration de Jetty, que constatez-vous ?

Jetty utilise ce path pour la Webapp :
[INFO] Webapp source directory = /Users/myhomedir/workspace/tp-web-rest/src/main/webapp, donc dans la branche src.

RESTEasy va tenter d'y trouver des classes .class qui utilisent REST, mais il n'en trouvera pas ; il ne trouvera que la source ZooServlet.java dont il n'a que faire mais pas la classe compilée, puisqu'elle est dans la branche target.

Dans la documentation du plugin Jetty trouvez le paramètre qui permet de corriger ce problème, et ajoutez-le au POM.

Dans le POM, le plugin Jetty devient :

      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
        <version>6.1.26</version>
        <configuration>
          <!-- target/tp-web-rest -->
          <webAppSourceDirectory>${project.build.directory}/${project.build.finalName}</webAppSourceDirectory>
        </configuration>
      </plugin>

Relancez Jetty, vérifiez les traces. Que constatez-vous ?

  1. Le path est correct : [INFO] Webapp source directory = /Users/myhomedir/workspace/tp-web-rest/target/tp-web-rest, Jetty positionne le path de la Webapp dans la branche target
  2. RESTEasy y a cette fois-ci trouvé notre classe : 280 [main] INFO org.jboss.resteasy.plugins.server.servlet.ConfigurationBootstrap - Adding scanned resource: org.inria.ns.tp.rest.ZooService

Lancez un navigateur sur l'URL : http://localhost:8080/tp-web-rest/zoo/welcome.txt. Quel est le résultat obtenu ?

Bienvenue au Zoo !

Yeah ! Ca marche enfin !

On veut pouvoir réduire le path : http://localhost:8080/zoo/welcome.txt. Configurez Jetty dans le POM pour que l'application soit déployée à la racine.

Ajoutez à la configuration de Jetty la ligne suivante :
    <contextPath>/</contextPath>

Transformer le REST

Maintenant que nous disposons d'une Web application REST correctement Mavenisée, aggrémentons la d'une transformation XSLT.

Utilisez ce document XML, cette feuille de style XSLT et ce module XSLT, avec cette image, celle-ci et celle-là. La transformation étant susceptible de s'opérer sur le serveur, quel est le répertoire le plus approprié dans lequel déposer ces fichiers ?

  • WEB-INF pour les fichiers XSLT car ils n'ont pas à être publics, et éventuellement pour le fichier XML en fonction de la visibilité qu'on souhaite lui donner.
  • les images pourraient être mises dans un répertoire img

(dans la branche contenant les sources de la webapp bien sûr)

Ajoutez à votre classe :

  1. Un constructeur dans lequel le contexte de la servlet est injecté :
        public ZooService(@Context ServletContext ctxt)
    L'annotation @javax.ws.rs.core.Context permet de transmettre au constructeur le contexte de la servlet. Le contexte sert à résoudre le path d'une ressource relative à l'application grâce à la méthode getRealPath().
    Dans ce constructeur, compilez la feuille de style XSLT pour utilisation ultérieure.
        // la feuille de style compilée
        javax.xml.transform.Templates zooTemplate;
    
        public ZooService(@Context ServletContext ctxt) {
            String xsltPath = ctxt.getRealPath("WEB-INF/zoo.xsl");
            this.zooTemplate = // compiler la feuille de style XSLT
        }
  2. Une méthode mappée sur le path animals.html et produisant une ressource du type MIME text/html ; au lieu de retourner une String, cette méthode doit retourner un javax.ws.rs.core.StreamingOutput.
    Utilisez cette classe pour écrire le résultat de la transformation XSLT.
        // ajouter les annotations
        public StreamingOutput getAnimals(final @Context ServletContext ctxt) {
            return new StreamingOutput() {
                // transformer WEB-INF/zoo.xml avec zooTemplate
            };
        }
  3. Faites attention aux paths qui doivent être spécifiés sur les méthodes et non plus sur la classe.

package org.inria.ns.tp.rest;

import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletContext;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.StreamingOutput;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

@Path("/")
public class ZooService {

    Templates zooTemplate;

    public ZooService(@Context ServletContext ctxt) throws TransformerConfigurationException, TransformerFactoryConfigurationError {
        String xsltPath = ctxt.getRealPath("WEB-INF/zoo.xsl");
        this.zooTemplate = TransformerFactory.newInstance().newTemplates(new StreamSource(xsltPath));
    }

    @GET
    @Path("/welcome.txt")
    @Produces("text/plain")
    public String welcome() {
        return "Bienvenue au Zoo !";
    }

    @GET
    @Path("/animals.html")
    @Produces("text/html")
    public StreamingOutput getAnimals(final @Context ServletContext ctxt) {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                try {
                    String xmlPath = ctxt.getRealPath("WEB-INF/zoo.xml");
                    Source xmlSource = new StreamSource(xmlPath);
                    Result streamResult = new StreamResult(output);
                    Transformer xslt = ZooService.this.zooTemplate.newTransformer();
                    xslt.transform(xmlSource, streamResult);
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
    }

}
                

La feuille de style XSLT accepte un paramètre :

    <xsl:param name="liste-par-nom" select="true()" />

Modifiez votre classe pour que ce paramètre puisse être passé dans l'URL et transmise à la feuille de style XSLT.

Utilisez les annotations @javax.ws.rs.QueryParam et @javax.ws.rs.DefaultValue
    @GET
    @Path("/animals.html")
    @Produces("text/html")
    public StreamingOutput getAnimals(
            final @Context ServletContext ctxt,
            final @DefaultValue("true") @QueryParam("listByName") boolean listByName) {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                try {
                    String xmlPath = ctxt.getRealPath("WEB-INF/zoo.xml");
                    Source xmlSource = new StreamSource(xmlPath);
                    Result streamResult = new StreamResult(output);
                    Transformer xslt = ZooService.this.zooTemplate.newTransformer();
                    xslt.setParameter("liste-par-nom", listByName);
                    xslt.transform(xmlSource, streamResult);
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
    }

A tester sur les 3 URLs :

  • http://localhost:8080/zoo/animals.html?listByName=false
  • http://localhost:8080/zoo/animals.html?listByName=true
  • http://localhost:8080/zoo/animals.html

Paquetage du projet

La phase "package" va construire le .war du projet. Mais ce n'est pas suffisant, on pourrait également avoir besoin de distribuer les sources. Le plugin Assembly est conçu pour produire des archives, et parmi les assemblages prédéfinis l'un permet d'empaqueter les sources.

Après consultation de la documentation, ajoutez le plugin Assembly à votre POM et configurez-le pour empaqueter les sources.

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
            <descriptorRefs>
                <descriptorRef>src</descriptorRef>
            </descriptorRefs>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
            </execution>
        </executions>
      </plugin>

Info Phase d'exécution des goals

Notez que le goal assembly:single est configuré pour être associé à la phase package.

Lancez mvn package et regardez les fichier produits dans le répertoire target.

Vous y trouverez les fichiers assemblés dans différents formats :

  • tp-web-rest-src.tar.bz2
  • tp-web-rest-src.tar.gz
  • tp-web-rest-src.zip

Fichiers du projet

Voici l'application complète :

Et les fichiers complets principaux :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.inria.ns.tp</groupId>
  <artifactId>tp-web-rest</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>

  <name>Webapp REST/XSLT avec Maven</name>
  <url>http://www-sop.inria.fr/members/Philippe.Poulard/tp-web-rest.html</url>

  <developers>
    <developer>
      <id>ppoulard</id>
      <name>Philippe Poulard</name>
      <email>philippe.poulard@inria.fr</email>
      <url>http://www-sop.inria.fr/members/Philippe.Poulard</url>
      <organization>Inria</organization>
      <organizationUrl>http://www.inria.fr</organizationUrl>
      <roles>
        <role>designer</role>
        <role>developer</role>
      </roles>
    </developer>
  </developers>

  <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <maven.compiler.source>1.7</maven.compiler.source>
      <maven.compiler.target>1.7</maven.compiler.target>
      <resteasyversion>3.0.4.Final</resteasyversion>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxrs</artifactId>
        <version>${resteasyversion}</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>tp-web-rest</finalName>
    <plugins>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
        <version>6.1.26</version>
        <configuration>
          <webAppSourceDirectory>${project.build.directory}/${project.build.finalName}</webAppSourceDirectory>
          <contextPath>/</contextPath>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
            <descriptorRefs>
                <descriptorRef>src</descriptorRef>
            </descriptorRefs>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
            </execution>
        </executions>
      </plugin>
    </plugins>
    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</outputDirectory>
  </build>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">
  <context-param>
    <param-name>resteasy.scan</param-name>
    <param-value>true</param-value>
  </context-param>
  <context-param>
    <param-name>resteasy.servlet.mapping.prefix</param-name>
    <param-value>/zoo</param-value>
  </context-param>
  <listener>
    <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
  </listener>
  <servlet>
    <servlet-name>ResteasyServlet</servlet-name>
    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>ResteasyServlet</servlet-name>
    <url-pattern>/zoo/*</url-pattern>
  </servlet-mapping>
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
</web-app>
package org.inria.ns.tp.rest;

import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletContext;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.StreamingOutput;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

@Path("/")
public class ZooService {

    Templates zooTemplate;

    public ZooService(@Context ServletContext ctxt) throws TransformerConfigurationException, TransformerFactoryConfigurationError {
        String xsltPath = ctxt.getRealPath("WEB-INF/zoo.xsl");
        this.zooTemplate = TransformerFactory.newInstance().newTemplates(new StreamSource(xsltPath));
    }

    @GET
    @Path("/welcome.txt")
    @Produces("text/plain")
    public String welcome() {
        return "Bienvenue au Zoo !";
    }

    @GET
    @Path("/animals.html")
    @Produces("text/html")
    public StreamingOutput getAnimals(
            final @Context ServletContext ctxt,
            final @DefaultValue("true") @QueryParam("listByName") boolean listByName) {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                try {
                    String xmlPath = ctxt.getRealPath("WEB-INF/zoo.xml");
                    Source xmlSource = new StreamSource(xmlPath);
                    Result streamResult = new StreamResult(output);
                    Transformer xslt = ZooService.this.zooTemplate.newTransformer();
                    xslt.setParameter("liste-par-nom", listByName);
                    xslt.transform(xmlSource, streamResult);
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
    }

}

Maven est d'une richesse sans fin ; il vous reste à apprendre :

  • Comment produire des jeux de tests pour vos applications
  • Comment générer des rapports pour votre projet
  • Comment gérer des projets multi-modules
  • Comment utiliser des librairies non-Mavenisées
  • Comment créer vos propres archétypes
  • Comment définir des profils Maven
  • etc