domingo, 12 de abril de 2015

Patron para conectar con un backend Restful

Es tipico de las aplicaciones de empresa la conectividad con alguno o normalmente varios servicios web. Normalmente el estandar para servicios actualmente es el Rest.  Lo que sigue es una manera de organizar el codigo para que nuestro intercambio con el backend este ordenado. No se si corresponde llamarlo patron aunque de hecho yo lo utilizo como una estructura fija en mi codigo.


En todo intercambio de datos con el backend podemos encontrar codigo que obedece a la conexiones propiamente dichas (POST, GET, headers, etc.) y codigo que permite wrapear el intercambio (JSON o XML) como objetos java.

Objetos de dominio para intercambio JSON

Un ejemplo de codigo que permite wrapear JSON en objetos java lo tenemos a continuacion:

 public class User extends DataObject implements Serializable  {  
      private static final long serialVersionUID = 1L;  
      @Expose  
      public String firstName;  
      @Expose  
      public String lastName;  
      @Expose  
      public String email;  
      @Expose  
      public String phone;  
      public User(){  
      }  
 }  

En este caso tenemos un objeto User que podriamos estar enviando al backend luego del proceso de registracion.
Tambien podriamos manejar grupos de objetos. A continuacion tenemos un objeto de tipo Contact que luego recibiremos como una array de objetos JSON:

   public class Contact extends DataObject implements Serializable {   
      private static final long serialVersionUID = 1L;   
        @Expose   
        public String contactName;   
        @Expose   
        public String phoneNumber;   
  }   

Y para recibir un conjunto de objetos Contact usariamos una clase como:

 public class Contacts extends DataObject {  
   public Contacts(){  
   }  
   @Expose  
   public int totalCount;  
   @Expose  
   public int pageCount;  
   @Expose  
   public int pageSize;  
   @Expose  
   public int pageIndex;  
   @Expose  
   public List<Contact> results = new ArrayList<Contact>();  
 }  

La anotacion @Expose permite incluir o excluir un campo de los procesos de serializacion y deserializacion que proporciona la clase Gson. Esta entrada no pretende ser una guia exahustiva y ni siquiera introductoria sobre GSON.  Pero si deseamos incluir un campo en ambos procesos de serializacion y deserializacion utilizaremos la anotacion @Expose. Esta anotacion admite los siguientes parametros:


@Expose (serialize = true/false, deserialize = true/false)


Que permite anular alguno o ambos procesos para un campo dado. Obviamente una anotacion de tipo @Expose (serialize = false, deserialize = false) es equivalente a no anotar con @Expose un campo. Esto es el resultado sera el mismo (el campo sera ignorado).


Para deserializar un objeto JSON conteniendo datos del usuario instanciariamos el objeto Gson y luego su metodo fromJson:


 Gson gson = new Gson();  
 User user = gson.fromJson((String) result, User.class);  

Resulta conveniente para mantener nuestro codigo de conexion generico hacer extender estos objetos de una superclase con algun metodo helper.

 public class DataObject {  
      public DataObject() {  
      }   
      public List<Integer> extractArrayIntegers(String json) throws JSONException {  
           JSONArray array = new JSONArray(json);  
           List<Integer> extractedInts = new ArrayList<Integer>(array.length());  
           for (int i = 0; i < array.length(); i++) {  
                extractedInts.add(array.getInt(i));  
           }  
           return extractedInts;  
      }  
      public List<String> extractArrayStrings(String json) throws JSONException {  
           JSONArray array = new JSONArray(json);  
           List<String> extractedStrings = new ArrayList<String>(array.length());  
           for (int i = 0; i < array.length(); i++) {  
                extractedStrings.add(array.getString(i));  
           }  
           return extractedStrings;  
      }  
 }  

Los metodos helper en este caso sirven para obtener listas de String e Integer para integrar a nuestro codigo donde lo necesitemos.


Clases cliente

Siempre que conectamos con servicios necesitamos que la clase que efectua la llamada reciba el resultado, sea este el objeto de datos buscado o la identificacion del problema que impidio hacernos con los datos.

Una manera efectiva de hacer esto es creando interfaces que las clases clientes deben implementar para recibir el resultado. Es decir, que crearemos interfaces que definiran metodos callback donde el objeto encargado de la conexion podra eventualmente volcar el resultado (Datos, Excepciones, etc.):


 public class Interfaces {  
   public abstract interface APIUserback {  
     public void apiDidFinish(Response response, User user);  
   }  
   public abstract interface APIContactsCallback {  
     public void apiDidFinish(Response response, Contacts contacts);  
   }  
 }  

   Segun vemos en el codigo los metodos a implementar en las clases clientes recibiran (en este caso) dos parametros, un objeto Response que contendra los detalles de la conexion y objetos de tipo User y de tipo Contacts que contendran los datos propios que se han ido a buscar o null en el caso de que haya habido problemas en la conexion.

