Consuming RESTful web services in Java with Jersey and FailSafe


One of the most common tasks is to consume RESTful web services in Java, in this post I will explain a design pattern using Jersey client.

Step 1 : Configuration

The project will include the dependencies described in the following pom.xml, as you can see there we have the following support:

  • Jackson : For serialization and deserialization
  • slf4j : For logging
  • failsafe: Api for retry logic

And at the end we can see we have the maven-compiler-plugin to define the Java version we are using.

Step 2 : Creating an abstract client

The next step will be to create an AbstractClient, you can use it in any rest client you want, lets see and analyze the code:


import java.io.IOException;

import java.util.Optional;

import java.util.concurrent.TimeUnit;

import java.util.logging.Logger;

import javax.ws.rs.client.Client;

import javax.ws.rs.client.ClientBuilder;

import javax.ws.rs.client.Entity;

import javax.ws.rs.client.WebTarget;

import javax.ws.rs.core.Response;

import javax.ws.rs.core.Response.Status;

import com.fasterxml.jackson.core.JsonParseException;

import com.fasterxml.jackson.core.type.TypeReference;

import com.fasterxml.jackson.databind.JsonMappingException;

import net.jodah.failsafe.Failsafe;

import net.jodah.failsafe.RetryPolicy;

/**

 * @author raidentrance

 *

 */

public abstract class AbstractClient {

	private String url;

	private String contextPath;

	private RetryPolicy defaultRetryPolicy;

	private static final Logger log = Logger.getLogger(AbstractClient.class.getName());

	public AbstractClient(String url, String contextPath) {

		this.url = url;

		this.contextPath = contextPath;

	}

	public AbstractClient(String url, String contextPath, int maxRetries, int delay, TimeUnit unit) {

		this.url = url;

		this.contextPath = contextPath;

		defaultRetryPolicy = new RetryPolicy().retryIf((Response resp) -> {

			if (resp != null) {

				return resp.getStatusInfo().getFamily() == Status.Family.SERVER_ERROR;

			} else {

				return false;

			}

		}).withDelay(delay, unit).withMaxRetries(maxRetries);

	}

	protected WebTarget createClient(String path) {

		String assembledPath = assembleEndpoint(path);

		Client client = ClientBuilder.newClient();

		return client.target(assembledPath);

	}

	private String assembleEndpoint(String path) {

		String endpoint = url.concat(contextPath).concat(path);

		log.info(String.format("Calling endpoint %s", endpoint));

		return endpoint;

	}

	/**

	 * Execute a GET http request over the endpoint received with the content

	 * type specified

	 *

	 * @param endpoint

	 *            Defines the endpoint that will be executed

	 * @param type

	 *            Defines the content type it can be JSON,XML, etc

	 * @return A response object with the entity response and the Http status

	 */

	protected Response get(String endpoint, String type) {

		WebTarget client = createClient(endpoint);

		Optional result = getDefaultRetryPolicy();

		if (result.isPresent()) {

			return Failsafe.with(result.get()).get((response) -> client.request(type).get());

		} else {

			return client.request(type).get();

		}

	}

	/**

	 * Execute a PUT request over the endpoint received with the content type

	 * specified and sending the object received in the body

	 *

	 * @param endpoint

	 *            Endpoint will be executed

	 * @param type

	 *            Defines the content type, it can be JSON, XML, etc

	 * @param entity

	 *            Object will be sent in the body of the request

	 * @return A response object with the entity response and the Http status

	 */

	protected Response put(String endpoint, String type, Object entity) {

		WebTarget client = createClient(endpoint);

		Optional result = getDefaultRetryPolicy();

		if (result.isPresent()) {

			return Failsafe.with(result.get()).get((response) -> client.request(type).put(Entity.entity(entity, type)));

		} else {

			return client.request(type).put(Entity.entity(entity, type));

		}

	}

	/**

	 * Execute a POST request over the endpoint received with the content type

	 * specified and sending the object received in the body

	 *

	 * @param endpoint

	 *            Endpoint will be executed

	 * @param type

	 *            Defines the content type, it can be JSON, XML, etc

	 * @param entity

	 *            Object will be sent in the body of the request

	 * @return A response object with the entity response and the Http status

	 */

	protected Response post(String endpoint, String type, Object entity) {

		WebTarget client = createClient(endpoint);

		Optional result = getDefaultRetryPolicy();

		if (result.isPresent()) {

			return Failsafe.with(result.get())

					.get((response) -> client.request(type).post(Entity.entity(entity, type)));

		} else {

			return client.request(type).put(Entity.entity(entity, type));

		}

	}

