martes, 28 de abril de 2015

Proyectos Gradle con múltiples módulos

Introducción

Al igual que Maven, Gradle tiene soporte para proyectos con múltiples módulos (multi-modulemulti-project). Esto permite tener un conjunto de configuraciones y librerías comunes, compartidas entre varios proyectos relacionados. Esto puede usarse tanto en aplicaciones Java como en librerías de clases o aplicaciones Web, inclusive en proyectos que no son Java.

Para demostrar esta funcionalidad se desarrollarán unos proyectos muy simples:
  • common: tendrá la clase "Util" con un método que imprimirá en consola una cadena indicada como parámetro, intercambiando las mayúsculas y las minúsculas.
  • hello-world: tendrá la clase "Example" con un método "main" usando la clase "Util" para imprimir el texto "Hello World".
  • hola-mundo: tendrá la clase "Example" con un método "main" usando la clase "Util" para imprimir el texto "Hola Mundo". 
En este escenario el proyecto "common" será una librería de clases Java, la cual será usada por los proyectos "hello-world" y "hola-mundo" para imprimir textos de manera "estandarizada".

El proyecto completo se podrá descargar desde: https://github.com/guillermo-varela/gradle-multi-project-example

Proyecto Raíz/Padre (Root/Parent)

Este es el proyecto que contiene a los demás sub-proyectos (o módulos). Para empezar se creará la siguiente estructura de carpetas y archivos:
gradle-multi-project-example
..build.gradle
..gradle.properties
..settings.gradle
..common/
..hello-world/
..hola-mundo/

Las carpetas "common", "hello-world" y "hola-mundo" corresponden a los sub-proyectos, mientras que los demás archivos tendrán la configuración de Gradle común a todos estos.

Archivo build.gradle

plugins {
  id 'net.researchgate.release' version '2.0.2'
}

