Affichage des articles dont le libellé est Maven. Afficher tous les articles
Affichage des articles dont le libellé est Maven. Afficher tous les articles

mercredi 19 février 2014

JSP, JS, CSS hot-deployment with Maven in Eclipse WTP

Eclipse WTP use Adtaper to provide hot deploy fonctionnalities with JEE application servers or web containers. Depending on the configuration, the adapter may reload the whole .war or .ear and take several seconds when not minutes to do so.

While WTP adatpers hot deployment is a time-saver as it rely on the Eclipse incremental compiler and does not pass through all the maven build phase to package the final .war or .ear, this is not the good choice when you work with resources like .jsp, .js, .css, .html, .tag, ...

For those static resources that don't need the whole application context to be reloaded a faster way is to copy themn automatically to the application server work directory. Some benefits are :
  • save time : no .war or .ear undeploy/redeploy
  • use [F5] to reload your resource instantly
    • the session is kept
    • the context is alive (jBPM, Spring web flow, DROOLS)
  • no more OutOfMemory or PermGenSpace when redeploying...
Next lines will show you how to set it up under JonAS. This HowTo is based on JonAS 5.2.4, Sun JKD 1.7, Maven 3.1.1, Windows 7 and Eclipse Juno. This should work on other OS as long as you go with Maven 3 and might be ported to other application servers...

WTP Adapter configuration

First of all, you need to tell JonAS WTP adatper not to publish automatically. The option is available when you double-click on the application server icons in the Eclipse "Servers" view :


Choose "Never publish automatically"... You'll then need to undeploy/deploy manually (remove and publish from the Eclipse "servers" view) when you want a code change to be taken in account or setup Cargo and configure the cargo:deploy goal to publish changes for you.

JonAS configuration

By default, JonAS will create a new directory each time an EAR or WAR is deployed. What we want is JonAS to use the same directory each time it deploys the same file, so we need to set the jonas.development property to false in the jonas.properties configuration file :

#  Set to true in order to execute the JOnAS Server in development mode.
#
#  WAR archive deployment case in development mode (for single or EAR packaged WARs):
#  Each modified WAR archive will be unpacked in the working directory of the 
#  JOnAS Server in a different folder to avoid file locks. 
#  This is especially useful in a Windows environment.
jonas.development    false
JonAS will then use a directory named against Eclipse project name.

Maven settings.xml configuration

As of Maven 3.0, profiles in the POM can also be activated based on properties from active profiles from the settings.xml.
As we want this trick to be a per user configuration we are going to rely on a property defined in the user's settings.xml to activate the static resource hot deployment :
<profile>
    <id>project-static-resources-hot-deploy</id>
    <activation>
        <activeByDefault>true</activeByDefault>
        <file>
            <exists>D:\PROJECT\jonas-full-5.2.4</exists>
        </file>
    </activation>
    <properties>
        <project.static.resources.hot.deploy.path>D:\PROJECT\jonas-full-5.2.4\work\webapps\jonas\single\{directory}</project.static.resources.hot.deploy.path>
    </properties>
</profile>
The {directory} part of the property must point to the WTP adapter deploy path for the desired Eclipse project.

Project configuration

Then we just need to add a profile activated based on the property defined in the user settings.xml that will trigger the use of the maven-resources-plugin to copy selected static resources to the application deployment path under JonAS. The copy is executed each time a static resource is saved in Eclipse and rely on the Maven Integration (m2e) for Eclipse.
<profile>
    <id>static-resources-hot-deploy</id>
    <activation>
        <property>
            <name>project.static.resources.hot.deploy.path</name>
        </property>
    </activation>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.6</version>
                <executions>
                    <execution>
                        <id>static-resources-hot-deploy</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.static.resources.hot.deploy.path}</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>src/main/webapp</directory>
                                    <!-- if you use filtering in your build use true -->
                                    <filtering>false</filtering>
                                    <!-- whatever extension you want to copy -->
                                    <includes>
                                        <include>**/*.css</include>
                                        <include>**/*.jsp</include>
                                        <include>**/*.js</include>
                                        <include>**/*.tag</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

Troubleshooting

Sometimes, Eclipse ignore user settings.xml and does not activate project profiles based on settings.xml properties. As a workaround, you can :
  1. verify that Eclipse points to the correct user settings.xml in the following Eclipse menu Settings -> Maven -> Installations (you may need to restart Eclipse after changing this setting).
  2. add the profile that set the property directly in the project pom. 

samedi 9 mars 2013

Generate Android image resources from SVG with Maven