	/**

	 * If there is a default retry policy it will be returned

	 *

	 * @return

	 */

	public Optional getDefaultRetryPolicy() {

		return (defaultRetryPolicy != null) ? Optional.of(defaultRetryPolicy) : Optional.empty();

	}

	/**

	 * Modify the current default retry policy

	 *

	 * @param maxRetries

	 *            Number of times that it will retry until a successful request

	 * @param delay

	 *            The time that will wait until the next attempt

	 * @param unit

	 *            Unit of the time of the delay, it can be in seconds, minutes,

	 *            etc.

	 */

	public void setDefaultRetryPolicy(int maxRetries, int delay, TimeUnit unit) {

		defaultRetryPolicy = new RetryPolicy()

				.retryIf((Response resp) -> resp.getStatusInfo().getFamily() == Status.Family.SERVER_ERROR)

				.withDelay(delay, unit).withMaxRetries(maxRetries);

	}

	/**

	 * Get a Response and Parse to type T. if the response is not 200 throw

	 * Exception

	 *

	 * @param response

	 *            the HTTP response

	 * @param entityType

	 *            is a generic type that the function will return.

	 * @param

	 *            The Generic Type that the method is returing

	 * @return T

	 * @throws Exception

	 *             if a problem occurs

	 * @throws IOException

	 * @throws JsonMappingException

	 * @throws JsonParseException

	 */

	protected abstract  T parseResponse(Response response,TypeReference entityType) throws Exception;

}

Now lets analyze the class:

  • Constructors: There are two ways to construct our object
    • Only with the url and the context path
    • Including also the maxRetries, delay and TimeUnit
  • createClient(String path) : Will create a Jersey client used to consume the api’s.

  • assembleEndpoint(String path) : Based on the Endpoint will build the url we are going to consume.

  • Response get(String endpoint, String type) : Generic get method that receives two parameters an endpoint and a type.

  • Response put(String endpoint, String type, Object entity): Generic put method that receives two parameters an endpoint and a type.

  • Response post(String endpoint, String type, Object entity): Generic post method that receives two parameters an endpoint and a type.

  • Optional getDefaultRetryPolicy() : if the parameters maxRetries, delay and TimeUnit were assigned it will return the retry policy to use.

  • setDefaultRetryPolicy(int maxRetries, int delay, TimeUnit unit): You can use it if you want to change the default policy .

  • abstract T parseResponse(Response response,TypeReference entityType) throws Exception: Your implementation has to override the parseResponse method to define how it will translate the response.

Step 3 : Define a class to set all the endpoints

Once we have our abstract class, we have to define an endpoints class, in our case it will be ApplicationEndpoints, it will centralize all the endpoints used in the client.


/**

 * @author raidentrance

 *

 */

public class ApplicationEndpoints {

	private static final String TICKER = "/ticker";

	private ApplicationEndpoints() {

	}

	public static String getTickers() {

		return TICKER;

	}

	public static String getTickerByBook(String book) {

		return TICKER.concat("?book=").concat(book);

	}

}

In this case we are using the api of bitso to get prices of crypto currencies, you can see the documentation here and query the api in the url https://api.bitso.com/v3/ticker/

Step 4 : Defining the error model

Now we have to handle errors, to do it we will analyze the api we are querying, lets see the error model:


{

    "success": false,

    "error": {

        "code": "0301",

        "message": "Unknown OrderBook xrp_mxnq"

    }

}

Now we have to translate it to java classes.

ErrorCode.java


import java.io.Serializable;

/**

 * @author raidentrance

 *

 */

public class ErrorCode implements Serializable {

	private String code;

	private String message;

	private static final long serialVersionUID = 1735206115257033120L;

	public ErrorCode() {

	}

	public ErrorCode(String code, String message) {

		super();

		this.code = code;

		this.message = message;

	}

	public String getCode() {

		return code;

	}

	public void setCode(String code) {

		this.code = code;

	}

	public String getMessage() {

		return message;

	}

	public void setMessage(String message) {

		this.message = message;

	}

}

ErrorMessage.java


import java.io.Serializable;

/**

 * @author raidentrance

 *

 */

public class ErrorMessage implements Serializable {

	private boolean success;

	private ErrorCode error;

	private static final long serialVersionUID = -8921696489057035324L;