subprojects {
    apply plugin: 'java'

    project(':hello-world') {
        apply plugin: 'application'
    }

    project(':hola-mundo') {
        apply plugin: 'application'
    }

    sourceCompatibility = 1.7
    targetCompatibility = 1.7

    repositories {
        mavenCentral()
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

Al principio se indica que todo el proyecto usará el plugin release para automatizar el versionamiento, como se indicó en un post anterior.

Luego se tiene la sección "subprojects" en la cual se indican las configuraciones y tareas que se aplicarán a los sub-proyectos. En este caso se indica que todos serán proyectos Java, compilando con el JDK 1.7 y usarán el repositorio Maven Central. También se indica que solamente los proyectos "hello-world" y "hola-mundo" serán aplicaciones ejecutables. Esto es preferible que se encuentre dentro de la configuración de Gradle de cada sub-proyecto, pero se dejó así para demostrar cómo aplicar configuraciones diferenciadas según el proyecto.

Ya al final del archivo se indica que se usará el Wrapper de Gradle en su versión 2.3.

Arhivo gradle.properties

version=1.0.0-SNAPSHOT
Nota: es importante notar que todos los sub-proyectos usarán la misma versión que se indica en el proyecto raíz, dado que de momento el plugin release no soporta tener diferentes números de versión: https://github.com/researchgate/gradle-release#multi-project-builds

Archivo settings.gradle

include ":hello-world", ":hola-mundo", ":common"

En este archivo se indican cuales son los sub-proyectos a tener en cuenta.

Proyecto common

Cuando se usan múltiples proyectos con Gradle no es obligatorio tener un archivo de configuración en todos los proyectos, si toda la configuración necesaria está en el proyecto raíz. Sin embargo en este caso se tendrá la identificación del proyecto en el JAR generado y una dependencia.

Archivo build.gradle

jar {
    manifest {
        attributes 'Implementation-Title': 'Gradle Multi-Project Example - Common', 'Implementation-Version': version
    }
}

dependencies {
    compile 'org.apache.commons:commons-lang3:3.3.2'
}

Archivo Util.java

package com.blogspot.nombre_temp.gradle.multi_project.common;

import org.apache.commons.lang3.StringUtils;

public class Util {
  public static void println(String text) {
    System.out.println(StringUtils.swapCase(text));
  }
}

Esta clase (ubicada en la ruta "gradle-multi-project-example/common/src/main/java/com/blogspot/nombre_temp/gradle/multi_project/common/Util.java") es la que usarán los otros proyectos para imprimir cadenas.

Proyecto hello-world

Archivo build.gradle

mainClassName = 'com.blogspot.nombre_temp.gradle.multi_project.hello_world.Example'

jar {
    manifest {
        attributes 'Implementation-Title': 'Gradle Multi-Project Example - Hello World', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

dependencies {
    compile project(':common')
}

Dado que buena parte de la configuración de Gradle está en el proyecto raíz, aquí solamente se indica cual será la clase con el método "main" (punto de entrada de la aplicación) y se indica que se tendrá como dependencia el proyecto "common". Nótese que para declarar la dependencia con ese proyecto se usó "compile project(':common')" en lugar de una nombre completo de un repositorio Maven como en otros casos. Adicionalmente no fue necesario incluir manualmente la dependencia de Apache Commons Lang, ya que esta se trae por trasitividad al tener la dependencia con "common".

Nota: de ser posible la única relación entre los sub-proyectos en las configuración de Gradle debe ser a nivel de dependencias, no de configuraciones (por ejemplo que un proyecto no use o modifique tareas de Gradle de otro proyecto), esto con el fin de mantener los proyectos lo menos acoplados posible.

Archivo Example.java

package com.blogspot.nombre_temp.gradle.multi_project.hello_world;

import com.blogspot.nombre_temp.gradle.multi_project.common.Util;

public class Example {
  public static void main(String[] args) {
    Util.println("Hello World");
  }
}

Como se explicaba inicialmente, esta clase (ubicada en la ruta "gradle-multi-project-example/hello-world/src/main/java/com/blogspot/nombre_temp/gradle/multi_project/hello_world/Example.java") contiene el método "main" y usa la clase "Util" para imprimir la cadena "Hello World".

Proyecto hola-mundo

Es casi idéntico al proyecto "hello-world", pero imprimiendo "Hola Mundo".

Archivo build.gradle

mainClassName = 'com.blogspot.nombre_temp.gradle.multi_project.hola_mundo.Example'

jar {
    manifest {
        attributes 'Implementation-Title': 'Gradle Multi-Project Example - Hola Mundo', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

dependencies {
    compile project(':common')
}

Archivo Example.java

package com.blogspot.nombre_temp.gradle.multi_project.hola_mundo;

import com.blogspot.nombre_temp.gradle.multi_project.common.Util;

public class Example {
  public static void main(String[] args) {
    Util.println("Hola Mundo");
  }
}

Construcción y Ejecución

Para construir todos los proyectos, basta con abrir una instancia de Terminal (línea de comandos) y ejecutar el comando "gradle wrapper" para inicializar el Wrapper de Gradle y luego "./gradlew build" (o "gradlew.bat build" si se está usando Windows).
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:common:assemble
:common:compileTestJava UP-TO-DATE
:common:processTestResources UP-TO-DATE
:common:testClasses UP-TO-DATE
:common:test UP-TO-DATE
:common:check UP-TO-DATE
:common:build

:hello-world:compileJava
:hello-world:processResources UP-TO-DATE
:hello-world:classes
:hello-world:jar
:hello-world:startScripts
:hello-world:distTar
:hello-world:distZip
:hello-world:assemble
:hello-world:compileTestJava UP-TO-DATE
:hello-world:processTestResources UP-TO-DATE
:hello-world:testClasses UP-TO-DATE
:hello-world:test UP-TO-DATE
:hello-world:check UP-TO-DATE
:hello-world:build

:hola-mundo:compileJava
:hola-mundo:processResources UP-TO-DATE
:hola-mundo:classes
:hola-mundo:jar
:hola-mundo:startScripts
:hola-mundo:distTar
:hola-mundo:distZip
:hola-mundo:assemble
:hola-mundo:compileTestJava UP-TO-DATE
:hola-mundo:processTestResources UP-TO-DATE
:hola-mundo:testClasses UP-TO-DATE
:hola-mundo:test UP-TO-DATE
:hola-mundo:check UP-TO-DATE
:hola-mundo:build

BUILD SUCCESSFUL

Total time: 5.544 secs

Como puede verse los tres proyectos se compilaron y se construyeron según las configuraciones realizadas. En el caso de "hello-world" y "hola-mundo" se generaron también los archivos comprimidos con los ejecutables de cada aplicación.

Para ejecutar las aplicaciones se puede usar el comando "./gradlew run", también desde el proyecto raíz:
:common:compileJava UP-TO-DATE
:common:processResources UP-TO-DATE
:common:classes UP-TO-DATE
:common:jar UP-TO-DATE

:hello-world:compileJava UP-TO-DATE
:hello-world:processResources UP-TO-DATE
:hello-world:classes UP-TO-DATE
:hello-world:run
hELLO wORLD

:hola-mundo:compileJava UP-TO-DATE
:hola-mundo:processResources UP-TO-DATE
:hola-mundo:classes UP-TO-DATE
:hola-mundo:run
hOLA mUNDO

BUILD SUCCESSFUL

Total time: 4.303 secs

El resultado es que primero se compila el proyecto "common" (en este caso aparece "UP-TO-DATE" ya que no fue necesario generar el JAR porque se acabó de ejecutar build) ya que se tiene la dependencia hacia este y posteriormente se ejecutan los proyectos "hello-world" y "hola-mundo" (se ejecutan en orden alfanumérico).

Referencias

http://gradle.org/docs/current/userguide/multi_project_builds.html

Más sobre Gradle

http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

lunes, 13 de abril de 2015

Simplificando REST usando Retrofit

Retrofit

En un post anterior se habló de la librería OkHttp y como esta podía usarse como un potente y eficiente cliente HTTP, el cual inclusive permite realizar llamados asíncronos. Sin embargo, su uso está más enfocado a operaciones bajo nivel. En esta ocasión se hablará de Retrofit, una librería desarrollada por la misma compañía (Square Open Source) la cual facilita bastante el acceso a APIs REST, tanto JSON como XML, mediante el uso de interfaces y anotaciones, evitando la construcción y manipulación manual de peticiones HTTP.

Retrofit no depende de OkHttp, por defecto usará los componentes nativos de Java para los llamados HTTP. Sólo usará OkHttp si encuentra la librería en el classpath de la aplicación. Dado que anteriormente se mencionaron e ilustraron algunas de las ventajas de OkHttp, se incluirá esta librería.

Este cliente REST es completamente configurable, a tal punto que es posible adicionar interceptores para manipular headers en cada petición, realizar autenticación Basic o mediante OAuth, etc. Algunas de estas características se pueden encontrar en la página web de Retrofit: http://square.github.io/retrofit

Por defecto, esta librería realiza la serialización de JSON mediante la librería de Google Gson, sin embargo quienes prefieran usar Jackson, pueden hacerlo simplemente indicando que se usará el conversor que el equipo de Square desarrolló para ello: https://github.com/square/retrofit/tree/master/retrofit-converters

En esta ocasión se trabajarán dos ejemplos muy sencillos, ya que el gran poder que tiene Retrofit (en mi opinión) es la facilidad de su uso, lo cual es lo que se quiere demostrar en este post.

Ejemplo sencillo: consulta del clima

Para este ejemplo se consultará el servicio OpenWeatherMap el cual provee un API REST gratuito para consultar el estado del clima en las ciudades del mundo.

La siguiente consulta retornará el estado del clima en la ciudad de Londres:
http://api.openweathermap.org/data/2.5/weather?id=2643743
{  
  "coord":{  
    "lon":-0.13,
    "lat":51.51
  },
  "sys":{  
    "message":0.0344,
    "country":"GB",
    "sunrise":1428901694,
    "sunset":1428951224
  },
  "weather":[  
    {  
      "id":800,
      "main":"Clear",
      "description":"Sky is Clear",
      "icon":"01n"
    }
  ],
  "base":"stations",
  "main":{  
    "temp":276.272,
    "temp_min":276.272,
    "temp_max":276.272,
    "pressure":1037.14,
    "sea_level":1045.23,
    "grnd_level":1037.14,
    "humidity":87
  },
  "wind":{  
    "speed":1.11,
    "deg":10.0094
  },
  "clouds":{  
    "all":0
  },
  "dt":1428899601,
  "id":2643743,
  "name":"London",
  "cod":200
}

El ejemplo completo se puede encontrar en: https://github.com/guillermo-varela/retrofit-example

Lo primero entonces será crear el archivo de configuración de Gradle con las librerías necesarias.

Nota: quienes aún no conozcan del todo Gradle pueden consultar: http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

build.gradle

plugins {
  id 'net.researchgate.release' version '2.0.2'
}

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'eclipse'

sourceCompatibility = 1.7
targetCompatibility = 1.7

mainClassName = 'com.blogspot.nombre_temp.retrofit.example.Example'

jar {
    manifest {
        attributes 'Implementation-Title': 'OkHttp Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'com.squareup.okhttp:okhttp:2.3.0'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0'
    compile 'org.apache.commons:commons-lang3:3.4'
}

Como puede verse, además de las dependencias de Retrofit y OkHttp se agregaron también:

  • okhttp-urlconnection: permite la integración entre OkHttp y Retrofit
  • Apache Commons Lang3: Simplemente se agregó para facilitar el desarrollo de métodos "toString" que se usarán en este ejemplo. No se requiere para el funcionamiento de Retrofit.
Teniendo en cuenta la estructura de la respuesta JSON que retorna el servicio del clima, se procederá a desarrollar una clase con algunos de los atributos necesarios para almacenar esta información. Retrofit usará Gson para convertir el JSON que retorna el API en una instancia de esta clase. Es de notar que la clase no tiene todos los atributos que vienen en la respuesta JSON, esto se debe a que no es necesario tener todos los datos y se dejan solamente algunos para efectos del ejemplo:
package com.blogspot.nombre_temp.retrofit.example.model;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import com.google.gson.annotations.SerializedName;

public class WeatherData {

  private Main main;
  private Wind wind;

  public Main getMain() {
    return main;
  }
  public void setMain(Main main) {
    this.main = main;
  }
  public Wind getWind() {
    return wind;
  }
  public void setWind(Wind wind) {
    this.wind = wind;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }

  public static class Main {
    private float temp;
    @SerializedName("temp_min")
    private float tempMin;
    @SerializedName("temp_max")
    private float tempMax;
    private float pressure;
    private float humidity;

    public float getTemp() {
      return temp;
    }
    public void setTemp(float temp) {
      this.temp = temp;
    }
    public float getTempMin() {
      return tempMin;
    }
    public void setTempMin(float tempMin) {
      this.tempMin = tempMin;
    }
    public float getTempMax() {
      return tempMax;
    }
    public void setTempMax(float tempMax) {
      this.tempMax = tempMax;
    }
    public float getPressure() {
      return pressure;
    }
    public void setPressure(float pressure) {
      this.pressure = pressure;
    }
    public float getHumidity() {
      return humidity;
    }
    public void setHumidity(float humidity) {
      this.humidity = humidity;
    }
    @Override
    public String toString() {
      return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
  }

  public static class Wind {
    private float speed;
    private float deg;

    public float getSpeed() {
      return speed;
    }
    public void setSpeed(float speed) {
      this.speed = speed;
    }
    public float getDeg() {
      return deg;
    }
    public void setDeg(float deg) {
      this.deg = deg;
    }
    @Override
    public String toString() {
      return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
  }
}

La anotación "@SerializedName" se usa para indicar a Gson el nombre del dato que encontrará dentro del JSON.

A continuación se desarollará la interfaz Java con las operaciones del API REST que se usarán:
package com.blogspot.nombre_temp.retrofit.example.api;

import retrofit.Callback;
import retrofit.http.GET;
import retrofit.http.Query;

import com.blogspot.nombre_temp.retrofit.example.model.WeatherData;

public interface WeatherApi {

  @GET("/weather")
  WeatherData getWeather(@Query("id") long cityId);

  @GET("/weather")
  void getWeatherAsync(@Query("id") long cityId, Callback<Weatherdata> callback);
}

  • Método getWeather: Se indica mediante la anotación "@GET" que al llamar este método se realizará una petición GET a la operación "/weather" del API REST. Con la anotación "@Query" se está indicando que en la URL se debe agregar el parámetro "id" con el valor indicado en el parámetro "cityId", es decir si este método se llama con el parámetro "123" el llamado al API del clima será: http://api.openweathermap.org/data/2.5/weather?id=123. El JSON que retorne dicho llamado será convertido a una instancia de la clase "WeatherData".
  • Método getWeatherAsync: Es muy similar al anterior, con la diferencia de que este no retorna un resultado, ya que se trata de un llamado asíncrono. El procesamiento de la respuesta se dará en la instancia del objeto "Callback" que se indica en el segundo parámetro. Es de aclarar que no es la misma interfaz "com.squareup.okhttp.Callback" que se usó en el ejemplo de OkHttp, pero es el mismo principio.
Finalmente se tiene la clase que hace los llamados al API usando Retrofit:
package com.blogspot.nombre_temp.retrofit.example;

import java.util.Date;

import org.apache.commons.lang3.time.DateFormatUtils;

import retrofit.Callback;
import retrofit.RestAdapter;
import retrofit.RestAdapter.LogLevel;
import retrofit.RetrofitError;
import retrofit.client.Response;

import com.blogspot.nombre_temp.retrofit.example.api.WeatherApi;
import com.blogspot.nombre_temp.retrofit.example.model.WeatherData;

public class Example {

  private static final RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("http://api.openweathermap.org/data/2.5")
    .setLogLevel(LogLevel.FULL)
    .build();

  private static final WeatherApi weatherService = restAdapter.create(WeatherApi.class);

  private static void print(String message) {
    System.out.println(message + ": " + DateFormatUtils.format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.sss"));
  }

  public static void main(String[] args) {
    print("Starting main method");
    long londonId = 2643743L;

    WeatherData weatherData = weatherService.getWeather(londonId);
    print(String.format("Sync response [%s]", weatherData));

    weatherService.getWeatherAsync(londonId, new Callback<Weatherdata>() {
      @Override
      public void success(WeatherData weatherData, Response response) {
        print(String.format("Async response [%s]", weatherData));
      }
      @Override
      public void failure(RetrofitError error) {
        error.printStackTrace();
      }
    });

    print("Ending main method");
  }
}
  • Líneas 18-21: Se crea un "RestAdapter", el cual contiene las propiedades que tendrá el cliente REST a usar. En este caso se está indicando que use "LogLevel.FULL" sólo para propósitos de la demostración.
  • Línea 23: Se crea una instancia de la interfaz que se creó previamente con la definición de las operaciones del API. Es importante notar que en el código desarrollado no se hizo una clase que implementara dicha interfaz y definiera el cuerpo de sus métodos, esta es una tarea que Retrofit se encarga de hacer por nosotros.
  • Líneas 25-27: Definición de un método para que al imprimir un texto por consola se le adicione la fecha y hora actual. Esto con el fin de comprobar el comportamiento de las peticiones HTTP.
  • Líneas 29-48: Es el método main de la aplicación y allí se realiza:
    • Líneas 33-34: Llamado síncrono al API del clima y se imprime el resultado.
    • Líneas 36-45: Llamado asíncrono al API del clima. Se define también la clase anónima del tipo "Callback" en la cual se define cómo procesar la respuesta obtenida (método success) y cómo procesar un posible error en el llamado (método failure). Al igual que sucede con el ejemplo del post anterior sobre OkHttp, tanto el llamado HTTP al API REST, como el código dentro de la instancia de "Callback" se realizan en un hilo separado, es decir no se bloquea la ejecución del método main.
Al ejecutar esta clase se obtiene un resultado como el siguiente:
Starting main method: 2015-04-13T01:13:45.045
---> HTTP GET http://api.openweathermap.org/data/2.5/weather?id=2643743
---> END HTTP (no body)
<--- HTTP 200 http://api.openweathermap.org/data/2.5/weather?id=2643743 (735ms)
Server: nginx
Date: Mon, 13 Apr 2015 06:13:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Source: redis
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1428905625799
OkHttp-Received-Millis: 1428905625933

{"coord":{"lon":-0.13,"lat":51.51},"sys":{"message":0.0169,"country":"GB","sunrise":1428901694,"sunset":1428951224},"weather":[{"id":800,"main":"Clear","description":"Sky is Clear","icon":"01d"}],"base":"stations","main":{"temp":276.272,"temp_min":276.272,"temp_max":276.272,"pressure":1037.14,"sea_level":1045.23,"grnd_level":1037.14,"humidity":87},"wind":{"speed":1.11,"deg":10.0094},"clouds":{"all":0},"dt":1428904512,"id":2643743,"name":"London","cod":200}

<--- END HTTP (461-byte body)
Sync response [WeatherData[main=WeatherData.Main[temp=276.272,tempMin=276.272,tempMax=276.272,pressure=1037.14,humidity=87.0],wind=WeatherData.Wind[speed=1.11,deg=10.0094]]]: 2015-04-13T01:13:45.045
Ending main method: 2015-04-13T01:13:46.046
---> HTTP GET http://api.openweathermap.org/data/2.5/weather?id=2643743
---> END HTTP (no body)
<--- HTTP 200 http://api.openweathermap.org/data/2.5/weather?id=2643743 (122ms)
Server: nginx
Date: Mon, 13 Apr 2015 06:13:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Source: redis
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1428905626004
OkHttp-Received-Millis: 1428905626126

{"coord":{"lon":-0.13,"lat":51.51},"sys":{"message":0.0169,"country":"GB","sunrise":1428901694,"sunset":1428951224},"weather":[{"id":800,"main":"Clear","description":"Sky is Clear","icon":"01d"}],"base":"stations","main":{"temp":276.272,"temp_min":276.272,"temp_max":276.272,"pressure":1037.14,"sea_level":1045.23,"grnd_level":1037.14,"humidity":87},"wind":{"speed":1.11,"deg":10.0094},"clouds":{"all":0},"dt":1428904512,"id":2643743,"name":"London","cod":200}

<--- END HTTP (461-byte body)
Async response [WeatherData[main=WeatherData.Main[temp=276.272,tempMin=276.272,tempMax=276.272,pressure=1037.14,humidity=87.0],wind=WeatherData.Wind[speed=1.11,deg=10.0094]]]: 2015-04-13T01:13:46.046

Inicialmente se ve que se imprime la petición y la respuesta del llamado síncrono (letras verdes) y es sólo cuando termina dicho llamado que termina de ejecutarse el método main (letras azules). Ya después de terminado el método main es que aparece la petición y la respuesta del llamado asíncrono, con lo cual comprobamos que dicho llamado no ha bloqueado la ejecución de la aplicación, sino que se ha realizado en un hilo aparte.

Puede comprobarse también al comparar las respuestas en JSON y las representaciones String de los objetos "WeatherData" (resaltado en amarillo) que la conversión se ha realizado correctamente, sin que tampoco se haya tenido que procesar manualmente el JSON de la respuesta de API.

Nota: la aplicación termina de ejecutarse aproximadamente un minuto después de procesar la respuesta del llamado asíncrono, por la espera de 60 segundos para descartar los hilos del pool que se explicaba al final del post anterior sobre OkHttp.

Conclusión

Como puede verse en el resultado de la ejecución usando Retrofit (y dejando que el nivel de log sea FULL) ha sido la propia librería la encargada de generar todo el procesamiento JSON y HTTP requerido para consumir el API REST de una manera muy sencilla, simplemente definiendo una interfaz Java con anotaciones, inclusive de manera asíncrona sin tener que manipular hilos tampoco.

Referencias

http://square.github.io/retrofit
https://github.com/square/retrofit
http://openweathermap.org/api

sábado, 11 de abril de 2015

Peticiones HTTP asíncronas con OkHttp

OkHttp

Es una librería de código abierto la cual permite realizar operaciones tanto en HTTP como en SPDY de manera sencilla y eficiente en ambientes Java (versión 1.7 como mínimo) y Android (2.3 como mínimo), sin necesidad de cambiar el código de la aplicación entre ambas plataformas, con una interfaz fluida.

Entre las virtudes que tiene esta librería es que automáticamente evalúa las características de la conexión y determina cómo operar de mejor manera, por ejemplo si se tiene soporte para SPDY, contenido HTTP comprimido (GZIP), recuperación de algunos errores en la conexión, entre otras, la cuales se pueden encontrar en su página principal: http://square.github.io/okhttp

Internamente está usando Okio, el cual permite optimizar las operaciones I/O mediante "Non-blocking I/O" (NIO).

Actualmente se encuentra en la versión 2.3.0 y como puede verse en su registro de Maven Central, es un proyecto en activo desarrollo: http://mvnrepository.com/artifact/com.squareup.okhttp/okhttp

Ejemplo Simple

OkHttp permite realizar todas las operaciones HTTP. En este caso se realizará una petición GET, para demostrar lo sencillo que es usar esta librería:
import java.io.IOException;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class Example {

  private static final OkHttpClient client = new OkHttpClient();

  public static void main(String[] args) throws IOException {
    Request request = new Request.Builder()
      .url("http://www.textfiles.com/art/sunlogo.txt")
      .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) {
      throw new IOException("Error in the response: " + response);
    }

    System.out.println(response.body().string());
  }
}

Esto imprimirá en la consola el resultado de una petición GET a la URL "http://www.textfiles.com/art/sunlogo.txt", lo cual será un logo algo nostálgico.

En la documentación de la librería se pueden encontrar algunos ejemplos un poco más avanzados: https://github.com/square/okhttp/wiki/Recipes

Ejemplo Asíncrono

Como se mencionaba inicialmente, OkHttp permite realizar operaciones HTTP de manera asíncrona y es en este caso en el cual se desarrollará un ejemplo un poco más completo, para explicar y demostrar mejor esta característica.

Para este ejemplo se desarrollará una aplicación Java, la cual realice una petición HTTP, la cual demorará 10 segundos en responder. La idea es que el hilo principal (main en este caso) continúe su ejecución a pesar de que no se ha obtenido la respuesta de la petición (HTTP).

Se usará la librería MockServer para subir un servicio local, el cual tenga el retraso (delay) que se necesita para este caso.

El ejemplo completo se puede encontrar en: https://github.com/guillermo-varela/OkHttp-example

Lo primero entonces será crear el archivo de configuración de Gradle con las librerías necesarias.

Nota: quienes aún no conozcan del todo Gradle pueden consultar: http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

build.gradle

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'eclipse'

sourceCompatibility = 1.7
targetCompatibility = 1.7
version = '1.0.0'

mainClassName = 'example.Example'

jar {
    manifest {
        attributes 'Implementation-Title': 'OkHttp Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.squareup.okhttp:okhttp:2.3.0'
    compile 'org.mock-server:mockserver-netty:3.9.6'
}

A continuación se tiene la clase con el código del servidor local y el petición asíncrona, junto con la explicación de lo que se hizo, aunque se tratará que el código sea lo más auto-documentado posible:
package example;

import java.io.IOException;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.time.DateFormatUtils;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.Delay;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;

import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

public class Example {

  private static final OkHttpClient client = new OkHttpClient();
  private static final ClientAndServer mockServer = ClientAndServer.startClientAndServer(8080);

  static {
    mockServer.when(
        HttpRequest.request()
          .withPath("/test")
        )
        .respond(
            HttpResponse.response()
              .withBody("OK")
              .withDelay(new Delay(TimeUnit.SECONDS, 10))
        );

    client.setConnectTimeout(15, TimeUnit.SECONDS);
    client.setWriteTimeout(15, TimeUnit.SECONDS);
    client.setReadTimeout(15, TimeUnit.SECONDS);
  }

  private static void print(String message) {
    System.out.println(message + ": " + DateFormatUtils.format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.sss"));
  }

  private static void doRequest() {
    print("Starting doRequest method");

    Request request = new Request.Builder()
      .url("http://localhost:8080/test")
      .build();

    client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Request request, IOException e) {
        e.printStackTrace();
        stopMockServer();
      }

      @Override
      public void onResponse(Response response) throws IOException {
        print("Response [" + response.body().string() + "]");
        stopMockServer();
      }

      private void stopMockServer() {
        print("Stopping MockServer");
        if (mockServer != null) {
          mockServer.stop();
        }
      }
    });

    print("Ending doRequest method");
  }

  public static void main(String[] args) {
    print("Starting main method");
    doRequest();
    print("Ending main method");
  }
}
  • Línea 20: Se crea el cliente HTTP de la librería OkHttp. Este cliente puede ser compartido para todos los hilos de la aplicación, siempre y cuando no se cambie su configuración (timeouts, interceptores, etc.)
  • Línea 21: Se crea e inicia el servidor local de MockServer en el puerto 8080.
  • Líneas 24-32: Dentro del bloque estático, se configura MockServer para que todas las peticiones a "http://localhost:8080/test" tengan el texto "OK" y se produzca con un demora de 10 segundos.
  • Líneas 34-36: Se indica al cliente HTTP que tenga timeouts superiores al delay recién configurado.
  • Líneas 39-41: Definición de un método para que al imprimir un texto por consola se le adicione la fecha y hora actual. Esto con el fin de comprobar el comportamiento asíncrono de la petición HTTP.
  • Líneas 43-72: Es el método "doRequest" y es el que realiza la petición asíncrona usando el cliente HTTP de OkHttp (OkHttpClient).
    • Líneas 46-48: Se indica que la petición se hará a la URL "http://localhost:8080/test".
    • Línea 50: Se solicita al cliente HTTP (variable client) que cree un nuevo llamado usando el método "newCall", pero en lugar de ejecutarlo usando el método "execute" como se hizo en el primer ejemplo, se indica que se agregue a la cola de llamados asíncronos usando el método "enqueue", es decir no se está ejecutando el llamado HTTP, sino que se está programando para su futura ejecución, por lo cual es una operación que no bloquea la ejecución. Normalmente la ejecución del llamado HTTP se inicia inmediatamente, pero si hay otros llamados en espera de ejecución puede tomar un tiempo.
    • Líneas 51-69: Para indicar qué hacer cuando se obtenga la respuesta del llamado HTTP, se crea una clase anónima de la interfaz "Callback" en la cual se definen operaciones tanto en caso de obtener la respuesta (onResponse), como en caso de error (onFailure). En caso de que no se necesite la respuesta, estos métodos pueden quedar vacíos. Adicionalmente se desarrolló un método extra "stopMockServer" el cual detiene la ejecución del servidor local MockServer, y se invoca tanto caso de éxito como de error.
  • Líneas 74-78: Es el método main de la aplicación y allí simplemente se indica que se debe ejecutar el método "doRequest".
Al ejecutar esta clase se obtiene un resultado como el siguiente:

Starting main method: 2015-04-11T22:22:11.011
Starting doRequest method: 2015-04-11T22:22:11.011
Ending doRequest method: 2015-04-11T22:22:11.011
Ending main method: 2015-04-11T22:22:11.011
Response [OK]: 2015-04-11T22:22:22.022
Stopping MockServer: 2015-04-11T22:22:22.022

Como puede verse, la ejecución del método main (y del método doRequest) se realizó en menos de un milisegundo. Internamente OkHttp creó un hilo (dentro de un pool de hilos) y fue allí donde se procesó el llamado HTTP y se ejecutó el callback desarrollado.

Una manera adicional de comprobarlo es adicionando puntos de interrupción (breakpoints), uno al final del método "main" y otro en el método "onResponse" del callback:
Figura 1 - Hilo adicional para los llamados HTTP

De esta manera se puede, por ejemplo informar a un sistema externo que un usuario de nuestra aplicación a realizado alguna acción, sin generar demoras adicionales en la respuesta al usuario por dicho llamado. Esto sin necesidad de manipular manualmente hilos dentro del código de la aplicación.

Notas adicionales sobre los hilos de OkHttp

Referencias

http://square.github.io/okhttp
https://github.com/square/okhttp
https://github.com/square/okhttp/wiki/Recipes
http://square.github.io/okhttp/javadoc
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
http://www.mock-server.com

jueves, 9 de abril de 2015

Gradle - Versionamiento Automático

Introducción

En el post anterior se mostró cómo se puede configurar un proyecto (muy simple) para que use Gradle como herramienta de construcción. En esta ocasión se mostrará cómo usar Gradle para automatizar el versionamiento de los proyectos.

Se partirá usando como base el proyecto desarrollado en aquel post anterior, el cual se puede descargar desde: https://github.com/guillermo-varela/gradle-example/releases/tag/1.0.0

Quienes prefieran ver el resultado final, lo podrán descargar desde: https://github.com/guillermo-varela/gradle-example/releases/tag/1.0.1

Repositorio Maven

Tanto en los proyectos corporativos de las organizaciones como en proyectos de código abierto es relativamente común que los proyectos compilados y construidos (JAR, WAR, EAR, etc.) se almacenen en un repositorio Maven no solamente para tener el proyecto versionado, sino también en el caso de los proyectos que son librerías, para que puedan incluirse fácilmente en otros proyectos, indicando la versión específica que se quiere.

Para este ejemplo se usará una instalación local de Nexus, pero los conceptos pueden aplicarse a repositorios remotos, corporativos o públicos. Una vez descargado Nexus, para instalarlo se debe descomprimir el archivo obtenido en la carpeta de preferencia y dentro de la sub-carpeta "bin" ejecutar el comando que corresponda según el sistema operativo:

Linux/Mac: nexus install
Windows: nexus.bat install

Nota: En Windows los comandos de Nexus deben ejecutarse desde una línea de comandos como administrador.

Para iniciar Nexus se ejecuta el siguiente comando:

Linux/Mac: nexus start
Windows: nexus.bat start

Una vez iniciado, se puede acceder a la interfaz web mediante la URL: http://localhost:8081/nexus

Las credenciales por defecto como administrador son:
Usuario: admin
Contraseña: admin123

Figura 1 - Instalación local de Nexus

Gradle Maven

Es el plugin de Gradle que se usará para automatizar la publicación de las versiones del proyecto en el repositorio de Maven (el Nexus que se acabó de instalar).

Para usarlo, lo primero que se debe hacer es incluirlo y aplicarlo en el archivo de configuración del Gradle "build.gradle", agregando la siguiente instrucción:

apply plugin: 'maven'

Anteriormente se había visto cómo es posible crear propiedades dentro del mismo archivo "build.gradle", las cuales quedan disponibles como variables para el resto del archivo, como por ejemplo:

version = 1.0.0

En esta ocasión se creará el archivo "gradle.properties" en el cual se declararán algunas de estas propiedades y quedarán disponibles para usarlas dentro de "build.gradle":

version=1.0.1-SNAPSHOT
group=com.blogspot.nombre-temp

Aquí se incrementó la versión y se adicionó el sufijo "-SNAPSHOT", el cual es un estándar de Maven para indicar que es una versión que aún se encuentra en desarrollo y no hace parte de un "release" definitivo. Como su nombre en inglés lo indica, puede entenderse como la "foto" del proyecto en un determinado momento, a la cual Nexus le concatenará automáticamente la fecha en que se creó.

También se añadió la propiedad "group" la cual corresponderá al "groupId" del proyecto de Maven, mientras que "artifactId" será el mismo nombre del proyecto por defecto.

Ahora se deben indicar lo repositorios Maven a los que se deben subir las versiones, tanto "SNAPSHOT" como "RELEASE" (versiones definitivas) mediante la siguiente tarea de Gradle:
uploadArchives {
    repositories {
        mavenDeployer {
            snapshotRepository(url: "http://localhost:8081/nexus/content/repositories/snapshots/") {
                authentication(userName: "admin", password: "admin123")
            }
            repository(url: "http://localhost:8081/nexus/content/repositories/releases/") {
                authentication(userName: "admin", password: "admin123")
            }
        }
    }
}

Nota: Se usan los repositorios "snapshots" y "releases" porque ya vienen en la instalación de Nexus, sin embargo pueden crearse repositorios dentro de Nexus con cualquier nombre.

En este punto ya es posible ejecutar manualmente la nueva tarea de Gradle y subir el proyecto a Nexus. Si se deja en la versión el sufijo "-SNAPSHOT", el proyecto se subirá automáticamente al repositorio "snapshots", de lo contrario se usará "releases" por defecto. Para esto se usará el comando "gradlew uploadArchives":

gradlew uploadArchives

:compileJava
:processResources UP-TO-DATE
:classes
:jar
:startScripts
:distTar
:distZip
:uploadArchives
Uploading: com/blogspot/nombre-temp/gradle-example/1.0.1-SNAPSHOT/gradle-example-1.0.1-20150408.053732-1.jar to repository remote at http://localhost:8081/nexus/content/repositories/snapshots/
Transferring 1K from remote
Uploaded 1K
Uploading: com/blogspot/nombre-temp/gradle-example/1.0.1-SNAPSHOT/gradle-example-1.0.1-20150408.053732-1.zip to repository remote at http://localhost:8081/nexus/content/repositories/snapshots/
Transferring 377K from remote
Uploaded 377K
Uploading: com/blogspot/nombre-temp/gradle-example/1.0.1-SNAPSHOT/gradle-example-1.0.1-20150408.053732-1.tar to repository remote at http://localhost:8081/nexus/content/repositories/snapshots/
Transferring 420K from remote
Uploaded 420K

BUILD SUCCESSFUL

Total time: 15.108 secs

Figura 2 - Proyecto versionado en Nexus

Como puede verse en la figura 2, en Nexus quedan almacenados los archivos de la construcción del proyecto (JAR, ZIP, TAR) con la versión indicada (1.0.1-SNAPSHOT).

Repositorio Git

Para quienes tenga el proyecto desarrollado desde cero localmente, y no hayan creado un repositorio de Git, es un buen momento para crear uno. No necesariamente se debe crear un repositorio remoto en servidores como GitHub o Bitbucket, un repositorio local sirve para los propósitos de este post, y de hecho es una buena práctica en el desarrollo de software, ya que permite versionar los proyectos y devolver cambios más fácilmente.

Quienes sean completamente nuevos usando Git o sistemas de control de versiones en general, pueden revisar el siguiente tutorial, el cual no solamente explica cómo funciona sino también el porqué es un tema tan importante: http://readwrite.com/2013/09/30/understanding-github-a-journey-for-beginners-part-1

Para crear el repositorio local de Git lo único que se necesita es ejecutar desde la raíz del proyecto el siguiente comando:

git init
Initialized empty Git repository

Luego se debe modificar (o crear) el archivo ".gitignore", también en la raíz del proyecto:

.gradle/
build/

Estas dos líneas indican que las carpetas ".gradle" y "build" no se incluirán en el repositorio de código. En general no se recomienda incluir en los repositorios carpetas y archivos que se generen como parte del proceso de compilación/construcción del proyecto, mas no como parte del código como tal, ya que estos pueden generarse desde el equipo en el cual se esté trabajando, ahorrando espacio en el servidor remoto (en caso de tenerlo).

Un artículo que explica cómo se pueden determinar los archivos a incluir en un repositorio de código se puede encontrar en: http://blog.jooq.org/2014/09/08/look-no-further-the-final-answer-to-where-to-put-generated-code

Para indicar que los demás archivos se deben incluir en el repositorio se ejecuta el siguiente comando, incluyendo el punto del final:

git add .

Y finalmente para efectivamente incluir los archivos en el repositorio se ejecuta:

git commit -m "First commit."

Para marcar versiones definitivas (release) del proyecto en el repositorio Git se usará la funcionalidad de Tagging, la cual permite marcar un punto en la historia del repositorio como importante y por cada Tag creado crea una copia del estado del proyecto en ese momento y permite su descarga específica. Por ejemplo, quienes usaron el enlace para descargar el proyecto desde GitHub, pudieron ver que se está referenciando el tag "1.0.0".

Manualmente puede crearse un tag con el nombre "1.0.0" y el comentario "First tag" mediante el siguiente comando:

git tag -a "1.0.0"

Los tags creados en un repositorio pueden listarse mediante el comando:
git tag -l
1.0.0

Automatización del Versionamiento

Gradle-Release es el plugin que permite automatizar el versionamiento del código del proyecto en repositorios como Git, SVN, Mercurial, etc. Adicionalmente puede integrarse con el plugin de Maven anteriormente mencionado para que dicho versionamiento también sea automatizado.

Para usarlo, lo primero que se debe hacer es incluirlo y aplicarlo en el archivo de configuración del Gradle "build.gradle". Esto se debe hacer al principio del archivo y la sintaxis dependerá de la versión de Gradle que se esté usando:

Gradle 2.0 ó anterior:
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'net.researchgate:gradle-release:2.0.2'
    }
}
apply plugin: 'net.researchgate.release'

Gradle 2.1 ó posterior:
plugins {
  id 'net.researchgate.release' version '2.0.2'
}

Dado que en este ejemplo el proyecto está usando el Wrapper de Gradle con la versión 2.3, se asumirá que se usa la última sintaxis.

Para integrar este plugin con el de Maven, basta indicar que la creación de la versión "Release" en el repositorio de código (el tag) dependerá de que se haga lo mismo en Maven, relacionando las tareas de Gradle mediante la siguiente instrucción al final del archivo "build.gradle":

createReleaseTag.dependsOn uploadArchives

Este plugin se encarga de verificar, compilar, construir, versionar y publicar el proyecto por nosotros mediante el comando "gradlew release", sin embargo si se ejecuta en este momento el resultado tendrá un error:

gradlew release

:release
:gradle-example:initScmPlugin
:gradle-example:checkCommitNeeded FAILED
:release FAILED
Release process failed, reverting back any changes made by Release Plugin.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':checkCommitNeeded'.
> You have unversioned files:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
?? gradle.properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 7.419 secs

Básicamente se está indicando que se tienen cambios en el proyecto que aún no han sido incluidos en el repositorio de Git (commit). Este plugin dispone de múltiples configuraciones que permiten alterar su funcionamiento y en este caso se podría crear la propiedad "failOnCommitNeeded" con el valor "false" para que permita crear la nueva versión sin tener todos los cambios en el repositorio.

En este caso simplemente se incluirán los nuevos archivos (add), se guardarán los cambios (commit) y se publicarán en el repositorio remoto de Git (push) en caso de usar uno:

git add .
git commit -m "Maven and release plugins"
git push

Una vez hecho esto, ya debe ser posible la generación del nuevo release:

gradlew release
:release
:gradle-example:initScmPlugin
:gradle-example:checkCommitNeeded
:gradle-example:checkUpdateNeeded
:gradle-example:unSnapshotVersion
> Building 0% > :release > :gradle-example:confirmReleaseVersion
??> This release version: [1.0.1]
:gradle-example:confirmReleaseVersion
:gradle-example:checkSnapshotDependencies
:gradle-example:compileJava UP-TO-DATE
:gradle-example:processResources UP-TO-DATE
:gradle-example:classes UP-TO-DATE
:gradle-example:jar
:gradle-example:startScripts
:gradle-example:distTar
:gradle-example:distZip
:gradle-example:assemble
:gradle-example:compileTestJava UP-TO-DATE
:gradle-example:processTestResources UP-TO-DATE
:gradle-example:testClasses UP-TO-DATE
:gradle-example:test UP-TO-DATE
:gradle-example:check UP-TO-DATE
:gradle-example:build
> Building 0% > :release > :gradle-example:preTagCommit
Username for 'https://github.com': XXX
Password for 'https://XXX@github.com':
:gradle-example:preTagCommit
:gradle-example:uploadArchives
Uploading: com/blogspot/nombre-temp/gradle-example/1.0.1/gradle-example-1.0.1.jar to repository remote at http://localhost:8081/nexus/content/repositories/releases/
Transferring 1K from remote
Uploaded 1K
Uploading: com/blogspot/nombre-temp/gradle-example/1.0.1/gradle-example-1.0.1.tar to repository remote at http://localhost:8081/nexus/content/repositories/releases/
Transferring 420K from remote
Uploaded 420K
Uploading: com/blogspot/nombre-temp/gradle-example/1.0.1/gradle-example-1.0.1.zip to repository remote at http://localhost:8081/nexus/content/repositories/releases/
Transferring 377K from remote
Uploaded 377K
> Building 0% > :release > :gradle-example:createReleaseTag
Username for 'https://github.com': XXX
Password for 'https://XXX@github.com':
:gradle-example:createReleaseTag
> Building 0% > :release > :gradle-example
:updateVersion
??> Enter the next version (current one released as [1.0.1]): [1.0.2-SNAPSHOT]
:gradle-example:updateVersion
> Building 0% > :release > :gradle-example:commitNewVersion
Username for 'https://github.com': XXX
Password for 'https://XXX@github.com':
:gradle-example:commitNewVersion

BUILD SUCCESSFUL

Total time: 1 mins 12.978 secs

Han sucedido varias cosas con la ejecución de este comando:

  1. El plugin "Gradle Release" primero tomó el número la versión del proyecto indicado en el archivo "gradle.properties", automáticamente le quitó el sufijo "-SNAPSHOT" (ya que estamos generando una versión release) y pidió confirmar si esta es la versión a generar.
  2. Compilación, construcción y generación los archivos ejecutables del proyecto.
  3. Genera un commit en el repositorio Git.
  4. Dado que en este caso se estaba usando un repositorio Git remoto (GitHub), se solicitaron las credenciales para subir los cambios (push).
  5. Se cargaron los archivos de distribución (JAR, TAR, ZIP) en el repositorio "releases" de Nexus con la versión generada (1.0.1).

    Figura 3 - Versión release en Nexus

  6. Creación del tag en el repositorio Git. El tag usa como nombre la versión del proyecto. Nuevamente, como en este caso se tiene un repositorio remoto, se solicitaron las credenciales y se hizo el push.
    Figura 4 - Tags en GitHub

  7. Se presentó una sugerencia para el siguiente número de versión del proyecto y se pidió confirmarlo. Es de notar que el plugin automáticamente volvió a adicionar el sufijo "-SNAPSHOT",  ya que la versión release ya fue generada y publicada.
  8. Nuevo commit y push en el repositorio Git, dejando el proyecto listo para continuar su desarrollo.

En resumen, solamente ejecutando el comando "gradlew release" se ha podido generar una versión nueva del proyecto, publicando los archivos necesarios en los repositorios Maven y Git.

Como dato adicional, para quienes consideren que la manera de incrementar el número de la versión que usa el plugin es suficiente y no quieren confirmar la versión cada vez (o están usando un servidor de integración continua), pueden ejecutar lo siguiente:
gradlew release -Pgradle.release.useAutomaticVersion=true

O también indicarse desde el principio las versiones:
gradlew release -Pgradle.release.useAutomaticVersion=true -PreleaseVersion=1.0.0 -PnewVersion=1.1.0-SNAPSHOT


Para tener en cuenta

Quizás algunos piensan que se ha realizado demasiado trabajo para un proyecto tan pequeño. En primer lugar, un proyecto real es mucho más grande y complejo que el ejemplo presentado para estos posts, el cual se ha mantenido muy simple para centrar mejor la atención en los temas de Gradle, y a medida que los proyectos son más grandes se hacen más complejos de administrar, sin embargo los archivos de configuración de Gradle varían muy poco, por lo que es un esfuerzo de una sola vez, que en el mediano/largo plazo ahorra mucho trabajo.

Referencias

http://gradle.org/docs/current/userguide/maven_plugin.html
https://github.com/researchgate/gradle-release
http://readwrite.com/2013/09/30/understanding-github-a-journey-for-beginners-part-1
https://books.sonatype.com/nexus-book/reference/install.html
http://blog.jooq.org/2014/09/08/look-no-further-the-final-answer-to-where-to-put-generated-code

Más sobre Gradle