I recently wrote the AndroidGenDrawable Maven plugin for Android that allow you to generate density specific PNG drawable from SVG (scalable vector graphics) at build time for your Android projects. Here is a brief presentation.

Fragmentation

Fragmentation is an actual problem when speaking of Android development. Specially when programmers need to maintain density specific raster graphics image (bitmaps) to target all of the possible Android device screens. The very first example is the Launcher icon of an Android application that should be resized depending on the generalized screen density (see link) :

ldpi generalized density
mdpi generalized density
hdpi generalized density
xhdpi generalized density

androidgendrawable-maven-plugin

The androidgendrawable maven plugin allow you to work on a unique SVG version of your assets and to generate the density specific PNG versions required by the Android runtime at build time. The SVG artwork are automatically scaled and transcoded to PNG for you during the maven build. The plugin rely on apache batik to transcode the SVG to PNG. You just have to add the plugin declaration and its configuration to your pom.xml :
<plugin>
 <groupId>fr.avianey.modjo</groupId>
 <artifactId>androidgendrawable-maven-plugin</artifactId>
 <version>1.0-SNAPSHOT</version>
 <configuration>
  <from>${project.basedir}/svg</from>
  <to>${project.basedir}/res</to>
  <rename>
   <level>icon</level>
  </rename>
  <createMissingDirectories>true</createMissingDirectories>
  <targetedDensities>
   <density>ldpi</density>
   <density>mdpi</density>
   <density>hdpi</density>
   <density>xhdpi</density>
  </targetedDensities>
  <fallbackDensity>mdpi</fallbackDensity>
  <skipNoDpi>true</skipNoDpi>
 </configuration>
 <executions>
  <execution>
   <phase>initialize</phase>
   <goals>
    <goal>gen</goal>
   </goals>
  </execution>
 </executions>
<plugin>
Required resources are then generated during the initialize phase of the Maven build :
[INFO] ------------------------------------------------------------------------
[INFO] Building level 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- androidgendrawable-maven-plugin:1.0-SNAPSHOT:gen (default) @ level ---
[INFO] Transcoding level-xhdpi.svg to drawable
[INFO] Transcoding level-xhdpi.svg to drawable-hdpi
[INFO] Transcoding level-xhdpi.svg to drawable-ldpi
[INFO] Transcoding level-xhdpi.svg to drawable-xhdpi
You can fork the plugin on github here and read the documentation on the associated github page here.

mardi 5 février 2013

The Facebook Android API v3.0 on Maven

For those of you that might be interrested, I just created a Maven repository that hosts my Maven port of the Facebook Android API in its latest v3.0 version. You just need to reference the GitHub pages hosted repository in you Android Maven project pom.xml :
<repositories>  
  ...  
  <repository>  
    <id>The mavenized Facebook Android API</id>  
    <url>http://avianey.github.com/facebook-api-android-maven/</url>  
  </repository>  
</repositories>
...and to import the facebook-android-api as a dependency :
<dependencies>
  ...
  <dependency>
    <groupId>com.github.avianey</groupId>
    <artifactId>facebook-android-api</artifactId>
    <version>3.0.0</version>
    <type>apklib</type>
  </dependency>
</dependencies>
I picked the groupId to avoid conflict with other ports one might find on the Internet. As usual, you can fork this mavenized Facebook Android API on GitHub.

samedi 15 décembre 2012

Run background process for your Maven integration tests

The Maven Failsaif plugin makes it very simple to run integration test written with JUnit or TestNG. This example shows how to use the Maven Failsafe Plugin to run Integration Tests that properly starts and stop a Jetty server during both the pre-integration-test phase and the post-integration-test phase. This solution can be easyly derivated to start and stop any background process that is well integrated as a Maven plugin :
<plugin>
    <groupId>${plugin.groupId}</groupId>
    <artifactId>${plugin.artifactId}</artifactId>
    <version>${plugin.version}</version>
    <configuration>
        <anything>...</anything>
    </configuration>
    <executions>
        <execution>
            <id>do-it-before</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>${plugin.start.goal}</goal>
            </goals>
            <configuration>
                <something>...</something>
            </configuration>
        </execution>
        <execution>
            <id>do-it-after</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>${plugin.stop.goal}</goal>
            </goals>
            <configuration>
                <whatever>...</whatever>
            </configuration>
        </execution>
    </executions>
</plugin>
Sometimes, there is no plugin or maven integration that you can use to start and stop the background process(es) required by your intergation test case to run. You can start and stop the desired process(es) manually but let see how to do this properly with Maven.

Maven exec plugin

