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

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 ;-)

dimanche 23 septembre 2012

Mahout Taste walkthrough (initiation par l'exemple)

Ce post à pour objectif de défricher le fonctionnement du moteur de recommandation d'Apache Mahout Taste en étudiant les projets d’exemples fournis avec la version 0.7 d'Apache Mahout (L'API est susceptible d'évoluer en profondeur tant que la MileStone 1 n'est pas atteinte).
<dependency>
    <groupId>org.apache.mahout</groupId>
    <artifactId>mahout-core</artifactId>
    <version>0.7</version>
</dependency>

<dependency>
    <groupId>org.apache.mahout</groupId>
    <artifactId>mahout-examples</artifactId>
    <version>0.7</version>
</dependency>

Concepts

Mahout Taste permet de fournir des recommandation d'Items à des User. Users et Items étant tous deux représentés par des entiers. Afin de fournir des recommandations, le framework s'appuie sur un DataModel constitué de préférences. Une préférence est un nombre flottant représentant une affinité entre un User et un Item.


Le moteur de recommandation repose sur l'interface Recommender. Les implémentations de cette interface permettent de :
  • Récupérer une liste d'Items recommandées pour un User
  • D'estimer l'affinité entre un User et un Item
  • De modifier des préférences à la volée

Un exemple simple : ItemAverageRecommender

ItemAverageRecommender est une implémentation de l'interface Recommender qui estime l'affinité potentielle entre un User et un Item comme la moyenne des affinités des User ayant exprimés une préférence pour pour cet Item. Elle permet ainsi d'obtenir des recommandations indiférenciées par User.

Le BookCrossingRecommender

Un peu plus intéressant, le BookCrossingRecommender est un exemple concret de Recommender qui s'appuie sur un jeu de données réel :
public final class BookCrossingRecommender implements Recommender {

    private final Recommender recommender;

    public BookCrossingRecommender(DataModel bcModel) throws TasteException {
        UserSimilarity similarity = new CachingUserSimilarity(
                new EuclideanDistanceSimilarity(bcModel), bcModel);
        UserNeighborhood neighborhood = new NearestNUserNeighborhood(10, 0.2, similarity, bcModel, 0.2);
        recommender = new GenericUserBasedRecommender(bcModel, neighborhood, similarity);
    }
}
En regardant de plus près le code de ce BookCrossingRecommender on remarque la présence des concepts suivants :
UserSimilarity
qui représente la ressemblance entre deux User.
UserNeighborhood
qui représente le voisinage d'un User; les voisins étant déterminés en fonction de leurs ressemblance.
Des implémentations diverses de ces deux concepts sont disponibles par défaut dans Mahout Taste. Le BookCrossingRecommender utilise la classe EuclideanDistanceSimilarity comme implémentation de la ressemblance. Celle-ci permet de classer les Users selon la distance euclidienne entre leurs affinités communes (chaque Item pour lequel ils ont chacun exprimé une préférence étant une dimension).

Une fois la notion de ressemblance posée, il est alors possible de définir le voisinage d'un User. Dans le cas du BookCrossingRecommender, le voisinage d'un User est constitué des 10 Users qui lui sont le plus ressemblant. La classe NearestNUserNeighborhood permet de fixer un seuil minimal en dessous duquel la ressemblance entre deux Users leur interdit d'être voisins.

Enfin, le GenericUserBasedRecommender est utilisé afin de proposer des recommandations à un User en considérant les Items ayant l'affinité moyenne la plus grande parmi son voisinage...

Comment choisir (construire) une bonne implémentation

Si Taste propose une API simple permettant de monter rapidement un système de recommandation sur un jeux de données quasi quelconque, comment savoir si la notion de ressemblance retenue est la bonne, si le voisinage n'a pas été choisi trop vaste ou trop restreint et si la méthode de sélection des recommandations est pertinente ?

Quelques requêtes sur Google permettent de se rendre compte que de nombreuses recherches plus ou moins compréhensibles (plutôt moins que plus) ont été menées sur le sujet.

Pour le commun des mortels, Mahout Taste est livré avec un RecommenderEvaluator sui, comme son nom l'indique, permet d'estimer la qualité d'un Recommender. A partir d'un DataModel, le RecommenderEvaluator va construire le Recommender à évaluer en utilisant une partie du jeu de données : le "training percentage". Il va ensuite évaluer le Recommender sur un sous ensemble du jeu de données appelé "evaluation percentage" et retourner une note. Plus la note retournée est basse, plus le Recommender a proposé des recommandations proches des préférences réelles de l'"evaluation percentage". Une note de 0 indiquant des résultats identiques entre les recommandations du Recommender et l'"evaluation percentage".

dimanche 29 janvier 2012

Jersey custom parameter, annotation and exception mapping

Jersey is the JSR-311 reference implementation for JAX-RS (Java API for RESTful Web Services). One of the drawback of this API is its lack of documentation when you want to go deeper into some complex or recurrent issues. Because we all like KISS code, let see how to keep Jersey simple, stupid!

Let's take the following example where we want to :
  1. Map Java Exception to specific HTTP response with localized messages
  2. Inject parameters of any type with custom validation
  3. Define a specialized annotation for specific injectable parameters
A first shot with no optimization will look like this :
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    private static final ResourceBundle resource = 
        ResourceBundle.getBundle("com.blogspot.avianey");
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") String dateStr,
            @Context HttpServletRequest request) {

        // verifying that the user is logged in
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
        
        // verifying the format of the sent date
        Date date = null;
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException pe) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
        
        try {
            // calling the business logic
            return service.testOperation(id, date, user);
        } catch (BusinessException boe) {
            log.log(Level.FINER, "Business problem while executing testMethod", boe);
            throw new WebApplicationException(
                    Response.status(Status.BAD_REQUEST)
                            .type(MediaType.TEXT_PLAIN)
                            .entity(resource.getString(boe.getMessage()))
                            .build());
        } catch (TechnicalException te) {
            log.log(Level.FINER, "Technical problem while executing testMethod", te);
            throw new WebApplicationException(
                    Response.status(Status.INTERNAL_SERVER_ERROR)
                            .type(MediaType.TEXT_PLAIN)
                            .entity(resource.getString("error.internal"))
                            .build());
        }
        
    }
    
}
It's easy to think about the number of duplicated lines of code we would have if such a solution is applied for all our exposed methods... Now it's time to go deeper into Jersey and JAX-RS.