	public ErrorMessage() {

	}

	public ErrorMessage(boolean success, ErrorCode error) {

		this.success = success;

		this.error = error;

	}

	public boolean isSuccess() {

		return success;

	}

	public void setSuccess(boolean success) {

		this.success = success;

	}

	public ErrorCode getError() {

		return error;

	}

	public void setError(ErrorCode error) {

		this.error = error;

	}

}

Once we have both classes we are able to deserialize the errors to java objects, now we just need to create an exception to propagate the errors.


import com.raidentrance.rest.error.model.ErrorMessage;

/**

 * @author raidentrance

 *

 */

public class ServiceException extends Exception {

	private ErrorMessage errorMessage;

	private static final long serialVersionUID = -7898115956660992515L;

	public ServiceException(ErrorMessage errorMessage) {

		this.errorMessage = errorMessage;

	}

	public ErrorMessage getErrorMessage() {

		return errorMessage;

	}

}

The ServiceException will be thrown when an error happens and it will contain the error message we receive from the api.

Step 5 : Creating the model

We defined the model for the errors, but now we have to define the model for our api’s, in this case we will be reading tickers with the following structure:

Payload.java


import com.fasterxml.jackson.annotation.JsonProperty;

/**

 * @author raidentrance

 *

 */

public class Payload {

	@JsonProperty("high")

	private String high;

	@JsonProperty("last")

	private String last;

	@JsonProperty("created_at")

	private String createdAt;

	@JsonProperty("book")

	private String book;

	@JsonProperty("volume")

	private String volume;

	@JsonProperty("vwap")

	private String vwap;

	@JsonProperty("low")

	private String low;

	@JsonProperty("ask")

	private String ask;

	@JsonProperty("bid")

	private String bid;

	public String getHigh() {

		return high;

	}

	public void setHigh(String high) {

		this.high = high;

	}

	public String getLast() {

		return last;

	}

	public void setLast(String last) {

		this.last = last;

	}

	public String getCreatedAt() {

		return createdAt;

	}

	public void setCreatedAt(String createdAt) {

		this.createdAt = createdAt;

	}

	public String getBook() {

		return book;

	}

	public void setBook(String book) {

		this.book = book;

	}

	public String getVolume() {

		return volume;

	}

	public void setVolume(String volume) {

		this.volume = volume;

	}

	public String getVwap() {

		return vwap;

	}

	public void setVwap(String vwap) {

		this.vwap = vwap;

	}

	public String getLow() {

		return low;

	}

	public void setLow(String low) {

		this.low = low;

	}

	public String getAsk() {

		return ask;

	}

	public void setAsk(String ask) {

		this.ask = ask;

	}

	public String getBid() {

		return bid;

	}

	public void setBid(String bid) {

		this.bid = bid;

	}

	@Override

	public String toString() {

		return "Payload [high=" + high + ", last=" + last + ", createdAt=" + createdAt + ", book=" + book + ", volume="

				+ volume + ", vwap=" + vwap + ", low=" + low + ", ask=" + ask + ", bid=" + bid + "]";

	}

}

Ticker.java


import com.fasterxml.jackson.annotation.JsonProperty;

/**

 * @author raidentrance

 *

 */

public class Ticker {

	@JsonProperty("success")

	private boolean success;

	@JsonProperty("payload")

	private Payload payload;

	public boolean isSuccess() {

		return success;

	}

	public void setSuccess(boolean success) {

		this.success = success;

	}

	public Payload getPayload() {

		return payload;

	}

	public void setPayload(Payload payload) {

		this.payload = payload;

	}

	@Override

	public String toString() {

		return "Ticker [success=" + success + ", payload=" + payload + "]";

	}

}

TickerList.java


import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;

/**

 * @author maagapi

 *

 */

public class TickerList {

	@JsonProperty("success")

	private boolean success;

	@JsonProperty("payload")

	private List payload;

	public boolean isSuccess() {

		return success;

	}

	public void setSuccess(boolean success) {

		this.success = success;

	}

	public List getPayload() {

		return payload;

	}

	public void setPayload(List payload) {

		this.payload = payload;

	}

	@Override

	public String toString() {

		return "TickerList [success=" + success + ", payload=" + payload + "]";

	}

}

Once we defined the model, we can call the api’s.

Step 6 : Creating the RestClient

The last step will be create the RestClient this class will be the responsible to join all the pieces:


import java.io.IOException;

import java.io.StringReader;

import java.util.concurrent.TimeUnit;