The Maven exec plugin provides 2 goals to help execute system and Java programs. However, the programs execution are blocking. It is thus impossible to run tests during the intergation-test phase with running programs started during the pre-integration-test phase. Hopefully, the Maven antrun plugin will help us...

Maven antrun plugin

The Maven antrun plugin provides the ability to run Ant tasks from within Maven. It can do anything you can put into an ant build.xml script. In particular, it is possible to use the Ant exec task with all its parameters. The solution to our problem is the spawn parameter of the exec task that will run the specified executable asynchronously in its own process :
<exec executable="command-to-run"
      dir="base/dir/for/the/command"
      spawn="true"> <!-- run it asynchronously and in background baby -->
    <arg value="arg"/>
    <arg value="arg"/>
    ...
    <arg value="arg"/>
</exec>
Okay but now, we want to start third parties programs with the Maven antrun plugin during the pre-integration-test phase, keeping them running during the integration-test phase and to shut them down during the post-integration-test phase. Assuming we have plateform dependent scripts to start and stop Zookeeper, Kafka or anything else... our pom.xml will look like this :
<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.6</version>
    <executions>
        <execution>
            <id>start-third-parties</id>
            <phase>pre-integration-test</phase>
            <configuration>
                <target>
                    <exec executable="${run.command}"
                          dir="${basedir}/../scripts"
                          spawn="true">
                        <arg value="${run.command.additionnal.arg}"/>
                        <arg value="${basedir}/../scripts/${zookeeper.start.script}"/>
                    </exec>
                    <exec executable="${run.command}"
                          dir="${basedir}/../scripts"
                          spawn="true">
                        <arg value="${run.command.additionnal.arg}"/>
                        <arg value="${basedir}/../scripts/${kafka.start.script}"/>
                    </exec>
                </target>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
        <execution>
            <id>stop-third-parties</id>
            <phase>post-integration-test</phase>
            <configuration>
                <target>
                    <exec executable="${run.command}"
                          dir="${basedir}/../scripts"
                          spawn="false">
                        <arg value="${run.command.additionnal.arg}"/>
                        <arg value="${basedir}/../scripts/${zookeeper.stop.script}"/>
                    </exec>
                    <exec executable="${run.command}"
                          dir="${basedir}/../scripts"
                          spawn="false">
                        <arg value="${run.command.additionnal.arg}"/>
                        <arg value="${basedir}/../scripts/${kafka.stop.script}"/>
                    </exec>
                </target>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
       </execution>
    </executions>
</plugin>
There is no need to spawn the shutdown of our third parties programs, but it's possible too...

Okay but what if I want to use both Linux and Windows ?

No problem ! Maven profiles are here to help you. You just have to define two profiles wich <activation> relies on the os family like this :
<profile>
    <id>windows-properties</id>
    <activation>
        <os>
          <family>Windows</family>
        </os>
    </activation>
    <properties>
        <run.command>cmd</run.command>
        <run.command.additionnal.arg>/c</run.command.additionnal.arg>
        <zookeeper.start.script>start-zookeeper.bat</zookeeper.start.script>
        <zookeeper.stop.script>stop-zookeeper.bat</zookeeper.stop.script>
        <kafka.start.script>start-kafka.bat</kafka.start.script>
        <kafka.stop.script>stop-kafka.bat</kafka.stop.script>
    </properties>
</profile>
<profile>
    <id>linux-properties</id>
    <activation>
        <os>
          <family>unix</family>
        </os>
    </activation>
    <properties>
        <run.command>sh</run.command>
        <run.command.additionnal.arg></run.command.additionnal.arg>
        <zookeeper.start.script>start-zookeeper.sh</zookeeper.start.script>
        <zookeeper.stop.script>stop-zookeeper.sh</zookeeper.stop.script>
        <kafka.start.script>start-kafka.sh</kafka.start.script>
        <kafka.stop.script>stop-kafka.sh</kafka.stop.script>
    </properties>
</profile>
Et voila, the start and stop scripts properties will be set correctly depending on the runtime OS.

Okay but I don't want to run whole intergation tests all the time ?

This time again, you can use a Maven profile but without <activation>. Plugins will be activated only if you specify the profile inside the maven command :
mvn clean install -P integration-tests
<profiles>
    <profile>
        <id>integration-tests</id>
        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>2.12.2</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <artifactId>maven-antrun-plugin</artifactId>
                    <version>1.6</version>
                    <executions>
                        <execution>
                            <id>start-third-parties</id>
                            <phase>pre-integration-test</phase>
                            <configuration>
                                <target>
                                    <!-- ANT stuff -->
                                </target>
                            </configuration>
                            <goals>
                                <goal>run</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>stop-third-parties</id>
                            <phase>post-integration-test</phase>
                            <configuration>
                                <target>
                                    <!-- ANT stuff -->
                                </target>
                            </configuration>
                            <goals>
                                <goal>run</goal>
                            </goals>
                       </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