Clase de conexion

Resulta util centralizar todas las conexiones al backend en una sola clase. Esta clase contendria metodos como los que siguen, que permiten conectar y obtener resultados de una manera generica. El siguiente metodo encapsula el resultado de la conexion en un objeto Response que permitira mantener informado al usuario o tomar opciones alternativas:

   private Response response(boolean success, String jsonResponse,  
                int statusCode, String error, String service, String location) {  
     Response response = new Response();  
     response.success = success;  
     response.statusCode = statusCode;  
     response.jsonResponse = jsonResponse;  
     response.errorCode = error;  
     response.location = location;  
     response.generateDisplayMessage(ctx);  
     response.logDetails();  
     return response;  
   }  
 }  

   El objeto Response podria ser algo parecido a:

 public class Response extends DataObject {  
      public boolean success;  
      public int statusCode;  
      public String errorCode;  
      public String displayMessage;  
      public String jsonResponse;  
      public String location;  
      public Response() {  
           super();  
      }  
      public void generateDisplayMessage(Context ctx) {  
           if(statusCode == 409){  
                displayMessage = ctx.getResources().getString(R.string.account_already_exists);  
           }  
      }  
 }  


Donde vemos que podemos agregar un metodo que genere un mensaje de alto nivel para mantener informado al cliente del problema que haya ocurrido.

Luego el metodo que finalmente hace la conexion podria verse como sigue:


      public Response execute(URL url, String method, String service,  
                DataObject dataObject, boolean getRedirectLocation,  
                boolean followRedirects) {  
           String responseString = "";  
           String responseMessage = "";  
           String location = "";  
           int statusCode = 0;  
           boolean success = false;  
           HttpEntity entity;  
           HttpResponse httpResp = null;  
           DefaultHttpClient httpClient;  
           HttpParams httpParameters = new BasicHttpParams();  
           HttpConnectionParams.setConnectionTimeout(httpParameters,  
                     Constants.TIMEOUT_CONNECTION);  
           HttpConnectionParams.setSoTimeout(httpParameters,  
                     Constants.TIMEOUT_CONNECTION);  
           httpClient = getNewHttpClient(url.getProtocol(), httpParameters);  
           if (!followRedirects) {  
                HttpParams params = httpClient.getParams();  
                HttpClientParams.setRedirecting(params, false);  
           }  
           try {  
                if (method.equals("GET")) {// GET  
                     HttpGet httpGet = new HttpGet(url.toURI());  
                     for (Map.Entry<String, String> entry : this.headers(service)  
                               .entrySet()) {  
                          httpGet.addHeader(entry.getKey(), entry.getValue());  
                     }  
                     httpResp = httpClient.execute(httpGet);  
                     entity = httpResp.getEntity();  
                     if (entity != null) {  
                          InputStream inputStream = entity.getContent();  
                          responseString = API.convertStreamToString(inputStream);  
                     }  
                     if (httpResp.getStatusLine().getStatusCode() < 400) {  
                          success = true;  
                     }  
                } else if ((method.equals("POST") && dataObject != null)) {// POST  
                     HttpPost httpPost = null;  
                     httpPost = new HttpPost(url.toURI());  
                     for (Map.Entry<String, String> entry : this.headers(service)  
                               .entrySet()) {  
                          RWLog.v("POST header---> " + entry.getKey() + ": "  
                                    + entry.getValue());  
                          httpPost.addHeader(entry.getKey(), entry.getValue());  
                     }  
                     entity = new StringEntity(dataObject.toJsonString(), "UTF-8");  
                     httpPost.setEntity(entity);  
                     httpResp = httpClient.execute(httpPost);  
                     InputStream inputStream = httpResp.getEntity().getContent();  
                     responseString = API.convertStreamToString(inputStream);  
                     if (httpResp.getStatusLine().getStatusCode() < 400) {  
                          success = true;  
                     }  
                } else if ((method.equals("DELETE"))) {  
                     HttpDelete httpDelete = new HttpDelete(url.toURI());  
                     for (Map.Entry<String, String> entry : this.headers(service)  
                               .entrySet()) {  
                          httpDelete.addHeader(entry.getKey(), entry.getValue());  
                     }  
                     httpResp = httpClient.execute(httpDelete);  
                     entity = httpResp.getEntity();  
                     if (entity != null) {  
                          InputStream inputStream = entity.getContent();  
                          responseString = API.convertStreamToString(inputStream);  
                     }  
                     if (httpResp.getStatusLine().getStatusCode() < 400) {  
                          success = true;  
                     }  
                }  
                responseMessage = httpResp.getStatusLine().getReasonPhrase();  
                statusCode = httpResp.getStatusLine().getStatusCode();  
                if (responseString.length() > 1) {  
                     JSONObject jsonError = new JSONObject(responseString);  
                     responseMessage = jsonError.getString("errorCode");  
                }  
                if (getRedirectLocation) {  
                     Header header = httpResp.getFirstHeader(("Location"));  
                     if (header != null) {  
                          location = header.getValue();  
                     }  
                }  
           } catch (NoHttpResponseException nhre) {  
                statusCode = OFFLINE_ERROR;  
                nhre.printStackTrace();  
           } catch (ConnectTimeoutException cte) {  
                statusCode = TIMEOUT_ERROR;  
                cte.printStackTrace();  
           } catch (IOException ioe) {  
                if (ioe.getMessage().equals("No authentication challenges found")) {  
                     statusCode = 401;  
                }  
                try {  
                     if (responseString.length() > 1) {  
                          // getErrorStream() does not exist in HttpClient, supposedly  
                          // the error stream will be be in the response body in case  
                          // of error, but this is something to check  
                          JSONObject jsonError = new JSONObject(responseString);  
                          responseMessage = jsonError.getString("errorCode");  
                     }  
                } catch (JSONException jse) {  
                     jse.printStackTrace();  
                }  
           } catch (JSONException jse) {  
                jse.printStackTrace();  
           } catch (URISyntaxException e) {  
                e.printStackTrace();  
           }  
           Response r = response(success, responseString, statusCode,  
                     responseMessage, service, location);  
           return r;  
      }  