Custom Exception mapping with Jersey

In a Three-Tier architecture, the Logic-Tier may throw an application specific BusinessException, the Data-Tier may throw a TechnicalException, and so on... As BusinessException are relative to business rule violations it might be interresting to alert the user with a comprehensive message in the Presentation-Tier. On the contrary, a synthetic message will be displayed to the user for TechnicalException that refers to problems that are not likely to happen.

Jersey makes it possible to bind Java Exception with specialized HTTP response. All we have to do is to register an ExceptionMapper for each Java Exception we want to handle in a generic manner.
@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {

    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    private static final ResourceBundle resource = 
        ResourceBundle.getBundle("com.blogspot.avianey");

    @Override
    public Response toResponse(BusinessException e) {
        log.log(Level.FINER, "Business problem while executing testMethod", e);
        return Response.status(Status.BAD_REQUEST)
            .type(MediaType.TEXT_PLAIN)
            .entity(resource.getString(e.getMessage()))
            .build();
    }

}
@Provider
public class TechnicalExceptionMapper implements ExceptionMapper<TechnicalException> {

    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    private static final ResourceBundle resource = 
        ResourceBundle.getBundle("com.blogspot.avianey");

    @Override
    public Response toResponse(TechnicalException e) {
        log.log(Level.FINER, "Technical problem while executing testMethod", e);
        return Response.status(Status.INTERNAL_SERVER_ERROR)
            .type(MediaType.TEXT_PLAIN)
            .entity(resource.getString("error.internal"))
            .build();
    }

}
Just as service classes, @Provider classes should be placed in a package that is scanned by the Jersey Servlet at startup so they can be used at runtime.
<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>pkg.provider; pkg.endpoint</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
We no longer have to handle explicitly BusinessException and TechnicalException in our exposed method :
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") String dateStr,
            @Context HttpServletRequest request) {

        // verifying that the user is logged in
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
        
        // verifying the format of the sent date
        Date date = null;
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException pe) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
        
        // calling the business logic
        // no need to catch exception here anymore
        return service.testOperation(id, date, user);
        
    }
    
}

Custom Jersey parameters

JAX-RS Param annotations like QueryParam, FormParam and PathParam can be apply to any Java Object that have a constructor with a single String argument. When calling a method with such an annotated parameter, Jersey instantiate a new object with the param value and pass it to the method.

We can easily use this feature to centralize the validation of date parameters accross all of our exposed methods :
/**
 * A DateParam to validate the format of date parameters received by Jersey
 */
public class DateParam {

    private Date date;
    
    public DateParam(String dateStr) {
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        try {
            this.date = sdf.parse(dateStr);
        } catch (ParseException pe) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
    }
    
    public Date value() {
        return this.date;
    }
    
}
Once again, our original code gained in maintainability :
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") DateParam date,
            @Context HttpServletRequest request) {

        // verifying that the user is logged in
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
        
        // calling the business logic
        return service.testOperation(id, date.value(), user);
        
    }
    
}

Contextual object injection

Now we will see how it possible to directly inject the logged User into our exposed method. There is two diferent approaches doing this :
  1. Use the @Context annotation with a custom provider to inject the desired Object
  2. Create a custom annotation and its Injectable and associated InjectableProvider