About the stop scripts

Stop scripts are sometimes not easy to write... but there is always a way to kill a process ! The examples bellow are made for Kafka (replace kafka.Kafka by anything that can identify the process you started) :

on Linux

ps ax | grep -i 'kafka.Kafka' | grep -v grep | awk '{print $1}' | xargs kill -SIGTERM
You may have to pay to run it on Mac OS X indeed...

on Windows

wmic process where (commandline like "%%kafka.Kafka%%" and not name="wmic.exe") delete
You only need to double '%' if you want the command to be executed within a .bat file.

Conclusion

This solution might work for a lot of third parties appliations that you might want to run in background in order to pass integration tests on your whole application layers :
  • run a queue like Kafka or ZeroMQ that doesn't provide Maven integration yet
  • run a database like MySQL, Cassandra (Maven plugin available btw...) or HBase
  • run an independent instance of anything : Memcached, ElasticSearch or Solar for example
Tell me if you use it for something else ;-)

vendredi 10 juin 2011

Déployer un Webservice avec la JSR-181 et CXF

La JSR-181 a facilité l'exposition de méthodes JAVA par webservice. Avec les outils et frameworks actuels, il n'a jamais été aussi simple de créer et d'exposer un service SOAP en JAVA. Voyons en pratique comment s'y prendre pour deployer un service SOAP en utilisant CXF, Maven, et n'importe quel serveur d'application (Jonas, JBoss, Websphere, Glassfish) ou conteneur de Servlet (Tomcat, Jetty).

La solution présentée est indépendante du serveur d'application utilisé et pourra être optimisée en fonction du serveur JEE cible.

La méthode que nous allons exposer permet de récupérer le résultat de la multiplication des nombres qui lui sont passés en paramètres :
package com.googlecode.avianey.cxf.v1;

public class Multiplier {

    public Long multiply(Long...in) {
        long result = 1l;
        for (long mult : in) {
            result *= mult;
        }
        return Long.valueOf(result);
    }

}

Quelques annotations suffisent...

Pour exposer la méthode en utilisant la JSR-181, il suffit de trois annotations :
WebService
Cette annotation permet de déclarer la classe comme webservice et et de configurer certains attributs du WSDL (portType, service et targetNamespace).
SOAPBinding
Cette annotation permet de spécifier le type d'encodage des messages SOAP encapsulés en HTTP.
WebMethod
Cette annotation permet de rendre visible une méthode de la classe. Une opération est associée à la méthode au niveau du WSDL.
package com.googlecode.avianey.cxf.v1;

import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(
    name = "multiplier",
    serviceName="multiplier",
    targetNamespace = "http://cxf.avianey.googlecode.com/V1")
@SOAPBinding(
    style = SOAPBinding.Style.DOCUMENT,
    use = SOAPBinding.Use.LITERAL,
    parameterStyle = SOAPBinding.ParameterStyle.WRAPPED)
public class Multiplier {

    @WebMethod( operationName = "multiply" )
    public Long multiply(Long...in) {
        long result = 1l;
        for (long mult : in) {
            result *= mult;
        }
        return Long.valueOf(result);
    }

}

...ou presque

