insideIT.fr : le blog des architectes IT de SFEIR

Aller au contenu | Aller au menu | Aller à la recherche

samedi 16 avril 2011

Du Jersey, du Guice et de l'App Engine 3/3

Et pour finir cette série d'article, nous allons nous intéresser à la génération de JSon avec Jersey et bien sûr à sa testabilité.

Le xml, c'est bien gentil, mais dans des échanges REST ça peut être un peu lourd, surtout si le consommateur est un appareil mobile. La génération JSon avec Jersey peut s'appuyer sur JAX-B. Et oui, c'est justement pour cela que l'on s'en est occupé dans le précédant article. Le mapping, lui, ne change pas.

La resource ne nécessite qu'un petit changement :

 @GET
 @Path("{name}")
 @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) 
 public Hello reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }

Comme on est sympa, on permet de renvoyer soit du Json, soit du Xml, c'est le consommateur qui décide. Par défaut, c'est le 1er format qui est choisit, soit le JSon.

La génération JSon va nécessiter un peu de configuration, principalement à cause du type de JSon généré. C'est le ContextResolver qui va s'occuper de ça :

@Provider
@Singleton
public class JAXBContextResolver implements ContextResolver<JAXBContext> {

 /** Package that contains object that can be mapped */
 private static final String JAXB_OBJECT_PACKAGE = Hello.class.getPackage().getName();

 private final JAXBContext context;

 public JAXBContextResolver() throws Exception {
  this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), 
                                     JAXB_OBJECT_PACKAGE);
 }

 @Override
 public JAXBContext getContext(Class<?> objectType) {
  if(objectType.getPackage().getName().equals(JAXB_OBJECT_PACKAGE)){
   return context;
         }
  return null;
 }
}

Ici le type de JSon souhaité est le natural. Cet objet doit être passés dans le même package que les resources, il profitera ainsi lui aussi de la découverte automatique au démarrage de Guice. Ce resolver n'est pas obligatoire, sans lui, le JSon généré est par défaut en mode mapped.

La configuration des tests, va devoir évoluer un peu pour prendre en compte notre génération en ode natural. La méthode configure() devient :

@Override
 protected AppDescriptor configure() {
  ClientConfig clientConfig = new DefaultClientConfig();
  clientConfig.getClasses().add(JAXBContextResolver.class);
  injector = Guice.createInjector(new ServletModule() {
   @Override
   protected void configureServlets() {
    bind(getTestingResourceClass());
    bind(JAXBContextResolver.class);
    serve("/*").with(GuiceContainer.class);
   }
  }); 
  return new WebAppDescriptor.Builder()
           .contextListenerClass(GuiceTestConfig.class)
           .filterClass(GuiceFilter.class)
           .clientConfig(clientConfig)
           .servletPath("/")
           .build();
 }

Ainsi du coté serveur comme du coté client, les échanges seront dans le même format. Nos tests deviendront :

@Test
 public void shoulReplyHelloInXml(){
  doShoulReplyHello(MediaType.APPLICATION_XML_TYPE);
 }
 
 @Test
 public void shoulReplyHelloInJson(){
  doShoulReplyHello(MediaType.APPLICATION_JSON_TYPE);
 } 
 
 private void doShoulReplyHello(MediaType type){
  String message = "Hello";
  String name ="Nicolas";
  Hello hello = new Hello(message, name);
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(hello);
  
  ClientResponse response = resource().path("hello").path(name)
                                      .accept(type).get(ClientResponse.class);
  
  verify(helloServiceMock).saysHelloToSomeone(name);
  assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
  assertThat(response.getType()).isEqualTo(type);
  Hello entity = response.getEntity(Hello.class);
  assertThat(entity).isNotNull().isEqualTo(hello);  
  
 } 

Une des différenciation entre les types de JSon générés se fait sur la façon dont sont écrites les listes. En mode natural, nous avons par exemple : objet1, objet2, ... avec des objet {"attributA":"valeurA", ....}

Imaginons que nous avons une autre ressource qui par grande politesse retourne 2 Hellos :

@Path("doublehello")
@Singleton
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class DoubleHelloResource {
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public List<Hello> reply(@PathParam("name") String name){
  List<Hello> hellos = new ArrayList<Hello>();
  hellos.add(helloService.saysHelloToSomeone(name));
  hellos.add(helloService.saysHelloToSomeone(name));
  return hellos;
 }
 
}