Aqui se utiliza el framework de Apache HTTPClient, pero se puede implementar facilmente con HTTPUrlConnection si se lo desea.


Finalmente la llamada a este metodo podriamos colocarla en un metodo como el siguiente:

   public void contacts(final int pageSize, final int pageIndex) {  
     new Thread(new Runnable() {  
       @Override  
       public void run() {  
         String service = "contacts";  
         Map<String, String> parameters = new HashMap<String, String>();  
         parameters.put("pageSize", Integer.toString(pageSize));  
         parameters.put("pageIndex", Integer.toString(pageIndex));  
         Response response = execute(urlForService(service, parameters),  
             "GET", service, null, false, false);  
         try {  
           Contacts responseDataObject = new Contacts();  
           if (response.success) {  
             if (response.statusCode == 200) {  
               responseDataObject = new Gson().fromJson(  
                   response.jsonResponse, Contacts.class);  
             } else if (response.statusCode == 204) {  
               // just init to an empty list if empty  
               responseDataObject.results = new ArrayList<Contact>();  
             }  
           }  
           apiContactsCallback.apiDidFinish(response,  
               responseDataObject);  
         } catch (Exception e) {  
           e.printStackTrace();  
         }  
       }  
     }).start();  
   }  

Sintetizando, que podriamos crear una clase API que tuviera los metodos:

public void contacts(final int pageSize, final int pageIndex)


public Response execute(URL url, String method, String service,  
                DataObject dataObject, boolean getRedirectLocation,  
                boolean followRedirects)

y
private Response response(boolean success, String jsonResponse,  
                int statusCode, String error, String service, String location)


Y a la cual llamariamos desde nuestras Activity con:

     API api = new API(this);  
     api.setContactsCallback(new Interfaces.APIContactsCallback() {  
       @Override  
       public void apiDidFinish(final Response response, final Contacts contacts) {  
         contactsAdapter = new ContactsAdapter(MyContactsActivity.this, contacts);  
         runOnUiThread(new Runnable() {  
           @Override  
           public void run() {  
             if(response.statusCode == 403){  
                 ............. Y AQUI COMUNICARIAMOS AL USUARIO LO QUE TOQUE  


En nuestra clase API agregariamos entonces todos los metodos que requieran conectar con el backend y todas las clases clientes implementarian una interface de tipo callback que recibiria los resultados. De esta manera todo nuestro codigo de conexion con el backend quedaria integrado en una sola clase (API). Representando las Actividades con cajas, las clases con circulos y los metodos con puntos de enlace entre clases quedaria algo como:


Bueno, se ma hecho larga esta primer entrada. En la proxima si lo solicitan agrego un ejemplo en codigo para que se bajen. Saludos y espero vuestros comentarios para mejorar la entrada.


Soy desarrollador Android, profesional. A veces cuando me encuentro con algo que nunca hice antes, o cuando encuentro una mejor manera de hacer algo que ya hice antes, suelo reenviarme la solucion por correo con algunas pocas notas. Este blog es la version 2.1 de esa misma idea. Es tener un lugar donde dejarlas y donde ademas de a mi les pueda servir a otros.

Las notas son un mix entre cosas que descubri por mi mismo, cosas interesantes que encontre manteniendo codigo ajeno y cosas que saque del arbol de la ciencia del bien y del mal. Y no tienen una hilacion tematica entre ellas, mas que su pertenencia al mundo de la programacion Android. No forman parte de un curso ni tiene sentido leerlas en secuencia.
Pero espero que les sirvan, como me sirvieron a mi ;-)