Cap 3 Your First Jax Rs Service

JAX-RS è un framework che si concentra sull'applicare le annotazioni java agli oggetti piani. Questa annotazione collega una specifica URI e una operazione HTTP a un singolo metodo per una classe java. Ha una iniezione di parametri attraverso l'annotazione che prende i parametri dalla richiesta http. Ha un lettore per il message body, e uno scrittore che permette di disaccopiare il formato dei dati serializzazione e deserializzazione dai tuoi oggetti java. Ha anche un mappatore di eccezioni per i codici di risposta http e i messaggi. Inolre ha diverse altre facility per http content negotiazion.
Questo capitolo è solo un'introduzione.

Developing a JAX-RS RESTful Service

Definiamo un servizio che permette di leggere creare e aggiornare i clienti, per fare questo definiamo due classi, una verrà usata per rappresentare Customers, le altre per i servizi.

Customer: The Data Class

La classe che rappresenta tutte le proprietà. Le proprietà sono attributi che possono essere definiti attraverso i campi della classe o attraverso i metodi set e get. Una classe java che segue queste regole è anche detta java bean.

package com.restfully.shop.domain;
public class Customer {
   private int id;
   private String firstName;
   private String lastName;
   private String street;
   private String city;
   private String state;
   private String zip;
   private String country;
   public int getId() { return id; }
   public void setId(int id) { this.id = id; }
   public String getFirstName() { return firstName; }
   public void setFirstName(String firstName) {
                   this.firstName = firstName; }
   public String getLastName() { return lastName; }
   public void setLastName(String lastName) {
                         this.lastName = lastName; }
   public String getStreet() { return street; }
   public void setStreet(String street) { this.street = street; }
   public String getCity() { return city; }
   public void setCity(String city) { this.city = city; }
   public String getState() { return state; }
   public void setState(String state) { this.state = state; }
   public String getZip() { return zip; }
   public void setZip(String zip) { this.zip = zip; }
   public String getCountry() { return country; }
   public void setCountry(String country) { this.country = country; }
}

Questa classe può essere usata anche per mappare gli oggetti nel database tramite JPA ecc.

CustomerResource: Our JAX-RS Service

Un servizio JAX-RS è una classe java che usa le annotazioni JAX-RS per legare e mappare specifiche richiesta HTTP a metodi java che possono servire queste richieste. JAX-RS si può legare a modelli a componenti famosi come EJB , JBoss Seam ecc ma non definisce il proprio modello.
I servizi possono essere oggetti singletons o per-request.
Singleton sta a significare che solo un oggetto java serve una richiesta http.
Per-request significa che un oggetto java viene creato per processare una richiesta e gettato quando la richiesta finisce, in questo caso abbiamo anche una mancanza di stato.
Per il nostro esempio abbiamo una classe che implementa a singleton, abbiamo una mappa di oggetti Customers in memoria che a cui il client vuole accedere, in uno scenario reale i Customer saranno degli oggetti su un db.
Vediamo un esempio:

package com.restfully.shop.services;
import ...;
@Path("/customers")
public class CustomerResource {
   private Map<Integer, Customer> customerDB = new ConcurrentHashMap<Integer, Customer>();
   private AtomicInteger idCounter = new AtomicInteger();

Come si vede è una classe java piana e non implementa particolari interfacce. C'è solo l'annotazione @Path in testa alla classe che designa il servizio. Una classe java che si vuole venga identificata come servizio JAX-RS deve implementare questa annotazione. Il valore è "/customers" questo è l'uri ROOT relativo. Se il server è http://shop.restfully.com, il metodo sarà su http://shop.restfully.com/customers.
Vi è un oggetto di tipo Map e si usa ConcurrentHashMap perché è un singleton e può avere richieste concorrenti per accedere alla map. Se usassimo java.util.HashMap avremmo eccezioni di accesso in un ambiente multithreaded. Usando java.util.Hashtable si creerebbero delle sincronizzazioni strettoie. ConcurrentHashMap è la scelta migliore. Per generare gli id unici senza problemi di concorrenza si usa l'oggetto java.util.concurrent.atomic.AtomicInteger .

Creating customers

Vediamo la creazione dell'oggetto Customers. Viene creata una post che manda un documento xml che rappresenta l'oggetto che vogliamo creare.

@POST
@Consumes("application/xml")
public Response createCustomer(InputStream is) {
   Customer customer = readCustomer(is);
   customer.setId(idCounter.incrementAndGet());
   customerDB.put(customer.getId(), customer);
   System.out.println("Created customer " + customer.getId());
   return Response.created(URI.create("/customers/"+ customer.getId())).build();
}

I passi sono creazione dell'oggetto parsando lo stream, aggiunta dell'id, inserimento nell'id map .
La risposta sarà un codice 201 "Created" con un Location header che punto all'uri assoluto del customer che abbiamo appena creato.
Le due annotazioni viste @Path e la @POST associano tutte le richiesta di tipo post alla query URI/customers al metodo java creatCustomer().
L'annotazione @Counsumes applicata al metodo creatCustomer() specifica che media type si aspetta in input dalla richiesta http. Se il client posta una richiesta che non sia una xml gli viene restituito un messaggio di errore.
Il metodo prendo un java.io.InputStream è una forma base per un InputStream.
Con l'oggetto Response e il metodo created() viene creato un oggetto che contiene un codice ci status http 201. "Created" e inoltre aggiunge una location all'header http di risposta con il valore , qualcosa tipo http://shop.restfully.com/customers/333 , 333 è l'id dell'URI generato.

Retrieving customers

Nel capitolo precedente abbiamo deciso di usare come forma di recupero la GET /customers/{id} per recuperare gli oggetti. Per fare questo useremo questo metodo.

@GET
@Path("{id}")
@Produces("application/xml")
public StreamingOutput getCustomer(@PathParam("id") int id) {
   final Customer customer = customerDB.get(id);
   if (customer == null) {
      throw new WebApplicationException(
                                       Response.Status.NOT_FOUND);
   }
   return new StreamingOutput() {
      public void write(OutputStream outputStream)
                    throws IOException, WebApplicationException {
         outputCustomer(outputStream, customer);
      }
   };
}

Dobbiamo usare un'annotazione aggiuntiva per specificare l'uri dove legare il metodo sempre usando l'annotazione @Path . C'è una nuova annotazione @Produce che indica quale HTTP Content-Type la risposta GET restituirà.
L'annotazione @PathParam (indicato nei parametri del metodo) indica al provider JAX-RS che vuoi iniettare un pezzo di incoming URI nel parametro id. Il valore di id del parametro di @PathParam deve mathare con il pattern di URI definito nei metodi e nelle classi delle annotazioni di @Path . In questo l'id pattern è /customers/{id} . All'arrivo della richiesta http://shop.restfully.com/customers/333 la stringa 333 verrà estratta dall'uri, convertita in un integer e iniettata nel parametro del metodo getCustomer().
Se l'oggetto non viene trovato verrà lanciata una javax.ws.rs.WebApplicationException. Questa exception setterà l'HTTP response code a 404, “Not Found,” .
In quest'esempio scriviamo manualmente la risposta al client attraverso la java.io.OutputStream. Se si vuole restituire lo streaming manualmentein jax-rs si deve implementare e restituire una istanza dell'interfaccia javax.ws.rs.core.StreamingOutput. StreamingOutput è una interfaccia di callback con un metodo di callback write() ecco il codice:
package javax.ws.rs.core;
public interface StreamingOutput {
   public void write(OutputStream os) throws IOException, WebApplicationException;
}

Nell'ultima riga del metodo visto sopra getCustomer() implementiamo questa interfaccia e la restituiamo. Implementiamo il metodo write dove usiamo un altro metodo della classe chiamato outputCustomer che esiste nella nostra classe, questo manderà l'output xml che rappresenterà l'oggetto Customer.
In generale non conviene usare il modo manuale per farsi mandare la risposta di output, ci sono degli altri metodi che convertono gli oggetti java nei formati che poi vengono spediti ma verranno visti nel capitolo 6.

Updating a Customer

Implementiamo l'aggiornamento che abbiamo visto nel capitolo due.

@PUT
@Path("{id}")
@Consumes("application/xml")
public void updateCustomer(@PathParam("id") int id,
                             InputStream is) {
   Customer update = readCustomer(is);
   Customer current = customerDB.get(id);
   if (current == null)
     throw new WebApplicationException(Response.Status.NOT_FOUND);
   current.setFirstName(update.getFirstName());
   current.setLastName(update.getLastName());
   current.setStreet(update.getStreet());
   current.setState(update.getState());
   current.setZip(update.getZip());
   current.setCountry(update.getCountry());
}

Viene implementato il metodo http put.
Il metodo prende due parametri l'id dell'oggetto da aggiornare e come l'altro metodo usiamo la stessa annotazione @PathParam per estrarre l'id dalla richiesta in entrata. Il secondo parametro è un InputStram che permette di leggere lo stream xml che è stato mandato con la richiesta di put. Come createCustomer() un parametro che non è annotato con la notazione jax-rs è considerato una rappresentazione del corpo della richiesta in entrata.
Nella prima parte dell'implementazione del metodo leggiamo dal documento xml e creiamo l'oggetto Customer. Il metodo recupera l'oggetto dalla map se non esiste lancia una WebApplicationException che lancerà una 404 in http che indica oggetto non trovato.

Utility methods

Questi sono metodi che usiamo in tutti e tre i servizi visti fin'ora. Il metodo outputCustomer() prende un oggetto e scrive il suo xml in risposta sullo stream.

protected void outputCustomer(OutputStream os, Customer cust) throws IOException 
{
   PrintStream writer = new PrintStream(os);
   writer.println("<customer id=\"" + cust.getId() + "\">");
   writer.println("    <first-name>" + cust.getFirstName() + "</first-name>");
   writer.println("    <last-name>" + cust.getLastName()  + "</last-name>");
   writer.println("    <street>" + cust.getStreet() + "</street>");
  writer.println("   <city>" + cust.getCity() + "</city>");
  writer.println("   <state>" + cust.getState() + "</state>");
  writer.println("   <zip>" + cust.getZip() + "</zip>");
  writer.println("   <country>" + cust.getCountry() + "</country>");
  writer.println("</customer>");
}

Come si vede effettua una conversione bruta dell'oggetto al testo xml.

L'altro metodo è readCustomer()

protected Customer readCustomer(InputStream is) {
   try {
      DocumentBuilder builder =
         DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document doc = builder.parse(is);
      Element root = doc.getDocumentElement();

Usiamo il parser xml della jdk invece di fare il parsing manuale. Il metodo inizia parsando l'InputStream e creando un oggetto java che rappresenta un documento xml. Il resto del readCustomer() , che sta sotto, muove i dati dal modello xml al nuovo oggetto Customer creato
Customer cust = new Customer();
if (root.getAttribute("id") != null
       && !root.getAttribute("id").trim().equals("")) {
   cust.setId(Integer.valueOf(root.getAttribute("id")));
}
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
   Element element = (Element) nodes.item(i);
   if (element.getTagName().equals("first-name")) {
      cust.setFirstName(element.getTextContent());
   }
   else if (element.getTagName().equals("last-name")) {
      cust.setLastName(element.getTextContent());
   }
   else if (element.getTagName().equals("street")) {
      cust.setStreet(element.getTextContent());
   }
   else if (element.getTagName().equals("city")) {
      cust.setCity(element.getTextContent());
   }
   else if (element.getTagName().equals("state")) {
      cust.setState(element.getTextContent());
   }
   else if (element.getTagName().equals("zip")) {
      cust.setZip(element.getTextContent());
   }
   else if (element.getTagName().equals("country")) {
      cust.setCountry(element.getTextContent());
          }
       }
       return cust;
    }
    catch (Exception e) {
       throw new WebApplicationException(e,
                     Response.Status.BAD_REQUEST);
    }
  }
}