import javax.ws.rs.core.MediaType;

import javax.ws.rs.core.Response;

import javax.ws.rs.core.Response.Status;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.type.TypeReference;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.raidentrance.rest.commons.AbstractClient;

import com.raidentrance.rest.endpoints.ApplicationEndpoints;

import com.raidentrance.rest.error.exception.ServiceException;

import com.raidentrance.rest.error.model.ErrorCode;

import com.raidentrance.rest.error.model.ErrorMessage;

import com.raidentrance.rest.model.Ticker;

import com.raidentrance.rest.model.TickerList;

/**

 * @author raidentrance

 *

 */

public class RestClient extends AbstractClient {

	private static final Logger log = LoggerFactory.getLogger(RestClient.class);

	public RestClient(String url, String contextPath) {

		super(url, contextPath);

	}

	public RestClient(String url, String contextPath, int maxRetries, int delay, TimeUnit unit) {

		super(url, contextPath, maxRetries, delay, unit);

	}

	public TickerList getTickers() throws Exception {

		return parseResponse(get(ApplicationEndpoints.getTickers(), MediaType.APPLICATION_JSON),

				new TypeReference() {

				});

	}

	public Ticker getTickerByBook(String book) throws Exception {

		return parseResponse(get(ApplicationEndpoints.getTickerByBook(book), MediaType.APPLICATION_JSON),

				new TypeReference() {

				});

	}

	@Override

	protected  T parseResponse(Response response, TypeReference entityType) throws ServiceException {

		int status = response.getStatus();

		log.info("Status {}", status);

		if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) {

			try {

				return new ObjectMapper().readValue(new StringReader(response.readEntity(String.class)), entityType);

			} catch (IOException e) {

				throw new ServiceException(

						new ErrorMessage(false, new ErrorCode("1000", "Request couldn't be processed")));

			}

		} else {

			throw new ServiceException(response.readEntity(ErrorMessage.class));

		}

	}

}

As you can see now just with one line of code we make an http request, handle the retries and parse the response.

Step 7 : Test it

In other posts we will show how to write unit tests for this kind of components, now lets just create a main method to test it:


public static void main(String[] args) throws Exception {

		RestClient client = new RestClient("https://api.bitso.com", "/v3", 3, 3, TimeUnit.SECONDS);

		TickerList tickers = client.getTickers();

		log.info("Getting tickers ");

		for (Payload payload : tickers.getPayload()) {

			log.info(payload.toString());

		}

		log.info("Getting ripple ticker");

		Ticker ripple = client.getTickerByBook("xrp_mxn");

		log.info(ripple.toString());

		log.info("Not existing ticker");

		Ticker alex = client.getTickerByBook("alex");

		log.info(alex.toString());

	}

If we execute the code we will see the following output (It can be different according with the ripple price :P):


mar 12, 2018 5:09:02 PM com.raidentrance.rest.commons.AbstractClient assembleEndpoint

INFORMACIÓN: Calling endpoint https://api.bitso.com/v3/ticker

[main] INFO com.raidentrance.rest.RestClient - Status 200

[main] INFO com.raidentrance.rest.RestClient - Getting tickers

[main] INFO com.raidentrance.rest.RestClient - Payload [high=187000.00, last=174233.33, createdAt=2018-03-12T23:09:02+00:00, book=btc_mxn, volume=244.45021807, vwap=175921.68443739, low=167000.00, ask=174216.10, bid=173560.15]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=13957.02, last=13388.86, createdAt=2018-03-12T23:09:02+00:00, book=eth_mxn, volume=340.00979171, vwap=13198.66244190, low=13000.00, ask=13388.86, bid=13052.63]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=0.00008704, last=0.00008600, createdAt=2018-03-12T23:09:02+00:00, book=xrp_btc, volume=44561.33425456, vwap=0.00008500, low=0.00008312, ask=0.00008678, bid=0.00008601]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=15.75, last=14.92, createdAt=2018-03-12T23:09:02+00:00, book=xrp_mxn, volume=688462.85426546, vwap=15.12242330, low=14.75, ask=15.08, bid=14.92]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=0.07688000, last=0.07550000, createdAt=2018-03-12T23:09:02+00:00, book=eth_btc, volume=56.85901332, vwap=0.07569318, low=0.07430000, ask=0.07641999, bid=0.07551000]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=0.11879867, last=0.11421000, createdAt=2018-03-12T23:09:02+00:00, book=bch_btc, volume=34.25655963, vwap=0.11580909, low=0.11336268, ask=0.11589999, bid=0.11430000]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=0.02011990, last=0.01965100, createdAt=2018-03-12T23:09:02+00:00, book=ltc_btc, volume=151.10013187, vwap=0.01966700, low=0.01942300, ask=0.01990000, bid=0.01965010]