In the first approach, we define a new Injectable implementation for the User Type and associate it with the @Context annotation.
@Provider
public class LoggedUserProvider extends AbstractHttpContextInjectable<User>
        implements InjectableProvider<Context, Type> {

    private final HttpServletRequest r;
    
    public LoggedUserProvider(@Context HttpServletRequest r) {
        this.r = r;
    }

    /**
     * From interface InjectableProvider
     */
    @Override
    public Injectable<user> getInjectable(ComponentContext ic, Context a, Type c) {
        if (c.equals(User.class)) {
            return this;
        }
        return null;
    }

    /**
     * From interface InjectableProvider
     * A new Injectable is instanciated per request
     */
    @Override
    public ComponentScope getScope() {
        return ComponentScope.PerRequest;
    }

    /**
     * From interface Injectable
     * Get the logged User associated with the request
     * Or throw an Unauthorized HTTP status code
     */
    @Override
    public User getValue(HttpContext c) {
        final User user = Contestr.getSessionUser(r);
        if (user == null) {
            throw new WebApplicationException(Response.Status.UNAUTHORIZED);
        }
        return user;
    }
}
The exposed method consist now of one line of code only !
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") DateParam date,
            @Context User user) {
        // calling the business logic
        return service.testOperation(id, date.value(), user);
    }
    
}
In the second approach, we need to define a new annotation :
@Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggedUser {}
Just as seen before, we need to associate the @LoggedUser with a new Injectable for the User type :
@Provider
public class LoggedUserProvider  
    implements Injectable<User>, InjectableProvider<LoggedUser, Type> {

    private final HttpServletRequest r;
    
    public LoggedUserProvider(@Context HttpServletRequest r) {
        this.r = r;
    }

    @Override
    public Injectable<user> getInjectable(ComponentContext cc, LoggedUser a, Type c) {
        if (c.equals(User.class)) {
            return this;
        }
        return null;
    }

    /**
     * From interface InjectableProvider
     * A new Injectable is instanciated per request
     */
    @Override
    public ComponentScope getScope() {
        return ComponentScope.PerRequest;
    }

    /**
     * From interface Injectable
     * Get the logged User associated with the request
     * Or throw an Unauthorized HTTP status code
     */
    @Override
    public User getValue() {
        final User user = (User) r.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Response.Status.UNAUTHORIZED);
        }
        return user;
    }
    
}
This solution is much more flexible than the previous one as we can build more than one annotation for the same Type :
  • @LoggedUser : retrieve the logged in User and throw an HTTP 401 unauthorized status code if no logged in User is associated with the current request.
  • @AdminLoggedUser : retrieve the logged in User and throw an HTTP 401 unauthorized status code if no logged in User is associated with the current request or if the User is not an administrator.
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") DateParam date,
            @LoggedUser User user) {
        // calling the business logic
        return service.testOperation(id, date.value(), user);
    }
    
}
In a next post, I will cover the integration of Jersey with Guice or any other JSR-330 compliant IOC framework.

dimanche 8 janvier 2012

Generic JPA sharded counter for Google App Engine

One of the drawback of The Google App Engine datastore is the rate at which it can handle update for a single entity or an entity group. The datastore documentation indicates that the maximum rates is arround 5 write operations per second for the same entity or entity group. To overpass this limitation, Google recommend to use horizontal partitioning by using sharded counter. The App Engine documentation provides a simple (an non transactional) JDO implementation of sharded counters. Let see how we can build an reusable and transactional JPA-based sharded counter for the Google App Engine.

The Counter Class

The Entity bellow allow us to implement sharded counters for almost anything :
count :
this attribute will handle the counter value for this sharded counter
refId :
the ID of the Entity this sharded counter is used for
entityClass :
the Entity class this sharded counter is used for
type :
what this counter is counting
@Entity
@SuppressWarnings("serial")
public class Counter implements Serializable {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long count;
    private Long refId;
    private int type;
    private String entityClass;
    
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getCount() {
        return count;
    }
    public void setCount(Long count) {
        this.count = count;
    }
    public Long getRefId() {
        return refId;
    }
    public void setRefId(Long refId) {
        this.refId = refId;
    }
    public String getEntityClass() {
        return entityClass;
    }
    public void setEntityClass(String entityClass) {
        this.entityClass = entityClass;
    }
    public int getType() {
        return type;
    }
    public void setType(int type) {
        this.type = type;
    }

}

The service and how to increment a counter

In order to increment the number of time a web page is viewed, we would like to do something like :
incrementCounter(WebPage.class, webPageId, WebPage.VIEWS)
Where webPageId is the id of the WebPage Entity and WebPage.VIEW a constant.

The incrementCounter method will work as follow :
  1. Defines a MAX_TRIES values to store the maximum number of time we will try to update an existing sharded counter
  2. Retrives the list of the sharded counter already persisted for the given type
  3. If none exists, a new sharded counter with a value of 1 is persisted for the given type, the method returns
  4. Else, one sharded counter is picked up at random and its value is incremented
  5. If the update fails, the number of remaining tries is decremented
  6. If there is no try left, a new sharded counter with a value of 1 is persisted for the given type, the method returns
  7. Else, start again at step 2
/**
 * Sum all the counter for the given type and entity
 * @param c
 * @param refId
 * @param type
 * @return
 */
protected long counterSum(Class<?> c, Long refId, int type) {
    long sum = 0;
    List<Counter> counters = getCounters(c, refId, type);
    for (Counter counter : counters) {
        sum += counter.getCount();
    }
    return sum;
}

/**
 * Get all the counter for the given type and entity
 * @param c
 * @param refId
 * @param type
 * @return
 */
private List<Counter> getCounters(Class<?> c, Long refId, int type) {
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("refId", refId);
    params.put("type", type);
    params.put("entityClass", c.getSimpleName());
    return list(Counter.class, 
        "SELECT c FROM Counter c WHERE refId = :refId AND type = :type AND entityClass = :entityClass", 
        params);
}

protected void incrementCounter(Class<?> c, Long refId, int type) {
    modifyCounter(c, refId, type, 1);
}

protected void decrementCounter(Class<?> c, Long refId, int type) {
    modifyCounter(c, refId, type, -1);
}

/**
 * Modify the counter value for the given type and entity
 * @param c
 * @param refId
 * @param type
 * @param step
 */
protected void modifyCounter(Class<?> c, Long refId, int type, int step) {
    int tries = MAX_TRIES;
    EntityManager em = getEntityManager();
    while (true) {
        try {
            List<Counter> counters = getCounters(c, refId, type);
            if (counters.size() == 0) {
                newCounter(c, refId, type, step);
                break;
            }
            try {
                em.getTransaction().begin();
                Random generator = new Random();
                int counterNum = generator.nextInt(counters.size());
                Counter counter = counters.get(counterNum);
                counter.setCount(counter.getCount() + step);
                em.merge(counter);
                em.getTransaction().commit();
                break;
            } finally {
                if (em != null) {
                    if (em.getTransaction().isActive()) {
                        em.getTransaction().rollback();
                    }
                }
            }
        } catch (ConcurrentModificationException cme) {
            if (--tries == 0) {
                newCounter(c, refId, type, step);
                break;
            }
        }
    }
}