Nel capitolo 6 vedremo come fare il parsing in automatico di tutto questo.

JAX-RS and Java Interfaces

Negli esempi precedenti abbiamo applicato direttamente le annotazioni alla classe java che implementa i nostri servizi. In jax-rs si può definire un'interfaccia che contiene tutte le annotazioni jax-rs invece che applicare tutte le annotazioni nelle classi implementate.
Le interfacce sono un grande modo to scope cosa si vuole modellare sui propri servizi. Con un'interfaccia puoi scrivere qualcosa che definisce cosa le tue api restful andranno a cercare con i metodi java prima che si scriva una singola riga di buiness logic.
Alcuni pensano che usando questa strategia il loro codice sarà più pulito e leggibile perché ci sono poche annotazioni.
Finally, sometimes you do have the case where the same business
logic must be exposed not only RESTfully, but also through SOAP and JAX-WS. In
this case, your business logic would look more like an explosion of annotations than
actual code. Interfaces are a great way to isolate all this metadata into one logical and
readable construct.

Trasformiamo il nostro esempio di prima in qualcosa basato sulle interfacce.

package com.restfully.shop.services;
import ...;
@Path("/customers")
public interface CustomerResource {
   @POST
   @Consumes("application/xml")
   public Response createCustomer(InputStream is);
   @GET
   @Path("{id}")
   @Produces("application/xml")
   public StreamingOutput getCustomer(@PathParam("id") int id);
   @PUT
  @Path("{id}")
  @Consumes("application/xml")
  public void updateCustomer(@PathParam("id") int id, InputStream is);
}