[main] INFO com.raidentrance.rest.RestClient - Payload [high=3700.00, last=3404.61, createdAt=2018-03-12T23:09:02+00:00, book=ltc_mxn, volume=2501.73845947, vwap=3466.53149080, low=3392.80, ask=3465.97, bid=3404.71]

[main] INFO com.raidentrance.rest.RestClient - Getting ripple ticker

mar 12, 2018 5:09:03 PM com.raidentrance.rest.commons.AbstractClient assembleEndpoint

INFORMACIÓN: Calling endpoint https://api.bitso.com/v3/ticker?book=xrp_mxn

[main] INFO com.raidentrance.rest.RestClient - Status 200

[main] INFO com.raidentrance.rest.RestClient - Ticker [success=true, payload=Payload [high=15.75, last=14.92, createdAt=2018-03-12T23:09:03+00:00, book=xrp_mxn, volume=688462.85426546, vwap=15.12242330, low=14.75, ask=15.08, bid=14.92]]

[main] INFO com.raidentrance.rest.RestClient - Not existing ticker

mar 12, 2018 5:09:03 PM com.raidentrance.rest.commons.AbstractClient assembleEndpoint

INFORMACIÓN: Calling endpoint https://api.bitso.com/v3/ticker?book=alex

[main] INFO com.raidentrance.rest.RestClient - Status 400

Exception in thread "main" com.raidentrance.rest.error.exception.ServiceException

	at com.raidentrance.rest.RestClient.parseResponse(RestClient.java:68)

	at com.raidentrance.rest.RestClient.getTickerByBook(RestClient.java:51)

	at com.raidentrance.rest.RestClient.main(RestClient.java:83)

As you can see we are testing the following cases:

  • Get the price of all the crypto currencies
  • Get the prices of one crypto currency
  • Ask for a price that doesn’t exist and throw an exception with the message.

You can find all the code of this post in the following repository https://github.com/raidentrance/rest-jersey-client.

If you get an SunCertPathBuilderException: unable to find valid certification path to requested target remember that you have to install the certificate to do https requests you can see a guide about how to do it here.

If you like our posts follow us in our social networks https://twitter.com/geeks_mx y https://www.facebook.com/geeksJavaMexico/.

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contact: raidentrance@gmail.com

Aprende a consumir servicios REST vía HTTPS leyendo precios de criptomonedas


Una de las tareas más comunes de un desarrollador es consumir servicios REST vía HTTPS, en este post se explicará lo siguiente:

  • Generar un certificado HTTPS
  • Instalar el certificado en la máquina virtual
  • Crear un cliente REST
  • Hacer una petición para obtener el valor de criptomonedas

Paso 1 Analizar el api a consumir

En este ejemplo leeremos información sobre criptomonedas, para hacerlo utilizaremos el api REST de Bitso (Empresa dedicada a la compra y venta de bitcoins, ripple, litecoin, entre otras criptomonedas). Para más información sobre los endpoints disponibles ver el siguiente link Documentación Bitso. En este ejemplo se consumirá un servicio para obtener el precio del bitcoin y ripple para esto se utilizará el endpoint ticker con los siguientes parámetros:

Es importante mencionar que los precios mostrados en este sitio son en pesos mexicanos.

Paso 2 Configurar el projecto

Una vez que entendemos el api que se va a consumir que conocemos los endpoints que se ejecutarán, el siguiente paso será crear un proyecto, para esto se creará un proyecto Maven e incluiremos las siguientes dependencias:

<dependencies>
	<dependency>
		<groupId>org.apache.storm</groupId>
		<artifactId>storm-core</artifactId>
		<version>1.0.1</version>
	</dependency>
	<dependency>
		<groupId>org.glassfish.jersey.core</groupId>
		<artifactId>jersey-client</artifactId>
		<version>2.17</version>
	</dependency>
	<dependency>
		<groupId>org.glassfish.jersey.media</groupId>
		<artifactId>jersey-media-json-jackson</artifactId>
		<version>2.17</version>
	</dependency>
</dependencies>

Y el siguiente plugin de Maven:

<build>
	<plugins>
		<plugin>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.2</version>
			<configuration>
				<source>1.8</source>
				<target>1.8</target>
			</configuration>
		</plugin>
	</plugins>
</build>

Con estas dependencias y este plugin tendremos la versión de Java 8 y las dependencias necesarias para trabajar con Jersey client (El api que se utilizará para consumir servicios).

Paso 3 Creando modelo de la aplicación

Un paso importante al crear un cliente REST es des serializar la respuesta a objetos java, para esto debemos crear una representación del JSON en clases Java.

Json:

{
	success: true,
	payload: {
		high: "15.13",
		last: "14.69",
		created_at: "2017-12-18T14:13:36+00:00",
		book: "xrp_mxn",
		volume: "1700513.79486861",
		vwap: "14.14839779",
		low: "14.07",
		ask: "14.69",
		bid: "14.50"
	}
}

Clases Java:

PayLoad.java


import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * @author raidentrance
 *
 */
public class Payload {

	@JsonProperty("high")
	private double high;

	@JsonProperty("last")
	private double last;

	@JsonProperty("created_at")
	private String createdAt;

	@JsonProperty("book")
	private String book;

	@JsonProperty("volume")
	private Double volume;

	@JsonProperty("vwap")
	private Double vwap;

	@JsonProperty("low")
	private Double low;

	@JsonProperty("ask")
	private Double ask;

	@JsonProperty("bid")
	private Double bid;

	public double getHigh() {
		return high;
	}

	public void setHigh(double high) {
		this.high = high;
	}

	public double getLast() {
		return last;
	}

	public void setLast(double last) {
		this.last = last;
	}

	public String getCreatedAt() {
		return createdAt;
	}

	public void setCreatedAt(String createdAt) {
		this.createdAt = createdAt;
	}

	public String getBook() {
		return book;
	}

	public void setBook(String book) {
		this.book = book;
	}

	public Double getVolume() {
		return volume;
	}

	public void setVolume(Double volume) {
		this.volume = volume;
	}

	public Double getVwap() {
		return vwap;
	}

	public void setVwap(Double vwap) {
		this.vwap = vwap;
	}

	public Double getLow() {
		return low;
	}

	public void setLow(Double low) {
		this.low = low;
	}

	public Double getAsk() {
		return ask;
	}

	public void setAsk(Double ask) {
		this.ask = ask;
	}

	public Double getBid() {
		return bid;
	}

	public void setBid(Double bid) {
		this.bid = bid;
	}

	@Override
	public String toString() {
		return "Payload [high=" + high + ", last=" + last + ", createdAt=" + createdAt + ", book=" + book + ", volume="
				+ volume + ", vwap=" + vwap + ", low=" + low + ", ask=" + ask + ", bid=" + bid + "]";
	}

}

CoinPrice.java


/**
 * @author raidentrance
 *
 */
public class CoinPrice {
	private boolean success;
	private Payload payload;

	public boolean isSuccess() {
		return success;
	}

	public void setSuccess(boolean success) {
		this.success = success;
	}

	public Payload getPayload() {
		return payload;
	}

	public void setPayload(Payload payload) {
		this.payload = payload;
	}

	@Override
	public String toString() {
		return "RipplePrice [success=" + success + ", payload=" + payload + "]";
	}

}

Paso 4 Creando el cliente REST

El siguiente paso es empezar a escribir código, lo primero que escribiremos será un AbstractClient, que servirá para dar soporte para escribir multiples clientes REST:

AbstractClient.java


import java.util.logging.Logger;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;

/**
 * @author raidentrance
 *
 */
public class AbstractClient {
	private String url;
	private String contextPath;

	private static final Logger log = Logger.getLogger(AbstractClient.class.getName());

	public AbstractClient(String url, String contextPath) {
		this.url = url;
		this.contextPath = contextPath;
	}

	protected WebTarget createClient(String path) {
		String assembledPath = assembleEndpoint(path);
		Client client = ClientBuilder.newClient();
		WebTarget target = client.target(assembledPath);
		return target;
	}

	private String assembleEndpoint(String path) {
		String endpoint = url.concat(contextPath).concat(path);
		log.info(String.format("Calling endpoint %s", endpoint));
		return endpoint;
	}
}

La clase AbstractClient será la clase base que se utilizará para todos los clientes que creemos, cuenta con dos métodos principales:

  • assembleEndpoint(String path) : Construye la url a ejecutar
  • WebTarget createClient(String path) : Crea el cliente HTTP para invocar la petición REST, en caso de requerir autenticar la petición este es el lugar para hacerlo.