Il nous reste à configurer CXF pour que le service soit exposé à l'adresse souhaitée. CXF s'appuyant sur Spring, il faut déclarer le ContextLoaderListener Spring au niveau du web.xml de l'application, puis paramétrer la Servlet CXF sur une URL. Veillez à bien être en Servlet Specification 2.4 au minimum, sans quoi le contexte Spring ne sera pas chargé avant l'initialisation de la Servlet CXF.

    contextConfigLocation/WEB-INF/beans.xml

    
        org.springframework.web.context.ContextLoaderListener
    



    CXFServlet
    
        org.apache.cxf.transport.servlet.CXFServlet
    
    1



    CXFServlet
    /services/*
Le fichier de configuration Spring permet de déclarer la classe Multiplier en tant que bean et de l'exposer en tant que endpoint sur une adresse précise au moyen des extensions JAX-WS.


    
    
    

    

    
    

Configuration Maven

La configuration des dépendances Maven est minimaliste :

    
        org.apache.cxf
        cxf-rt-frontend-jaxws
        ${cxf.version}
    
    
        org.apache.cxf
        cxf-rt-transports-http
        ${cxf.version}
    
    
        javax
        javaee-api
        6.0
        compile
    
Toutefois, CXF propose un plugin Maven (cxf-java2ws-plugin) permettant de générer le WSDL du service au moment de la compilation. Dans l'exemple ci dessous, le WSDL du service sera créé dans le répertoire :
  • /src/main/webapp/WEB-INF/wsdl/

    org.apache.cxf
    cxf-java2ws-plugin
    ${cxf.version}
    
        
            org.apache.cxf
            cxf-rt-frontend-jaxws
            ${cxf.version}
        
        
            org.apache.cxf
            cxf-rt-frontend-simple
            ${cxf.version}
        
    
    
        
            process-classes
            process-classes
            
                ${basedir}/src/main/webapp/WEB-INF/wsdl/multiplier.wsdl
                multiplier
                com.googlecode.avianey.cxf.v1.Multiplier
                true
                true
            
            
                java2ws
            
        
    

Déploiement sous Tomcat

Il ne reste plus qu'à démarrer notre serveur d'application. Le service est accessible à l'adresse {context-root}/{url-pattern}/{adresse} où :
  • context-root - Le contexte de la webapp
  • url-pattern - Le mapping de la Servlet CXF tel que défini dans le web.xml
  • adresse - L'adresse du endpoint telle que définie dans le fichier de configuration Spring/CXF

Liste des services exposés par CXF

dimanche 20 mars 2011

Tests unitaires d'un application Jersey JAX-RS + JPA

Nous allons aborder dans ce billet la problématique des tests unitaires pour une application WEB JPA en générale, et une application WEB JAX-RS utilisant Jersey en particulier. L'application WEB à tester est une application Maven et les tests seront intégrés à la phase de tests du cycle de vie Maven.

L'objectif est de parvenir à automatiser le déploiement de l'application en s'appuyant sur une base HSQLDB in memory et un fichier de configuration persistence.xml spécifiques à la phase de tests. Il sera alors possible de construire une campagne de tests unitaires via l'utilisation de la librairie Jersey Client API.

Application à tester

Nous utiliserons Hibernate 3.6, MySQL et Jersey 1.4 :

    
        org.hibernate
        hibernate-entitymanager
        3.6.0.Final
    
    
        mysql
        mysql-connector-java
        5.1.6
    
    
        com.sun.jersey
        jersey-bundle
        1.4
    
    
        javax.servlet
        servlet-api
        2.5
        provided
    
L'application à tester consiste en un datastore REST permettant de stocker des couples (clé, valeur) en s'appuyant sur l'entitée JPA suivante :
package com.googlecode.avianey.jersey;

import javax.persistence.Entity;
import javax.persistence.Id;

/**
 * Simple bean for storing (key, value) pairs
 */
@Entity
public class JerseyJPABean {

    @Id
    private String key;
    private String value;
    
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }

}
La persistance des données est réalisée par une base de données relationnelle MySQL. Le fichier persistence.xml de paramétrage JPA de "production" est le suivant :
<persistence
    version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>com.googlecode.avianey.jersey.JerseyJPABean</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/prod" />
            <property name="hibernate.connection.username" value="mysql" />
            <property name="hibernate.connection.password" value="mysql" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="false" />
        </properties>
    </persistence-unit>
    
</persistence>
Les services REST sont exposés par Jersey à l'adresse "/" :
<?xml version="1.0" encoding="UTF-8"?>
<web-app 
    version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    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_2_5.xsd">

    <!-- SERVICES REST -->
    <servlet>
        <servlet-name>Jersey</servlet-name>
        <servlet-class>
            com.sun.jersey.spi.container.servlet.ServletContainer
        </servlet-class>
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>com.googlecode.avianey.jersey</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>Jersey</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>

</web-app>
L'implémentation des services est un CRUD permettant de :
Stocker une valeur (PUT) :
Stocke une valeur à l'adresse correspondant à l'URL de la méthode HTTP PUT utilisée
Mettre à jour une valeur (POST) :
Met à jour la valeur stockée à l'adresse correspondant à l'URL de la méthode HTTP POST utilisée
Récupérer une valeur (GET) :
Récupère la valeur stockée à l'adresse correspondant à l'URL de la méthode HTTP GET utilisée
Supprimer une valeur (DELETE) :
Supprime la valeur à l'adresse correspondant à l'URL de la méthode HTTP DELETE utilisée
Les codes retours HTTP suivants sont utilisées afin de gérer les différentes erreurs et conflits rencontrés lors du traitement d'une requête :
200
Le traitement de la requête s'est correctement déroulé
400
La requête est refusée par le serveur : tentative de stockage d'une valeur indéfinie.
404
La donnée n'existe pas : pas de valeur stockée pour la clé demandée.
409
Conflit de données : création d'une valeur pour une clé déjà existante.
500
Erreur technique du serveur.
package com.googlecode.avianey.jersey;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityNotFoundException;
import javax.persistence.Persistence;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;