Ecco l'interfaccia definita adesso scriviamo una classe che implementa quest'interfaccia.
package com.restfully.shop.services;
import ...;
public class CustomerResourceService implements CustomerResource {
   public Response createCustomer(InputStream is) {
      ... the implementation ...
   }
   public StreamingOutput getCustomer(int id)
      ... the implementation ...
   }
   public void updateCustomer(int id, InputStream is) {
      ... the implementation ...
}

Come si vede nell'implementazione non sono presenti annotazioni che si trovano tutte nell'interfaccia.
Se serve si può sovrascrivere l'annotazione dell'implementazione
public class CustomerResourceService implements CustomerResource {
   @POST
   @Consumes("application/xml;charset=utf-8")
   public Response createCustomer(InputStream is) {
      ... the implementation ...
   }

Questo però è sconsigliabile perchè rende di difficile lettura il codice .

Inheritance

In alcuni casi è consigliabile usare l'ereditarietà , per esempio abbiamo i metodi outputCustomer() and
readCustomer() definiamo un metodo astratto.

package com.restfully.shop.services;
import ...;
public abstract class AbstractCustomerResource {
   @POST
   @Consumes("application/xml")
   public Response createCustomer(InputStream is) {
     ... complete implementation ...
   }
   @GET
   @Path("{id}")
   @Produces("application/xml")
   public StreamingOutput getCustomer(@PathParam("id") int id) {
      ... complete implementation
   }
   @PUT
   @Path("{id}")
   @Consumes("application/xml")
   public void updateCustomer(@PathParam("id") int id,
                                InputStream is) {
      ... complete implementation ...
   }
   abstract protected void outputCustomer(OutputStream os,
                                     Customer cust) throws IOException;
   abstract protected Customer readCustomer(InputStream is);
}

Dopo implementiamo la classe estendendo la abstract
package com.restfully.shop.services;
import ...;
@Path("/customers")
public class CustomerResource extends AbstractCustomerResource {
   protected void outputCustomer(OutputStream os, Customer cust)
                                                  throws IOException {
      ... the implementation ...
   }
protected Customer readCustomer(InputStream is) {
   ... the implementation ...
}

L'unico avvertimento è che una sottoclasse deve annotare se stessa con la @Path per identificare il metodo come una classe di servizio per il provider JAX-RS.

Deploying Our Service

Per fare il deploy c'è bisogno di un application server dentro dobbiamo deployare il nostro servizio jax , dobbiamo registrare il nostro servizio per fare questo abbiamo bisogno di una classe che estende quella base di jax-rs, di cui vediamo il codice sotto.

package javax.ws.rs.core;
import java.util.Collections;
import java.util.Set;
public abstract class Application {
   private static final Set<Object> emptySet = Collections.emptySet();
   public abstract Set<Class<?>> getClasses();
   public Set<Object> getSingletons() {
      return emptySet;
   }
}

Il metodo getClass restituisce una lista di classi servizio JAX-RS (e anche di provider che vedremo nel capitolo 6). Ogni classe servizio JAX-RS restituita da questo metodo seguirà il modello per-request menzionato prima. Quando l'implementazione del vendor JAX-RS determina che una richiesta http abbia bisogno di essere delivered da un metodo di una di queste classi , una istanza verrà creata per la durata di una richiesta e lanciata via. Si delega la creazione degli oggetti al runtime JAX-RS.

Il metodo getSingletons() restituisce una lista di oggetti servizio JAX-RS (e provider ). Il programmatore dell'applicazione è responsabile per creare e inizializzare questi oggetti.

Questi due metodi dicono al vendor JAX-RS quali servizi si vogliono sviluppare.

package com.restfully.shop.services;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
public class ShoppingApplication extends Application {
  private Set<Object> singletons = new HashSet<Object>();
  private Set<Class<?>> empty = new HashSet<Class<?>>();
  public ShoppingApplication() {
     singletons.add(new CustomerResource());
  }
  @Override
  public Set<Class<?>> getClasses() {
     return empty;
  }
  @Override
  public Set<Object> getSingletons() {
     return singletons;
  }
}

Deployment Within a Servlet Container

La maggior parte delle applicazioni sono scritte per essere eseguite in un application server servlet container come un WAR.
Bisogna far puntare il nostro servlet container alla nostra ShoppingApplication in modo che il runtime JAX-RS sa cosa deployare. Per far questo bisogna mettere alcune opzioni nel file web.xml . Se il nostro application server è JAX-RS-aware o in altre parole è fortemente integrato con JAX-RS possiamo dichiarare la nostra ShoppingApplication come un servlet.

<?xml version="1.0"?>
<web-app>
   <servlet>
      <servlet-name>Rest</servlet-name>
      <servlet-class>
          com.restfully.shop.services.ShoppingApplication
      </servlet-class>
   </servlet>
   <servlet-mapping>
      <servlet-name>Rest</servlet-name>
      <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>

Abbiamo anche bisogno di legare l'applicazione usando il servlet-mapping alla base URI.
Se il nostro application server non è JAX-RS-aware si deve specificare il JAX-RS provider servlet che manipola le invocazioni JAR-RS . L'application class dovrebbe essere specificata come un ini-param del servlet:

<?xml version="1.0"?>
<web-app>
   <display-name>Archetype Created Web Application</display-name>
   <servlet>
      <servlet-name>Rest</servlet-name>
      <servlet-class>
          com.jaxrs.vendor.JaxrsVendorServlet
      </servlet-class>
      <init-param>
          <param-name>javax.ws.rs.Application</param-name>
          <param-value>
             com.restfully.shop.services.ShoppingApplication
          </param-value>
      </init-param>
   </servlet>
   <servlet-mapping>
      <servlet-name>Rest</servlet-name>
      <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>

Questo è tutto.

Wrapping Up

It will walk you through installing JBoss RESTEasy, a JAX-RS implementation, and running the examples in this chapter within a servlet container.

Salvo diversa indicazione, il contenuto di questa pagina è sotto licenza Creative Commons Attribution-ShareAlike 3.0 License