ApplicationEndpoint.java


/**
 * @author raidentrance
 *
 */
public class ApplicationEndpoint {
	private static String TICKER = "/ticker";

	public static String getCoinPrice(String coin) {
		return TICKER.concat(String.format("?book=%s", coin));
	}
}

La clase ApplicationEndpoint se utiliza para definir los endpoints a ejecutar en la aplicación.

ServiceException.java


/**
 * @author raidentrance
 *
 */
public class ServiceException extends Exception {
	private Integer httpStatusCode;

	private static final long serialVersionUID = -1873750263916403862L;

	public ServiceException(String message, Integer httpStatusCode) {
		super(message);
		this.httpStatusCode = httpStatusCode;
	}

	public Integer getHttpStatusCode() {
		return httpStatusCode;
	}

	public void setHttpStatusCode(Integer httpStatusCode) {
		this.httpStatusCode = httpStatusCode;
	}
}

La clase ServiceException se utilizará para propagar los errores en caso de que existan.
BitsoClient


import java.util.logging.Logger;

import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import com.raidentrance.client.endpoints.ApplicationEndpoint;
import com.raidentrance.client.error.ServiceException;
import com.raidentrance.client.model.CoinPrice;

/**
 * @author raidentrance
 *
 */
public class BitsoClient extends AbstractClient {
	private static final Logger log = Logger.getLogger(BitsoClient.class.getName());

	public BitsoClient(String url, String contextPath) {
		super(url, contextPath);
	}

	public CoinPrice getRipplePrice() throws ServiceException {
		log.info("Getting ripple price");
		WebTarget client = createClient(ApplicationEndpoint.getCoinPrice("xrp_mxn"));
		Response response = client.request(MediaType.APPLICATION_JSON).get();
		log.info("Status " + response.getStatus());
		CoinPrice result = null;
		Integer status = response.getStatus();
		if (Status.OK.getStatusCode() == status) {
			result = response.readEntity(CoinPrice.class);
		} else {
			throw new ServiceException(response.readEntity(String.class), status);
		}
		return result;
	}

	public CoinPrice getBitcoinPrice() throws ServiceException {
		log.info("Getting ripple price");
		WebTarget client = createClient(ApplicationEndpoint.getCoinPrice("btc_mxn"));
		Response response = client.request(MediaType.APPLICATION_JSON).get();
		log.info("Status " + response.getStatus());
		CoinPrice result = null;
		Integer status = response.getStatus();
		if (Status.OK.getStatusCode() == status) {
			result = response.readEntity(CoinPrice.class);
		} else {
			throw new ServiceException(response.readEntity(String.class), status);
		}
		return result;
	}

}

Ahora es tiempo de crear el cliente REST en este caso será la clase BitsoClient como se puede ver recibe los siguientes parámetros en el constructor:

  • url : Representa la url a consumir
  • contextPath : Representa la url base de los servicios

También cuenta con 2 métodos:

  • getRipplePrice() : Como su nombre lo indica devuelve un objeto con el precio actual de la criptomoneda ripple.
  • getBitcoinPrice() : Como su nombre lo indica devuelve un objeto con el precio actual de la criptomoneda bitcoin.

TestClient.java


import com.raidentrance.client.error.ServiceException;
import com.raidentrance.client.model.CoinPrice;

/**
 * @author raidentrance
 *
 */
public class TestClient {
	public static void main(String[] args) throws ServiceException {
		BitsoClient client = new BitsoClient("https://api.bitso.com/", "v3");
		CoinPrice ripplePrice = client.getRipplePrice();
		CoinPrice bitcoin = client.getBitcoinPrice();
		System.out.println(String.format("Ripple price %s",ripplePrice.toString()));
		System.out.println(String.format("Bitcoin price %s",bitcoin.toString()));
	}
}

La clase TestClient se utilizará para mostrar en consola los valores obtenidos por las API’s REST.

Ejecutando la aplicación

Si ejecutamos la aplicación y no contamos con el certificado para ejecutar la petición recibiremos el siguiente error:

SunCertPathBuilderException: unable to find valid certification path to requested target

Así que utilizaremos una clase llamada InstallCert.java creada por el equipo de sun que puedes encontrar aquí, ejecutarla pasando como parámetro la url de la cual deseas extraer el certificado con el siguiente comando :

java InstallCert api.bitso.com