private void newCounter(Class<?> c, Long refId, int type, int step) {
    EntityManager em = null;
    try {
        em = getEntityManager();
        Counter counter = new Counter();
        counter.setCount(Long.valueOf(step));
        counter.setEntityClass(c.getSimpleName());
        counter.setRefId(refId);
        counter.setType(type);
        em.getTransaction().begin();
        em.persist(counter);
        em.getTransaction().commit();
    } finally {
        if (em != null) {
            if (em.getTransaction().isActive()) {
                em.getTransaction().rollback();
            }
        }
    }
}


protected final <R> List<R> list(Class<R> c, String query, Map<String, Object> parameters) {
    EntityManager em = getEntityManager();
    Query select = em.createQuery(query);
    for (String key : parameters.keySet()) {
        select.setParameter(key, parameters.get(key));
    }
    List<R> list = select.getResultList();
    return list;
}
Enjoy!

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

dimanche 27 février 2011

jRename : renommer des fichiers par lot

Cela fait quelques années que j'avais développé un outil bien pratique pour renommer des fichiers en série. Après avoir retrouvé les sources de cette application JAVA sur un disque de sauvegarde, je le propose aujourd'hui en licence open source GPL v3.

jRename (puisque c'est ainsi que je l'avais appelé à l'époque) est un utilitaire java gratuit et open source permettant de renommer des fichiers par lot. Il est multi-plateformes : Windows XP/Vista/7, Mac OS X et Linux et propose un grand nombre d'options : changement de case des caractères, insertion de dates, utilisation d'un compteur mais également filtrage et capture à base d'expressions régulières :




Parmi les fonctionnalités implémentées :
  • Utilisation d'un compteur
  • Changement de casse
  • Modification de caractères
  • Suppression de blancs superflus
  • Suppression de '.' superflus
  • Utilisation d'expressions régulières avec groupes capturants
  • Prévisualisation du résultat
  • Undo
  • Travail dans les répertoires et les sous répertoires

Ce qu'il reste à faire :
  • Utilisation de swing worker
  • Utilisation des données EXIF
  • Utilisation des données ID3 tag
  • Préciser l'ordre de traitement des fichiers
  • etc...

La notice d'utilisation en français se trouve à cette adresse : notice d'utilisation jrename.

D'un point de vue technique, jRename utilise le gestionnaire de Layout MiG Layout que je vous recommande pour sa simplicité et ses possibilités avancées de configuration. Il est disponible pour SWING, SWT et JavaFX.

Si vous avez des idées pour améliorer cette application, n'hésitez pas à m'en faire part.

mercredi 20 octobre 2010

Android JAX-RS Client : partie cliente avec Gson

Dans un post précédent, nous avons vu comment il était possible d'exposer des services JAX-RS JSON grâce au Google appEngine et aux API Java Jersey et Jackson. Nous allons cette fois-ci aborder la partie cliente de ces services sous Android dont voici une capture d'écran :

Téléchargement et installation des outils

Pour reproduire ce tutorial, vous aurez besoin des outils et API suivants :
  • Eclipse : le célèbre IDE
  • SDK Android : pour développer des applications Android avec Eclipse
  • Gson : API de sérialisation / désérialisation made in Google
Une fois téléchargés et installés, créez un nouvel Android Project dans votre workspace Eclipse :


Ajoutez un répertoire /lib à la racine du projet et importez-y la librairie gson.jar. Ajoutez également la dépendance au classpath du projet Eclipse.


Afin d'autoriser notre application à effectuer des appels réseaux via Internet, il faut ajouter au fichier AndroidManifest.xml la permission android.permission.INTERNET :
<?xml version="1.0" encoding="utf-8"?>
<manifest 
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.androgames.blog.sample.rest.client"
    android:versionCode="1"
    android:versionName="1.0">
      
    <application 
        android:icon="@drawable/icon" 
        android:label="@string/app_name">
    
        <activity 
            android:name=".UserConsumer"
            android:label="@string/app_name"
            android:screenOrientation="portrait">
                  
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            
        </activity>

    </application>
    
    <uses-sdk android:minSdkVersion="4" />
    
    <uses-permission android:name="android.permission.INTERNET" />

</manifest>

Mise en place de l'interface utilisateur

Notre application sera décomposée en deux parties. L'une permettra la création, la mise à jour et la suppression d'utilisateurs tandis que l'autre affichera la liste de tous les utilisateurs avec leur prénom, nom et id :


Le layout utilisé pour le rendu d'un utilisateur de la liste est définit par le fichier user.xml du répertoire res/layout :
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    android:padding="6dip">
    
    <ImageView
        android:src="@drawable/user"
        android:layout_width="48dip"
        android:layout_height="48dip"
        android:layout_marginRight="6dip" />

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="0dip"
        android:layout_weight="1"
        android:layout_height="fill_parent">

        <TextView
            android:id="@+id/name"
            android:textAppearance="?android:textAppearanceMedium"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:ellipsize="marquee"
            android:gravity="center_vertical" />

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1" >

            <TextView
                android:textAppearance="?android:textAppearanceSmall"
                android:textColor="#FFCC3333"
                android:textStyle="bold"
                android:layout_marginRight="6dip"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="ID :"
                android:ellipsize="marquee" />
            
            <TextView
                android:id="@+id/id"
                android:textAppearance="?android:textAppearanceSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="6dip"
                android:singleLine="true"
                android:ellipsize="marquee" />
            
        </LinearLayout>
            
    </LinearLayout>

</LinearLayout>
Le formulaire permettant la mise à jour ou la suppression d'un utilisateur existant, ainsi que la création de nouveaux utilisateurs se présentera sous les formes suivantes :



Les actions réalisables sont au nombre de 5 :
  1. Créer un nouvel utilisateur
  2. Enregistrer les modifications apportées au nom ou au prénom d'un utilisateur
  3. Supprimer un utilisateur
  4. Annuler l'édition en cours d'un utilisateur, le formulaire est alors remis à zéro
  5. En cliquant sur un utilisateur de la la liste, le formulaire se met à jour avec les informations de l'utilisateur sélectionné.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    
    <LinearLayout
        android:orientation="vertical"
        android:paddingLeft="6dip"
        android:paddingRight="6dip"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
        
        <TextView
            android:text="@string/forName"
            android:textAppearance="?android:textAppearanceLarge"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
        <EditText 
            android:id="@+id/forName"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
        
        <TextView
            android:text="@string/name"
            android:textAppearance="?android:textAppearanceLarge"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
        <EditText 
            android:id="@+id/name"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
        <LinearLayout
            android:orientation="horizontal"
            android:layout_gravity="right"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            
            <Button 
                android:id="@+id/insertOrUpdate"
                android:onClick="insertOrUpdate"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
            
            <Button 
                android:id="@+id/delete"
                android:text="@string/delete"
                android:onClick="delete"
                android:visibility="gone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
            
            <Button 
                android:id="@+id/initialize"
                android:text="@string/initialize"
                android:onClick="initialize"
                android:visibility="gone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
                
        </LinearLayout>
    
    </LinearLayout>
        
    <ListView 
        android:id="@+id/users"
        android:layout_height="0dip"
        android:layout_weight="1"
        android:layout_width="fill_parent" />
        
</LinearLayout>
Dans le Layout ci dessus, on remarquera en particulier que les boutons ne sont pas tous présents par défaut. Les boutons "Enregistrer", "Supprimer" et "Annuler" ne sont proposés que si un utilisateur de la liste à été sélectionné. L'attribut android:onClick des balises Button permet de spécifier le nom de la méthode appelée lorsque lorsque le bouton est pressé. Ces méthodes doivent être déclarées dans la ou les Activity qui utilisent le Layout comme content view. Elle doivent être publiques et prendre un unique paramètre de type View.

Pour le rendu de la liste des utilisateurs, nous utilisons un ListAdapter afin de faire le pont entre la ListView (couche de présentation) et notre liste d'utilisateurs (modèle de données).
package net.androgames.blog.sample.rest.client;

import java.util.List;

import android.content.Context;
import android.database.DataSetObserver;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class UserAdapter extends BaseAdapter {

    private List<User> users;
    private LayoutInflater inflater;

    public UserAdapter(final ListView list, Context context) {
        this.inflater = LayoutInflater.from(context);
        // on attache l'adapter à la ListView
        list.setAdapter(this);
        // raffraichissement de la liste lorsque
        // une donnée est modifiée
        registerDataSetObserver(new DataSetObserver() {
            public void onChanged() {
                list.invalidateViews();
            }
        });
    }

    public int getCount() {
        if (users == null) {
            return 0;
        } else {
            return users.size();
        }
    }

    public Object getItem(int position) {
        return users.get(position);
    }

    public long getItemId(int position) {
        return position;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        
        TaskHolder holder;
        
        // récupération du holder
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.user, null);
            holder = new TaskHolder();
            holder.name = (TextView) convertView.findViewById(R.id.name);
            holder.id = (TextView) convertView.findViewById(R.id.id);
            convertView.setTag(holder);
        } else {
            holder = (TaskHolder) convertView.getTag();
        }
        
        // affichage de l'utilisateur d'index demande
        // via l'utilisation du holder
        if (position < getCount()) {
            User user = (User) getItem(position);
            holder.name.setText(user.getPrenom() + " " +user.getNom());
            holder.id.setText(user.getId());
        }
        
        return convertView;
    }
    
    public void setUsers(List<User> users) {
        this.users = users;
        // mise à jour de la liste
        notifyDataSetChanged();
    }
    
    public void addUser(User user) {
        if (!users.contains(user)) {
            users.add(user);
            // mise à jour de la liste
            notifyDataSetChanged();
        }
    }

    public void removeUser(User user) {
        users.remove(user);
        // mise à jour de la liste
        notifyDataSetChanged();
    }
    
    /**
     * Holder class :
     * Permet de ne pas multiplier le nombre
     * d'instance de View utilisées pour
     * l'affichae des utilisateurs dans la
     * ListView
     */
    static class TaskHolder {
        TextView name;
        TextView id;
    }

}
La méthode getView de l'Adapter permet de retourner une instance du LinearLayout utilisé pour le rendu d'un utilisateur (/res/layout/user.xml) et dans lequel les vues sont remplies avec les données de l'utilisateur en position demandée.