@Path("/")
@Produces("application/json")
@Consumes("application/json")
public class JerseyJPAService {

    private static final EntityManagerFactory EMF =
            Persistence.createEntityManagerFactory("pu");

    /**
     * Add a new element
     * @param value
     */
    @PUT
    @Path("/{key: \\w+}")
    public void put(@PathParam("key") String key, String value) {
        if (value == null) {
            // HTTP 400 : Bad Request
            throw new WebApplicationException(400);
        }
        JerseyJPABean bean = new JerseyJPABean();
        bean.setKey(key);
        bean.setValue(value);
        EntityManager em = EMF.createEntityManager();
        try {
            em.getTransaction().begin();
            em.persist(bean);
            em.getTransaction().commit();
        } catch (PersistenceException e) {
            if (get(key) != null) {
                // HTTP 409 : Conflict
                throw new WebApplicationException(409);
            } else {
                // HTTP 500 : Internal Server Error
                throw new WebApplicationException(500);
            }
        } finally {
            if (em.getTransaction().isActive()) {
                try {
                    em.getTransaction().rollback();
                } catch (Exception e) {}
            }
            em.close();
        }
    }

    /**
     * Update an existing element
     * @param bean
     */
    @POST
    @Path("/{key: \\w+}")
    public void update(@PathParam("key") String key, String value) {
        if (value == null || value.trim().length() == 0) {
            // Delete existing stored value
            delete(key);
        } else {
            EntityManager em = EMF.createEntityManager();
            try {
                em.getTransaction().begin();
                JerseyJPABean bean = (JerseyJPABean) em.getReference(JerseyJPABean.class, key);
                bean.setValue(value);
                em.merge(bean);
                em.getTransaction().commit();
            } catch (EntityNotFoundException e) {
                // HTTP 404 : Not Found
                throw new WebApplicationException(404);
            } catch (PersistenceException e) {
                // HTTP 500 : Internal Server Error
                throw new WebApplicationException(500);
            } finally {
                if (em.getTransaction().isActive()) {
                    try {
                        em.getTransaction().rollback();
                    } catch (Exception e) {}
                }
                em.close();
            }
        }
    }
    
    /**
     * Retrieve a persisted element
     * @param key
     * @return
     */
    @GET
    @Path("/{key: \\w+}")
    public String get(@PathParam("key") String key) {
        EntityManager em = EMF.createEntityManager();
        try {
            JerseyJPABean bean = (JerseyJPABean) em.getReference(JerseyJPABean.class, key);
            return bean.getValue();
        } catch (EntityNotFoundException e) {
            // HTTP 404 : Not Found
            throw new WebApplicationException(404);
        } finally {
            em.close();
        }
    }
    
    /**
     * Delete a persisted element
     * @param key
     */
    @DELETE
    @Path("/{key: \\w+}")
    public void delete(@PathParam("key") String key) {
        EntityManager em = EMF.createEntityManager();
        try {
            em.getTransaction().begin();
            Query q = em.createQuery("DELETE JerseyJPABean WHERE key = :key");
            q.setParameter("key", key);
            if (q.executeUpdate() == 0) {
                // HTTP 404 : Not Found
                throw new WebApplicationException(404);
            }
            em.getTransaction().commit();
        } catch (Exception e) {
            // HTTP 500 : Internal Server Error
            throw new WebApplicationException(500);
        } finally {
            if (em.getTransaction().isActive()) {
                try {
                    em.getTransaction().rollback();
                } catch (Exception e) {}
            }
            em.close();
        }
    }

}

Configuration des tests

L'une des problématique principale concernant les tests unitaires d'une application WEB persistant ses données en base est de pouvoir disposer d'une base de données réservée aux tests qui permette d'accueillir des jeux de test divers et variés. Dans un contexte JPA, il convient de créer un fichier de configuration persistence.xml spécifique, stocké dans le répertoire src/test/resources du projet, et utilisant une base HSQLDB in memory :
<persistence
    version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>com.googlecode.avianey.jersey.JerseyJPABean</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:mem:test" />
            <property name="hibernate.connection.username" value="sa" />
            <property name="hibernate.connection.password" value="" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
    
</persistence>
L'arborescence du projet Maven est la suivante :