El cuál mostrará la siguiente salida:

Opening connection to api.bitso.com:443...
Starting SSL handshake...

No errors, certificate is already trusted

Server sent 3 certificate(s):

 1 Subject CN=ssl511101.cloudflaressl.com, OU=PositiveSSL Multi-Domain, OU=Domain Control Validated
   Issuer  CN=COMODO ECC Domain Validation Secure Server CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB
   sha1    11 bd 1f 03 0a b4 07 8c d3 f1 49 16 f2 97 0a 9f 97 07 55 cd
   md5     33 1e 1d 7f 95 09 de 0b 0b b8 31 9d f2 48 85 b8

 2 Subject CN=COMODO ECC Domain Validation Secure Server CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB
   Issuer  CN=COMODO ECC Certification Authority, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB
   sha1    75 cf d9 bc 5c ef a1 04 ec c1 08 2d 77 e6 33 92 cc ba 52 91
   md5     5e 0e 41 9b 20 ea 57 54 77 f1 1b 52 e2 c8 18 e0

 3 Subject CN=COMODO ECC Certification Authority, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB
   Issuer  CN=AddTrust External CA Root, OU=AddTrust External TTP Network, O=AddTrust AB, C=SE
   sha1    ae 22 3c bf 20 19 1b 40 d7 ff b4 ea 57 01 b6 5f dc 68 a1 ca
   md5     c7 90 a5 6c 69 cb af 0b f3 f3 0a 40 d0 a2 ae cc

<strong>Enter certificate to add to trusted keystore or 'q' to quit: [1]</strong>

Oprimiremos la tecla de 1 y enter.

Ejecutaremos de nuevo el comando, seleccionaremos 1 de nuevo y notaremos que la salida es ahora la siguiente:

Added certificate to keystore 'jssecacerts' using alias 'api.bitso.com-1'

Este comando generará un archivo llamado jssecacerts, el último paso será copiar ese archivo al directorio $JAVA_HOME\jre\lib\security y listo, tu aplicación podrá hacer peticiones https a el dominio de bitso.

El último paso será ejecutar nuestra aplicación para validar que todo funciona bien y generará la siguiente salida:

dic 18, 2017 10:37:04 AM com.raidentrance.client.BitsoClient getRipplePrice
INFORMACIÓN: Getting ripple price
dic 18, 2017 10:37:04 AM com.raidentrance.client.AbstractClient assembleEndpoint
INFORMACIÓN: Calling endpoint https://api.bitso.com/v3/ticker?book=xrp_mxn
dic 18, 2017 10:37:05 AM com.raidentrance.client.BitsoClient getRipplePrice
INFORMACIÓN: Status 200
dic 18, 2017 10:37:05 AM com.raidentrance.client.BitsoClient getBitcoinPrice
INFORMACIÓN: Getting ripple price
dic 18, 2017 10:37:05 AM com.raidentrance.client.AbstractClient assembleEndpoint
INFORMACIÓN: Calling endpoint https://api.bitso.com/v3/ticker?book=btc_mxn
dic 18, 2017 10:37:06 AM com.raidentrance.client.BitsoClient getBitcoinPrice
INFORMACIÓN: Status 200
<strong>Ripple price RipplePrice [success=true, payload=Payload [high=15.13, last=14.67, createdAt=2017-12-18T16:37:02+00:00, book=xrp_mxn, volume=1908408.5657127, vwap=14.20595744, low=14.07, ask=14.64, bid=14.52]]
Bitcoin price RipplePrice [success=true, payload=Payload [high=382000.0, last=374994.16, createdAt=2017-12-18T16:37:03+00:00, book=btc_mxn, volume=301.0574943, vwap=373301.92134721, low=362505.0, ask=375800.0, bid=374994.16]]</strong>

Como se puede ver se imprimieron los precios de las criptomonedas de forma exitosa, ya dependerá de ti la aplicación que crees con esto.

Conclusión

En este post se tocaron temas importantes como:

  • Configurar un proyecto para hacer peticiones REST con Jersey
  • Crear un patrón de diseño para la construcción de clientes REST
  • Ejecutar peticiones REST vía HTTPS
  • Obtener el valor en pesos de las criptomonedas Bitcoin y Ripple

Si te gusta el contenido y quieres enterarte cuando realicemos un post nuevo síguenos en nuestras redes sociales https://twitter.com/geeks_mx y https://www.facebook.com/geeksJavaMexico/.

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com