Appels aux services REST

L'interface utilisateur maintenant en place, nous allons nous pencher sur les appels aux services REST. Afin de ne pas tomber dans les fameuses erreurs ANR (Application Not Responding), les appels aux services s'exécuteront dans un Thread différent de celui utilisé pour les interactions avec l'utilisateur (rendu graphique, capture des événements, ...). Pour cela, nous utiliserons des AsyncTask afin que l'envoi de la requête au serveur, l'attente de la réponse et la récupération de la réponse s'exécute en parallèle du Thread principal. La méthode est similaire à l'utilisation du de la classe SwingWorker de Java 6.
/**
 * Classe abstraite pour l'envoi de requètes asynchrones au serveur.
 */
public abstract class AbstractTask 
        extends AsyncTask<HttpRequestBase, Void, HttpResponse> {
    
    /**
     * Appelée avant le lancement du traitement en arrière plan.
     * Cette méthode est exécutée dans le Thread appelant.
     */
    protected void onPreExecute() {}
    
    /**
     * Traitement en arrière plan.
     * Cette méthode est exécuté dans un Thread différent
     * du Thread appelant.
     */
    protected HttpResponse doInBackground(final HttpRequestBase...requests) {}

    /**
     * Appelée après la fin du traitement en arrière plan.
     */
    protected void onPostExecute(final HttpResponse response) {}
    
    /**
     * Traitement spécifique du JSON
     * @param in Le contenu de la réponse HTTP OK
     */
    protected abstract void handleJson(final InputStream in);

};
Cette classe nous permet de définir un comportement générique pour chacun des appels aux services REST ainsi qu'un comportement spécifique à implémenter :
onPreExecute
Cette méthode est appelée dans le Thread principal avant que le traitement en tâche de fond ne soit lancé. Dans notre cas, nous l'utiliserons pour afficher une fenêtre popup d'attente.
doInBackground
Cette méthode appelée dans un Thread annexe permet de lancer un traitement en tâche de fond. Nous l'utiliserons pour effectuer les requêtes HTTP à destination du serveur et attendre la réponse de ce dernier. Cette méthode retourne un objet dans le type correspond à celui de la méthode ci dessous.
onPostExecute
Cette méthode est appelée dans le Thread principal une fois que le traitement en tâche de fond a terminé.Elle prend en paramètre le résultat du traitement effectué en tache de fond. Dans notre cas, cette méthode sera utilisée pour analyser la réponse du serveur et traiter le contenu de celle-ci.
handleJson
Cette méthode abstraite doit être surchargée pour traiter de manière spécifique le contenu de la réponse du serveur au format JSON.
Nous utiliserons 4 implémentations différentes de cette classe abstraite, chacune ayant pour but le traitement d'un type particulier de réponse du serveur :
/**
 * Récupération de la liste des User
 */