Arborescence du projet Maven de test

Les librairies supplémentaires à ajouter au CLASSPATH de test contiennent notamment :
  • JUnit : pour monter les jeux de test et définir pour chaque test le résultat attendu
  • HSQLDB : pour disposer d'une base de données de test in memory
  • Jetty : pour déployer les sources à tester dans un serveur embarqué

    
        org.hibernate
        hibernate-entitymanager
        3.6.0.Final
    
    
        mysql
        mysql-connector-java
        5.1.6
    
    
        com.sun.jersey
        jersey-bundle
        1.4
    
    
        javax.servlet
        servlet-api
        2.5
        provided
    
    
        org.hsqldb
        hsqldb
        2.0.0
        test
    
    
        junit
        junit
        4.8.2
        jar
        test
    
    
        com.sun.jersey.jersey-test-framework
        jersey-test-framework-core
        1.4
        jar
        test
    
    
        org.mortbay.jetty
        jetty
        6.1.17
        jar
        test
        
            
                servlet-api
                org.mortbay.jetty
            
        
    
Lors de l'exécution de la phase de test par Maven, le CLASSPATH utilisé contient les librairies applicatives ainsi que les classes JAVA du répertoire src/main auquelles sont ajoutées les librairies de test (celles dont les scope est <test>) et les classes JAVA du répertoire src/test. Le CLASSPATH contient donc deux fichiers différents nommés tous deux META-INF/persistence.xml :
src/main/resources/META-INF/persistence.xml
Fichier de configuration JPA utilisé pour le packaging final
src/main/resources/META-INF/persistence.xml
Fichier de configuration JPA utilisé pour les tests unitaires
Pour s'assurer de l'utilisation du fichier de configuration de test lors de l'exécution de la phase de tests par Maven, il faut utiliser un ClassLoader personnalisé dont l'effet est de ne mettre en visibilité que le fichier META-INF/persistence.xml du répertoire src/test/resources :
public static class ClassLoaderProxy extends ClassLoader {

    public ClassLoaderProxy(final ClassLoader parent) {
        super(parent);
    }

    @Override
    public Enumeration<url> getResources(final String name) throws IOException {
        if (!"META-INF/persistence.xml".equals(name)) {
            return super.getResources(name);
        } else {
            System.out.println("Redirecting to specific persistence.xml");
            return new Enumeration<url>() {

                private URL persistenceUrl = (new File(
                        "src/test/resources/META-INF/persistence.xml"))
                        .toURI().toURL();

                @Override
                public boolean hasMoreElements() {
                    return persistenceUrl != null;
                }

                @Override
                public URL nextElement() {
                    final URL url = persistenceUrl;
                    System.out.println("Using custom persistence.xml : " + 
                            url.toString());
                    persistenceUrl = null;
                    return url;
                }

            };
        }
    }
}
Lors de la mise en place des tests dans la méthode setUp() du TestCase JUnit, la base HSQLDB in memory est démarrée à l'adresse indiquée dans le fichier persistence.xml de test. Une base vierge de toute donnée est ainsi disponible pour y déployer des jeux de tests et données spécifiques. Un serveur Jetty embedded est quant à lui lancé sur le port 8888 et configuré pour exposer notre application WEB sur le context "/" en utilisant le ClassLoader personnalisé définit précédemment :
@Override
protected void setUp() throws Exception {
    super.setUp();
    // Start HSQLDB
    try {
        Class.forName("org.hsqldb.jdbcDriver");
        con = DriverManager.getConnection(
                "jdbc:hsqldb:mem:test",
                "sa", "");
    } catch (Exception e) {
        e.printStackTrace();
        fail("Failed to start HSQLDB");
    }

    // Start Jetty
    try {
        System.setProperty("org.mortbay.log.class",
                "com.citizenmind.service.rest.TestRestService.CustomLogger");
        server = new Server(8888);
        WebAppContext context = new WebAppContext();
        context.setResourceBase("src/main/webapp");
        context.setContextPath("/");
        Thread.currentThread().setContextClassLoader(
                new ClassLoaderProxy(
                Thread.currentThread().getContextClassLoader()));
        context.setClassLoader(Thread.currentThread().getContextClassLoader());
        context.setParentLoaderPriority(true);
        server.setHandler(context);
        server.start();
    } catch (Exception e) {
        e.printStackTrace();
        server.stop();
        fail("Failed to start embedded Jetty");
    }
}
Lors de l'arrêt des tests dans la méthode tearDown() du TestCase JUnit, la base de données HSQLDB ainsi que le serveur Jetty embedded sont arrêtés :
@Override
protected void tearDown() throws Exception {
    super.tearDown();
    // Stop HSQLDB
    try {
        con.createStatement().execute("SHUTDOWN");
    } catch (Exception e) {
        e.printStackTrace();
    }
    // Stop jetty
    try {
        server.stop();
    } catch (Exception e) {
        e.printStackTrace();
        System.err.println("Jetty must be killed manually");
    }
}
La librairie Jersey Client API est utilisée pour dérouler les tests en interrogeant les services REST exposés par le serveur Jetty embedded :
/**
 * Test REST Services using Jersey client API
 */