Pour vérifier sa bonne génération, nous aurions le test suivant :

@Test
 public void shoudHaveTwoHello(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas"))
                        .thenReturn(new Hello(message, name)); 
  ClientResponse response = resource().path("doublehello").path(name)
                                      .get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
  List<hello> hellos = response.getEntity(new GenericType<List<Hello>>(){});
  assertThat(hellos).isNotNull().hasSize(2);
 }
 
 @Test
 public void shoudBeInNaturalJson(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas"))
                                .thenReturn(new Hello(message, name)); 
  ClientResponse response = resource().path("doublehello").path(name)
                                      .get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
  String hellos = response.getEntity(String.class);
  assertThat(hellos).isEqualTo(naturalHelloJSon(message, name));
 }
 
 public String naturalHelloJSon(String message, String name){
  StringBuilder sb = new StringBuilder();
  sb.append("[{\"message\":\"").append(message)
    .append("\",\"name\":\"").append(name).append("\"},");
  sb.append("{\"message\":\"").append(message)
    .append("\",\"name\":\"").append(name).append("\"}]");
  return sb.toString();
 }

Même s'il est un format intéressant, le JSon souffre d'un problème lié au javascript : celui du cross-domain qui fait que l'on ne peut pas interroger un autre domain que celui de la page web. <a href="http://en.wikipedia.org/wiki/JSONP">JSonP</a> permet d'évincer cette contrainte.

Jersey permet aussi de générer ce type de réponse mais un peu moins facilement. Nous allons créer une nouvelle méthode pour ce type de réponse :

 @GET
 @Path("{name}.jsonp")
 @Produces("application/x-javascript")
 public JSONWithPadding replyWithJsonP(@PathParam("name") String name,
           @QueryParam("callback") @DefaultValue(CALLBACK_DEFAULT_NAME) String callback){
  Hello hello = helloService.saysHelloToSomeone(name);
  return new JSONWithPadding(hello, callback);
 } 

Son test reste dans l'optique des précédents :

@Test
 public void shoudBeJsonpWithCallbackNameParam(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(new Hello(message, name));
  String callbackName = "monCallback";
  
  ClientResponse response = resource().path("hello").path(name+".jsonp")
                                      .queryParam("callback", callbackName)
                                        .get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType().toString()).isEqualTo("application/x-javascript");
  assertThat(response.getEntity(String.class)).isNotNull().startsWith(callbackName);
 }

Je n'ai pas malheureusement pas trouvé comment unmarshmaller ce message.

Et voilà, ce tour d'horizon est fini, amusez vous bien avec ces quelques technos. Comme à chaque fois, le code source est disponible

dimanche 10 avril 2011

Du Jersey, du Guice et de l'App Engine 2/3

Dans le dernier article, nous disposions d'un service Hello World qui renvoyait du texte brut. Cette fois, nous allons lui ajouter la capacité à répondre du xml en sérialisant le message avec JAX-B.

Tout d'abord, notre nouvelle réponse sera faite par l'objet suivant :

XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Hello {
 
 private String message;
 private String name;
 
 public Hello(){
 }
 
 public Hello(String message, String name) {
  super();
  this.message = message;
  this.name = name;
 }
 
 public String getMessage() {
  return message;
 }
 
 public void setMessage(String message) {
  this.message = message;
 }
 
 public String getName() {
  return name;
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 @Override
 public int hashCode() {
  return Objects.hashCode(message, name);
 }
 
 @Override
 public boolean equals(Object obj) {
     if(obj instanceof Hello){
         final Hello other = (Hello) obj;
         return Objects.equal(message, other.message)
             && Objects.equal(name, other.name);
     } else{
         return false;
     }
 }
}

La ressource exposée évolue peu :

Path("hello")
@Singleton
@Produces(MediaType.APPLICATION_XML)
public class HelloResource {
 
 @Context 
 UriInfo uriInfo; 
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public Hello reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }

 @POST
 public Response send(String name){
  Hello hello = helloService.sendHello(name);
  URI uri = uriInfo.getAbsolutePathBuilder().build();
  return Response.created(uri).entity(hello).build();
 }  
 
 
 public void setHelloService(HelloService helloService) {
  this.helloService = helloService;
 }
 
}

Les opérations de marshall/unmarshall sont opérées directement par Jersey lui même.