private class ListUsersTask extends AbstractTask {

    protected void handleJson(final InputStream in) {
        final Type collectionType = new TypeToken<List<User>>(){}.getType();
        List<User> users = null;
        synchronized (lock) {
            users = gson.fromJson(new InputStreamReader(in), collectionType);
        }
        // La liste récupérée initialement est la référence
        // des User pour l'application Android
        adapter.setUsers(users);
    }

};
/**
 * Recuperation d'un utilisateur
 * déjà référencé localement
 */
private class GetUserTask extends AbstractTask {

    protected void handleJson(final InputStream in) {
        synchronized (lock) {
            updateCurrentUser(gson.fromJson(new InputStreamReader(in), User.class));
        }
    }

};
/**
 * Récupération d'un nouvel utilisateur
 * non encore référencé localement
 */
private class AddUserTask extends AbstractTask {

    protected void handleJson(final InputStream in) {
        synchronized (lock) {
            updateCurrentUser(gson.fromJson(new InputStreamReader(in), User.class));
        }
        adapter.addUser(currentUser);
    }

};
/**
 * Suppression d'un utilisateur
 * référencé localement
 */
private class DeleteUserTask extends AbstractTask {

    protected void handleJson(final InputStream in) {
        User fakeUser = new User();
        try {
            fakeUser.setId(new BufferedReader(
                    new InputStreamReader(in, ENCODING_UTF_8)).readLine());
        } catch (Exception e) {}
        adapter.removeUser(fakeUser);
        updateCurrentUser(null);
    }

};
Dans le cas de la suppression d'un utilisateur, le serveur nous renvoie un objet de type String sans délimiteur d'objet JSON : '{' ou '['. Nous traitons donc le contenu directement comme une chaine de caractères au moyen d'un BufferedReader.

Pour lancer ces traitements asynchrones d'envoi des requêtes au serveur, nous devons au préalable construire la HttpRequestBase que nous allons passer en paramètre à l'une de nos AbstractTask. Le code ci-dessous nous permet de mettre à jour l'utilisateur en cours d'édition en quelques étapes :
  1. Création d'un bean User avec les informations modifiées du formulaire
  2. Création d'une instance de la classe HttpPost pointant sur /user/{user.getId()}
  3. Le Content-Type du Header de la requête précise que le contenu est de type application/json
  4. Le bean User est sérialisé en JSON via la librairie Gson
  5. La représentation JSON est encodée en UTF-8 et positionnée dans la requête HTTP
  6. La requête est envoyé via une GetUserTask afin de récupérer le bean User retourné par le serveur
// mise a jour du currentUser
User updatedUser = new User();
updatedUser.setNom(name.getEditableText().toString());
updatedUser.setPrenom(forName.getEditableText().toString());
// création d'une requête de type POST
// l'URL contient l'ID du User à mettre à jour
HttpPost request = new HttpPost(
        getString(R.string.user_endpoint) + "/" + currentUser.getId());