public void testJerseyJPAService() {

    // Create a Jersey client
    Client c = Client.create();
    WebResource r;
    
    // Insert two values :
    // - (A, 1)
    // - (B, 2)
    try {
        r = c.resource(URL + "/A");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .put("1");
        r = c.resource(URL + "/B");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .put("2");
    } catch (UniformInterfaceException e) {
        e.printStackTrace();
        fail("Could not insert values");
    }

    // Store a value with an existing key
    try {
        r = c.resource(URL + "/B");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .put("2");
    } catch (UniformInterfaceException e) {
        // HTTP 409 : Conflict
        assertTrue(e.getResponse()
                .getClientResponseStatus()
                .getStatusCode() == 409);
    }
    
    // Verify values
    r = c.resource(URL + "/A");
    String A = r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .get(String.class);
    assertTrue("1".equals(A));
    r = c.resource(URL + "/B");
    String B = r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .get(String.class);
    assertTrue("2".equals(B));
    
    // Update B
    r = c.resource(URL + "/B");
    r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .post("3");
    B = r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .get(String.class);
    assertTrue("3".equals(B));
    
    // Update C
    try {
        r = c.resource(URL + "/C");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .post("3");
    } catch (UniformInterfaceException e) {
        // HTTP 404 : Resource C does not exists
        assertTrue(e.getResponse()
                .getClientResponseStatus()
                .getStatusCode() == 404);
    }
    
    // Delete A
    r = c.resource(URL + "/A");
    r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .delete();
    try {
        Statement st = con.createStatement();
        ResultSet rs = st.executeQuery(
                "Select * From JerseyJPABean Where key = 'A'");
        assertTrue(!rs.next());
        rs.close();
        st.close();
    } catch (SQLException e) {
        e.printStackTrace();
        fail("Could not execute SQL Select");
    }
    
    // Get A
    try {
        A = r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .get(String.class);
    } catch (UniformInterfaceException e) {
        // HTTP 404 : Resource A does not exists enymore
        assertTrue(e.getResponse()
                .getClientResponseStatus()
                .getStatusCode() == 404);
    }
}

Résultat final

Le projet complet Maven utilisé pour illustrer ce billet est disponible sous licence Apache License 2.0 à cette adresse :

Un petit test :

mvn test

Et l'on remarque le déploiement des services sous Jetty, la redirection du ClassLoader personnalisé ainsi que le détail des requêtes Hibernate :

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.googlecode.avianey.jersey.JerseyJPATest
2011-03-20 17:10:18.427::INFO: Logging to STDERR via org.mortbay.log.StdErrLog
2011-03-20 17:10:18.463::INFO: jetty-6.1.17
2011-03-20 17:10:18.531::INFO: NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
20 mars 2011 17:10:18 com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Scanning for root resource and provider classes in the packages:
com.googlecode.avianey.jersey
20 mars 2011 17:10:18 com.sun.jersey.api.core.ScanningResourceConfig logClasses
INFO: Root resource classes found:
class com.googlecode.avianey.jersey.JerseyJPAService
20 mars 2011 17:10:18 com.sun.jersey.api.core.ScanningResourceConfig init
INFO: No provider classes found.
20 mars 2011 17:10:18 com.sun.jersey.server.impl.application.WebApplicationImpl _initiate
INFO: Initiating Jersey application, version 'Jersey: 1.4 09/11/2010 10:41 PM'
2011-03-20 17:10:19.323::INFO: Started SocketConnector@0.0.0.0:8888
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Redirecting to specific persistence.xml
Using custom persistence.xml : file:/C:/projets/jersey-jpa-test/src/test/resources/META-INF/persistence.xml

Hibernate: insert into JerseyJPABean (value, key) values (?, ?)
Hibernate: insert into JerseyJPABean (value, key) values (?, ?)
Hibernate: insert into JerseyJPABean (value, key) values (?, ?)
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: update JerseyJPABean set value=? where key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: delete from JerseyJPABean where key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.343 sec
Fork me on GitHub