De même, les tests vont peu évoluer, seul le type de données attendu va changer. Nous aurons par exemple :

@Test
 public void shoulReplyHello(){
  String name ="Nicolas";
  String hello = "Hello "+name;
  when(helloServiceMock.saysHelloToSomeone(name)).thenReturn(hello);
  
  ClientResponse response = resource().path("hello").path(name).get(ClientResponse.class);
  
  verify(helloServiceMock).saysHelloToSomeone(name);
  assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
  assertThat(response.getType()).isEqualTo(MediaType.TEXT_PLAIN_TYPE);
  assertThat(response.getEntity(String.class)).isEqualTo("Hello Nicolas");
  
 }

Et c'est tout, pour aujourd'hui ...

Bon ok, je reconnais, c'est un peu l'arnaque cet article, il y a peu de choses à faire. Mais n'est ce pas justement ça qui est intéressant, non ?

La prochaine fois, nous terminerons cette ballade autour de Jersey en générant du JSon.

Le code est disponible ici.

samedi 2 avril 2011

Du Jersey, du Guice et de l'App Engine 1/3

J'avais déjà parlé, dans de précédents articles, de la génération de xml et de json avec Jersey. Et si maintenant, on s'amusait à générer du protobuf ?
On parle de protobuf pour Protocol Buffers, une techno Google pour encoder des structures de données. Ce format de données compact est utilisé en interne chez Google des échanges de données. Etant basé sur la déclaration d'une structure de données dans un idl, protobuf possède plusieurs implémentation et est ainsi utilisable dans plusieurs langage. En java, la génération du code cible se fait avec ant. Mais bien sur reste utilisable avec maven par le plugin ant.
Nous allons reprendre notre Hello qui avait d'exemple. Voici sa structure protobuf :

package nfrancois.poc;

option java_package = "nfrancois.poc.protobuf.model";
option java_outer_classname = "HelloProto";

message Hello {
  required string name = 1;
  required string message = 2;
}


La structure se comprend assez facilement. Attention par contre, au trompeur package de 1ère ligne, qui n'est pas lié à la notion de package que nous avons en java. Il sert comme espace de nommage et éviter des collisions de nom si plusieurs objets protobuf portent le même nom. Puisque depuis cette idl, je pourrai aussi bien générer en C++ ou un autre langage, le vrai nom de package java est indiqué par l'option "java_package", de la même façon pour le nom de classe qui va tout encapsuler qui sera "java_outer_classname"
Pour plus d'information sur protobuf, je vous invite à consulter sa page google code.
Le générateur protobuf générera un fichier HelloProto.java, qui permettra de manipuler les Hello : création via pattern builder, encodage/désencodage, ... Le "vrai" Hello sera encapuslé au sein de ce dernier. Comme je disais, je génère le java par le ant plugin de maven :

<plugin>
 <groupid>org.apache.maven.plugins</groupId>
 <artifactid>maven-antrun-plugin</artifactId>
 <version>1.6</version>
 <executions>
  <execution>
   <id>generate-sources</id>
   <phase>generate-sources</phase>
   <configuration>
    <target>
     <exec executable='protoc'>
             <arg value='--java_out=src/main/java' />
             <arg value='src/main/resources/Hello.proto' />
             </exec>
    </target>
   </configuration>
   <goals>
    <goal>run</goal>
   </goals>
  </execution>     
 </executions>
</plugin>


et bien sûr des dépendances protobuf

<dependency>
    <groupid>com.google.protobuf</groupId>
    <artifactid>protobuf-java</artifactId>
    <version>2.4.0a</version>
</dependency>


Le contrat de test sera assez proche de se que nous avions dans les tests précédents :

@Test
public void shoulReplyHello(){
 // Given
 String message = "Hello";
 String name ="Nicolas";
 Hello hello = HelloProto.Hello.newBuilder().setName(name).setMessage(message).build();
 when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(hello);
 // When
 ClientResponse response = resource().path("hello").path(name).get(ClientResponse.class);
 // Then
 verify(helloServiceMock).saysHelloToSomeone(name);
 assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
 assertThat(response.getType().toString()).isEqualTo("application/x-protobuf");
 Hello entity = response.getEntity(Hello.class);
 assertThat(entity).isNotNull().isEqualTo(hello);
}


La resource REST, elle aussi va peut évoluer :