// précision du Content-Type
request.setHeader("Content-Type", JSON_CONTENT_TYPE);
synchronized (lock) {
    try {
        // l'objet de type User sérialisé est envoyé dans le corps
        // de la requête HTTP et encodé en UTF-8 (cf. Jackson)
        request.setEntity(new StringEntity(gson.toJson(updatedUser), ENCODING_UTF_8));
    } catch (UnsupportedEncodingException e) {}
}
(new GetUserTask()).execute(request);
L'emploi de l'encodage UTF-8 est imposé par la librairie Jackson utilisé par Jersey côté serveur pour la sérialisation / désérialisation du JSON. En espérant un support de l'encodage ISO-8859-1 dans une prochaine release...

Pour l'appel aux autres méthodes du serveur vous pouvez regarder le code complet de l'Activity donné ci-dessous :
package net.androgames.blog.sample.rest.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.AdapterView.OnItemClickListener;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class UserConsumer extends Activity implements OnItemClickListener {
    
    private static final DefaultHttpClient httpClient = new DefaultHttpClient();
    static {
        HttpParams params = new BasicHttpParams();
        HttpProtocolParams.setContentCharset(params, "UTF-8");
        httpClient.setParams(params); 
    }
    
    private static final Gson gson = new Gson();

    private static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8";
    private static final String ENCODING_UTF_8 = "UTF-8";

    private static final int DIALOG_ERROR = 0;
    private static final int DIALOG_LOADING = 1;
    
    private EditText name, forName;
    private Button insertOrUpdate, initialize, delete;
    
    private User currentUser; // utilisateur courant
    private UserAdapter adapter; // utilisé comme référentiel local
    
    private Object lock = new Object();
    
    /**
     * Classe abstraite pour l'envoi de requêtes asynchrones au serveur.
     */
    private abstract class AbstractTask 
            extends AsyncTask<HttpRequestBase, Void, HttpResponse> {
        
        /**
         * Appelée avant le lancement du traitement en arrière plan.
         * Cette méthode est exécutée dans le Thread appelant.
         */
        protected void onPreExecute() {
            showDialog(DIALOG_LOADING);
        }
        
        /**
         * Traitement en arrière plan.
         * Cette méthode est exécutée dans un Thread différent
         * du Thread appelant.
         */
        protected HttpResponse doInBackground(final HttpRequestBase...requests) {
            HttpResponse response = null;
            synchronized (lock) {
                try {
                    response = httpClient.execute(requests[0]);
                } catch (Exception e) {
                    Log.e(UserConsumer.class.getSimpleName(), 
                            "Erreur d'appel au serveur", e);
                }
            }
            return response;
        }

        /**
         * Appelé après la fin du traitement en arrière plan.
         */
        protected void onPostExecute(final HttpResponse response) {
            dismissDialog(DIALOG_LOADING);
            if (response == null 
                    || !(response.getStatusLine()
                            .getStatusCode() == HttpStatus.SC_OK)) {
                showDialog(DIALOG_ERROR);
            } else {
                try {
                    handleJson(response.getEntity().getContent());
                } catch (IOException e) {
                    Log.e(UserConsumer.class.getSimpleName(), 
                            "Erreur de flux", e);
                }
            }
        }
        
        /**
         * Traitement spécifique du JSON
         * @param in Le contenu de la réponse HTTP OK
         */
        protected abstract void handleJson(final InputStream in);

    };
    
    /**
     * Récupération de la liste des User
     */
    private class ListUsersTask extends AbstractTask {

        protected void handleJson(final InputStream in) {
            final Type collectionType = new TypeToken<List<User>>(){}.getType();
            List<User> users = null;
            synchronized (lock) {
                users = gson.fromJson(new InputStreamReader(in), collectionType);
            }
            // La liste récupérée initialement est la référence
            // des User pour l'application Android
            adapter.setUsers(users);
        }

    };
    
    /**
     * Recuperation d'un utilisateur
     * déjà référencé localement
     */
    private class GetUserTask extends AbstractTask {

        protected void handleJson(final InputStream in) {
            synchronized (lock) {
                updateCurrentUser(gson.fromJson(new InputStreamReader(in), User.class));
            }
        }

    };
    
    /**
     * Récupération d'un nouvel utilisateur
     * non encore référencé localement
     */
    private class AddUserTask extends AbstractTask {

        protected void handleJson(final InputStream in) {
            synchronized (lock) {
                updateCurrentUser(gson.fromJson(new InputStreamReader(in), User.class));
            }
            adapter.addUser(currentUser);
        }

    };
    
    /**
     * Suppression d'un utilisateur
     * référencé localement
     */
    private class DeleteUserTask extends AbstractTask {

        protected void handleJson(final InputStream in) {
            User fakeUser = new User();
            try {
                fakeUser.setId(new BufferedReader(
                        new InputStreamReader(in, ENCODING_UTF_8)).readLine());
            } catch (Exception e) {}
            adapter.removeUser(fakeUser);
            updateCurrentUser(null);
        }

    };
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        adapter = new UserAdapter((ListView) findViewById(R.id.users), this);
        insertOrUpdate = (Button) findViewById(R.id.insertOrUpdate);
        initialize = (Button) findViewById(R.id.initialize);
        delete = (Button) findViewById(R.id.delete);
        name = (EditText) findViewById(R.id.name);
        forName = (EditText) findViewById(R.id.forName);
    }
    
    @Override
    public void onResume() {
        super.onResume();
        // recuperation de tous les utilisateurs
        (new ListUsersTask()).execute(new HttpGet(getString(R.string.user_endpoint)));
        // initialisation des actions
        ((ListView) findViewById(R.id.users)).setOnItemClickListener(this);
        // initialisation de l'IHM
        // currentUser peut ne pas être null
        // si l'activité a été résumée
        updateCurrentUser(currentUser);
    }
    
    /**
     * Clique sur le bouton Créer ou Enregistrer
     * @param v
     */
    public void insertOrUpdate(View v) {
        if (currentUser == null) {
            // nouvel utilisateur
            User user = new User();
            user.setNom(name.getEditableText().toString());
            user.setPrenom(forName.getEditableText().toString());
            // création d'une requête de type POST
            // l'URL contient l'ID du User Ã  mettre Ã  jour
            HttpPut request = new HttpPut(getString(R.string.user_endpoint));
            // précision du Content-Type
            request.setHeader("Content-Type", JSON_CONTENT_TYPE);
            synchronized (lock) {
                try {
                    // l'objet de type User sérialisé est envoyé dans le corps
                    // de la requête HTTP et encodé en UTF-8 (cf. Jackson)
                    request.setEntity(new StringEntity(
                            gson.toJson(user), ENCODING_UTF_8));
                } catch (UnsupportedEncodingException e) {}
            }
            (new AddUserTask()).execute(request);
        } else {
            // mise a jour du currentUser
            User updatedUser = new User();
            updatedUser.setNom(name.getEditableText().toString());
            updatedUser.setPrenom(forName.getEditableText().toString());
            // création d'une requête de type POST
            // l'URL contient l'ID du User Ã  mettre Ã  jour
            HttpPost request = new HttpPost(
                    getString(R.string.user_endpoint) + "/" + currentUser.getId());
            // précision du Content-Type
            request.setHeader("Content-Type", JSON_CONTENT_TYPE);
            synchronized (lock) {
                try {
                    // l'objet de type User sérialisé est envoyé dans le corps
                    // de la requête HTTP et encodé en UTF-8 (cf. Jackson)
                    request.setEntity(new StringEntity(
                            gson.toJson(updatedUser), ENCODING_UTF_8));
                } catch (UnsupportedEncodingException e) {}
            }
            (new GetUserTask()).execute(request);
        }
    }
    
    /**
     * Clique sur le bouton Supprimer
     * @param v
     */
    public void delete(View v) {
        // envoi d'une requête DELETE au serveur
        // sur l'URL correspondant au User Ã  supprimer
        (new DeleteUserTask()).execute(new HttpDelete(
                getString(R.string.user_endpoint) + "/" + currentUser.getId()));
    }
    
    /**
     * Clique sur le bouton Annuler
     * @param v
     */
    public void initialize(View v) {
        // raz du formulaire
        updateCurrentUser(null);
    }

    // Met a jour le formulaire avec les
    // informations de l'utilisateur passé
    // en paramètre. Si le formulaire est 
    // positionné sur les données d'un utilisateur
    // de même id, le référentiel local est mis à jour
    private void updateCurrentUser(User user) {
        if (user == null) {
            currentUser = null;
            name.setText("");
            forName.setText("");
            delete.setVisibility(View.GONE);
            initialize.setVisibility(View.GONE);
            insertOrUpdate.setText(R.string.create);
        } else {
            if (!user.equals(currentUser)) {
                // changement de User
                currentUser = user;
            } else {
                // mise a jour des informations du User
                // dans le référentiel local
                currentUser.setNom(user.getNom());
                currentUser.setPrenom(user.getPrenom());
                adapter.notifyDataSetChanged();
            }
            // mise à jour du formulaire
            name.setText(currentUser.getNom());
            forName.setText(currentUser.getPrenom());
            delete.setVisibility(View.VISIBLE);
            initialize.setVisibility(View.VISIBLE);
            insertOrUpdate.setText(R.string.update);
        }
    }
    
    @Override
    public Dialog onCreateDialog(int dialogId) {
        Dialog dialog = null;
        AlertDialog.Builder builder = null;
        switch (dialogId) {
        
            case DIALOG_LOADING : // recuperation en cours...
                dialog = new ProgressDialog(this);
                ((ProgressDialog) dialog).setIndeterminate(true);
                ((ProgressDialog) dialog).setMessage(getString(R.string.loading));
                break;
                
            case DIALOG_ERROR : // message d'erreur
                builder = new AlertDialog.Builder(this);
                builder.setTitle(R.string.error)
                       .setMessage(R.string.error_message)
                       .setNegativeButton(R.string.close, new OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                            }
                        });
                dialog = builder.create();
                break;
        }
        return dialog;
    }
    
    /**
     * L'utilisateur à cliqué sur un User de la liste,
     * le formulaire est mis à jour avec les informations
     * du User récupérés depuis le serveur
     */

    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // mise a jour du formulaire avec les informations
        // du User sélectionné par l'utilisateur
        updateCurrentUser((User) adapter.getItem(position));
        
        // on aurait également pu demander l'utilisateur 
        // au serveur à chaque fois, mais on considere
        // que la liste initialement chargée est notre
        // référence afin de garder un jeu de donnée cohérent
        
        // (new GetUserTask()).execute(new HttpGet(
        //        getString(R.string.user_endpoint) + 
        //        "/" + ((User) adapter.getItem(position)).getId()));
    }
    
}
Vous pouvez récupérer l'exemple complet sur Google Code : SampleAndroidRestClient.
Fork me on GitHub