@Path("hello")
@Singleton
@Produces("application/x-protobuf")
public class HelloResource {
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public Hello reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }
 
 public void setHelloService(HelloService helloService) {
  this.helloService = helloService;
 }
 
}


La difficulté à laquelle il faut se confronter, c'est que Jersey ne permet pas de gérer de base le protobuf… Pas grave, on va s'occuper de faire le lien entre l'encodage/désencodage de protobuf et Jersey.

Commençons par le reader qui s'occupe de désencoder le protobuff. Pour celà, nous devons implémenter l'interface MessageBodyReader où nous aurons du code technique protobuf.

@Provider
@Consumes("application/x-protobuf")
@Singleton
public class ProtobufMessageBodyReader implements MessageBodyReader<Message> {

 public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations,
                                                            MediaType mediaType) {
  return Message.class.isAssignableFrom(type);
 }

 public Message readFrom(Class<Message> type, Type genericType, Annotation[] annotations, 
    MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
    throws IOException, WebApplicationException {
  try {
   Method newBuilder = type.getMethod("newBuilder");
   GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(type);
   return builder.mergeFrom(entityStream).build();
  } catch (Exception e) {
   throw new WebApplicationException(e);
  }
 }

}



C'est par le content type "application/x-protobuf" que JAX-RS fera matcher le type le reader/writer à l'entrée/sortie de la resource. Pour l'encodage, c'est l'interface MessageBodyWriter qu'il faut implémenter.

@Provider
@Produces("application/x-protobuf")
@Singleton
public class ProtobufMessageBodyWriter implements MessageBodyWriter<Message> {
 public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,
                                                              MediaType mediaType) {
  return Message.class.isAssignableFrom(type);
 }

 private Map<Object, byte[]> buffer = new WeakHashMap<Object, byte[]>();

 public long getSize(Message m, Class<?> type, Type genericType, Annotation[] annotations, 
                                                                 MediaType mediaType) {
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  try {
   m.writeTo(baos);
  } catch (IOException e) {
   return -1;
  }
  byte[] bytes = baos.toByteArray();
  buffer.put(m, bytes);
  return bytes.length;
 }

 public void writeTo(Message m, Class type, Type genericType, Annotation[] annotations, 
                MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream)
                throws IOException, WebApplicationException {
  entityStream.write(buffer.remove(m));
 }
}


La configuration de test, quant à elle sera un peu plus complexe, car il faut que la partie cliente puisse désencoder toute seule le protobuf :

protected AppDescriptor configure() {
 ClientConfig clientConfig = new DefaultClientConfig();
 clientConfig.getClasses().add(ProtobufMessageBodyReader.class);
 clientConfig.getClasses().add(ProtobufMessageBodyWriter.class);
 injector = Guice.createInjector(new ServletModule() {
 @Override
 protected void configureServlets() {
  bind(ProtobufMessageBodyReader.class);
  bind(ProtobufMessageBodyWriter.class);
  bind(HelloResource.class);
  serve("/*").with(GuiceContainer.class);
  }
 }); 
 return new WebAppDescriptor.Builder()
          .contextListenerClass(GuiceTestConfig.class)
          .filterClass(GuiceFilter.class)
          .clientConfig(clientConfig)
          .servletPath("/")
          .build();
}


Le code complet est ici.

lundi 23 novembre 2009

Atmosphere, atmosphere


Au devoxx de cette année nous avons pu assister à la présentation du framework Atmosphere par Jean-François Arcand, créateur du framework et Paul Sandoz, contributeur sur le projet Jersey utilisé par Atmosphere en ce qui concerne les aspects REST.

Après une rapide présentation des concepts liés aux architectures PUSH pour les applications WEB. Les deux speakers sont rentrés dans le vif du sujet en développant en direct une petite application permettant de démontrer les possibilités offertes par Atmosphere.



Le PUSH ?

Dans les applications WEB, le PUSH correspond à l'utilisation des protocoles WEB pour permettre aux différents clients (navigateurs généralement) de se voir notifier des informations de la part du serveur sans avoir à faire des requêtes périodiques sur ce dernier. Comme son nom l'indique, les informations sont poussées du serveur vers le(s) client(s) et non plus tirées par chaque client désireux d'y avoir accès. L'intérêt d'une telle technique réside dans la possibilité de voir se propager rapidement une modification de l'état du serveur et atteindre ainsi un pseudo temps-réel tout en économisant des échanges réseaux inutiles. Avec le PUSH on peut par exemple dans le cas d'un tchat, propager les nouveaux messages d'une discussion du serveur vers les clients connectés de manière quasi instantanée.
Lors de cette conférence Jean-François nous a décrit les trois types de communications existantes dans les applications WEB à savoir :
  • Polling (technique traditionnelle)
  • Ajax PUSH  ou Long Polling : ce que propose Atmosphere
  • Http Streaming : Une technique idéale théoriquement mais qui souffre de problème d'intégration avec les proxys notamment.
Atmosphere est un framework (le premier) à base de POJO proposant aux développeurs d'applications PUSH une couche de programmation masquant la complexité des serveurs Java dans ce domaine.
Pour développer la partie serveur d'une application, le framework est utilisable sur de nombreux serveurs : des conteneurs legers (Tomcat, Jetty), certains serveurs d'applications comme Glassfish et Weblogic, il est même disponible pour le Google App Engine. Concernant ce dernier, on est en droit de se demander comment peut fonctionner le Long Polling avec des requêtes censées durer moins de 30s ? Jean-François nous a donné la solution en expliquant qu'Atmosphere implémente sont propre système de continuation permettant de passer outre cette contrainte. Il faut aussi noter la compatibilité d'Atmosphere avec les Servlets 3.0.

En ce qui concerne la partie cliente des applications, de nombreux frameworks sont supportés dont GWT et Wicket. 
Lors de la présentation, les speakers se sont focalisés sur une sous-partie du framework à savoir les annotations disponibles pour les POJOs et les aspects REST intégrés via Jersey.

Lors de la démo, les deux conférenciers ont pu nous démontrer la simplicité avec laquelle il est possible de créer une application WEB asynchrone utilisant le PUSH. Ils ont insisté sur le fait d'utiliser de simples POJO et de les rendre manipulables via REST. On obtient par exemple des classes ressemblant à ceci :

@Path("/{topic}")
@Produces("text/plain;charset=ISO-8859-1")
public class PubSub {
    private @PathParam("topic") Broadcaster topic;
    @GET
    @Suspend
    public Broadcastable subscribe() {
        return new Broadcastable("OK",topic);
    }
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Broadcast
    public Broadcastable publish(@FormParam("message") String message){
        return new Broadcastable(message,topic);
    }
}
L'idée principale de cette démo était d'utiliser Atmosphere pour suspendre la connection client lorsqu'il s'incrit à un topic "test" en appelant http://mon_serveur/test. Un autre client peut alors publier des messages qui seront diffusés à tous les clients connectés sur ce topic en soumettant un simple formulaire HTML contenant un paramètre "message" sous forme textuelle.

Ce que nous aurons retenu

Atmosphere simplifie effectivement le travail des développeurs en ce qui concerne la gestion du PUSH dans une application Java. Il n'est pas nécessaire de refondre entièrement une application existante pour y intégrer Atmosphere, la plupart des problèmes d'intégration sont adressés par le framework (gestion des proxys pour les connections HTTP, les limitations de chacun des navigateurs).

Resources:
Le site du projet : http://atmosphere.dev.java.net
Blogs :
Twitter : 
    @atmosphere_java 
    @jfarcand 

Ludovic Meurillon et Vincent Bostoen

dimanche 29 juin 2008

débuter avec Restlet

restlet_logo Dans le cadre d'un projet sur lequel je suis intervenu, j'ai eu a refactorer le format des URIs d'un middleware RESTful. Ce fut également l'occasion d'introduire Restlet, le framework Java de référence pour faire du REST, à la place d'un framework "maison". La refacto fut courte et sans douleur, c'est pourquoi je recommande Restlet aux personnes qui souhaitent faire du REST en Java et leur donne quelques liens pour débuter.

Pour commencer, je me suis appuyé sur l'exemple "First Resource", qui permet de se familiariser avec les notions de Restlet grâce à un exemple simple.

Concernant la sécurité, tout ce qu'il y a à savoir se trouve sur la page "Securing a Restlet application"

Pour la gestion des exceptions, il suffit d'étendre la classe StatusService puis d'affecter la nouvelle classe à l'Application concernée.

Pour finir, il ne faut pas oublier de modifier le fichier "web.xml" comme indiqué ici.