JavaRush /Java Blog /Random EN /A guide to creating a client for the Skyscanner API and p...

A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1]

Published in the Random EN group

Content:

A guide to creating a client for the Skyscanner API and publishing it to jCenver and Maven Central [Part 2] The idea of ​​​​developing the client as a separate library came at the moment when I wrote the article Creating a ticket price monitoring system: a step-by-step guide ( part 1 , part 2 , part 3 ). A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 1Why is this needed? For example, so that you can simply add it as a dependency to the project, not think about how and what to do, but simply use the created API. For the article you need to have an idea of ​​​​what is:
  • Gradle project build system. Last time we used Maven, this time we will use Gradle for publishing. For a quick reference, this article will suffice .
  • groupId, artifactId, version. This is for a client post.

Action plan

The Skyscanner API has four groups of requests:
  • Live flight search
  • Places
  • Browse Flight Prices
  • localization
So the idea is to write a client with four interfaces to work with these groups, which only requires you to pass a token to work with the Rapid API and the necessary data for the request, and the client takes care of everything else. The benefit of this project is really tangible, because after searching I did not find any client implementation for this API (there are two clients on GitHub, but they use an API that no longer exists, so even they are not valid at this point). I described in detail how to find and get data for the client in the article, namely here . This is the first part to do. The second part is equally important - publish the client in Maven Central and JCenter. I've come across this before, and I'll tell you it's not the most obvious thing. Because we want something like this:git push mavenCentral , but in reality it doesn't. Therefore, the second part will be exactly about this - publishing the client to the largest Maven Central and JCenter repositories . The result of the article will be the use of a client for a project to monitor prices for air tickets. If the behavior does not change after adding the client, then everything is done correctly, and it will be possible to move further towards version 1.0-RELEASE.

Part One: Writing the Skyscanner API Client

Step 1: Create an Empty Gradle Project

Using Intellij IDEA, we create a gradle project. Select Create New Project : A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 2Go to Gradle and click Next : A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 3Select the name skyscanner-flight-search-api-client , open Artifact Coordinates and groupId, artifactId and version : A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 4And here's what you need to keep in mind:
  • GroupId: You can think of it as an identifier for an account, organization, or package name under which a library or several libraries are distributed. GROUP_ID must be in Reverse FQDN format;
  • ArtifactId: the name of the library or, in Maven terminology, the name of the "artifact";
  • Version: The xyz pattern is recommended, but any string value is acceptable.
Note: When choosing a GROUP_ID, keep in mind that you must own the selected domain. Otherwise, there will be problems when registering it with Sonatype. It follows from this that you need to choose a GroupId such that it is your domain, for example, like my com.github.romankh3 account on GitHub. Next, we'll reuse a lot of the code, with slight modifications, from flights-monitoring , from which the article ( Creating a Flight Price Monitoring System: A Step-by-Step Guide ) was based

Step 2: Add the Required Dependencies

While writing the client, it turned out that the Unirest library moved to github and continues to develop, but under a different groupId. So now add the following dependency to build.gradle in the dependencies block:

compile 'com.konghq:unirest-java:3.2.00'
we also need to work with the Lombok Project already known from the last article . Add it so that it works at runtime:

runtime 'org.projectlombok:lombok:1.18.10'
Further, to work with JSON files, we will also use the Jackson Project . It has more than one dependency, we need annotations and databind:

compile 'com.fasterxml.jackson.core:jackson-annotations:2.10.0'
compile 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
And, of course, don't forget to add JUnit and Mockito for testing. testCompile means that the dependency will only be visible to tests. As in maven<scope>test</scope>

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.26.0'
Well, we have prepared all the required dependencies, now we can move on to the code.

Step 3: write UniRestUnit and package with DTO objects

To send REST requests, we create a UniRestUtil with static methods for requests. This version is 0.1, and it will only have the queries already implemented in the previous article, so there will be one method getthat takes the key and String path required to work with Rapidapi . It will already be formed exactly as needed for the request. path was created to make this method generic. In the course of writing the client, almost all classes were redesigned and changes were made. Actually, UniRestUtil itself:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.exception.FlightSearchApiClientException;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.validation.ValidationErrorDto;
import java.util.List;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import kong.unirest.UnirestException;
import org.apache.http.HttpStatus;

/**
* Unit with static methods for using Unirest library.
*
* @author Roman Beskrovnyi
* @since 0.1
*/
final class UniRestUtil {

   private static final String HOST = "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com";

   static final String PLACES_FORMAT = "/apiservices/autosuggest/v1.0/%s/%s/%s/?query=%s";
   static final String CURRENCIES_FORMAT = "/apiservices/reference/v1.0/currencies";
   static final String COUNTRIES_FORMAT = "/apiservices/reference/v1.0/countries/%s";
   static final String VALIDATIONS_KEY = "ValidationErrors";

   static final String PLACES_KEY = "Places";
   static final String CURRENCIES_KEY = "Currencies";
   static final String COUNTRIES_KEY = "Countries";

   private static ObjectMapper objectMapper = new ObjectMapper();


   static HttpResponse<JsonNode> get(String xRapidApiKey, String path) {
       HttpResponse<JsonNode> response;
       try {
           response = Unirest.get("https://" + HOST + path)
                   .header("x-rapidapi-host", HOST)
                   .header("x-rapidapi-key", xRapidApiKey)
                   .asJson();

           if (response.getStatus() != HttpStatus.SC_OK) {
               throw new FlightSearchApiClientException(
                       String.format("There are validation errors. statusCode = %s", response.getStatus()),
                       readValueWrapper(response.getBody().getObject().get(VALIDATIONS_KEY).toString(),
                               new TypeReference<list<validationerrordto>>() {
                               }));
           }
       } catch (UnirestException e) {
           throw new FlightSearchApiClientException(String.format("Request failed, path=%s", HOST + path), e);
       }

       return response;
   }

   static <T> T readValueWrapper(String content, TypeReference<T> valueTypeRef) {
       try {
           return objectMapper.readValue(content, valueTypeRef);
       } catch (JsonProcessingException e) {
           throw new FlightSearchApiClientException("Object Mapping failure.", e);
       }
   }
}
Here you can see that I wrapped checked exceptions with my own RuntimeException. This is done so that checked exceptions do not pollute the codebase of client users, and if an exception occurs, RuntimeException will pass all the information to the client user. To do this, I created a method readValueWrapperthat does the above behavior for reading from JSON to POJO. Also, with the help of access identifiers, classes were encapsulated that do not need access from outside, so UniRestUtil has a package-private identifier. Actually, here is the RuntimeException itself:
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.validation.ValidationErrorDto;
import java.util.List;

/**
* A {@link RuntimeException} that is thrown in case of an flight monitoring failures.
*
* @author Roman Beskrovnyi
* @since 0.1
*/
public final class FlightSearchApiClientException extends RuntimeException {

   private List<ValidationErrorDto> validationErrorDtos;

   /**
    * Constructs a new {@link FlightSearchApiClientException} with the specified detail message and cause.
    * Note that the detail message associated with cause is not automatically incorporated in this {@link
    * FlightSearchApiClientException}'s detail message.
    *
    * @param message the detail message (which is saved for later retrieval by the Throwable.getMessage() method).
    * @param throwable the cause (which is saved for later retrieval by the Throwable.getCause() method).
    * (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
    */
   public FlightSearchApiClientException(String message, Throwable throwable) {
       super(message, throwable);
   }

   /**
    * Constructs a new {@link FlightSearchApiClientException} with specified collection of the
    * {@link ValidationErrorDto} objects.
    *
    * @param message the detail message (which is saved for later retrieval by the Throwable.getMessage() method).
    * @param errors the collection of the {@link ValidationErrorDto} which contain errors from Skyscanner API.
    */
   public FlightSearchApiClientException(String message, List<ValidationErrorDto> errors) {
       super(message);
       this.validationErrorDtos = errors;
   }
}
The largest package will be the model package , which stores all DTO (data transfer object) objects. Both those that will be needed to create a request, and those that will return values. For greater clarity and structure, the dots will be divided into the groups in which they are used. Why? Because the Place object has different fields and field names in different groups. Therefore, there are BrowsePlaceDto and PlacesPlaceDto , and accordingly, they are separated as shown in the figure below: A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 5In order not to pour all these classes on you, I will describe two types, and I will give a link to GitHub for others. The first type of DTO is Search, that is, those used for searching through the client. They are overlaid with field validation and indicate which of them are required and which are optional. Consider BrowseSearchDto :
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
* DTO object for search in Browse Flight Search calls.
*
* @since 0.1
* @author Roman Beskrovnyi
*/
@Getter
@Builder(builderMethodName = "hiddenBuilder")
public class BrowseSearchDto {

   @NonNull
   private String country;

   @NonNull
   private String currency;

   @NonNull
   private String locale;

   @NonNull
   private String originPlace;

   @NonNull
   private String destinationPlace;

   @NonNull
   private LocalDate outboundPartialDate;

   private LocalDate inboundPartialDate;
}
Three annotations from Project Lombok are used here:
  • @Getter - generates getters for all fields;
  • @Builder - generates all the data needed to use the Builder pattern . It turned out that there are no getters in @Builder, so I added @Getter separately;
  • @NotNull tells Lombok that these fields must have values ​​when the object is created. This is done because these fields are required in the search, and the annotation validates them when they are created. Simple and fast. It is also worth noting that the inboundPartialDate field does not have this annotation, as it is optional in this query. It can be seen here:
A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 6The second type of DTO is one that returns a result. Their feature is that you need to add an annotation for the Jackson Project. Below is the CountryDto:
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

/**
* Data transfer object for Country.
*
* @since 0.1
* @author Roman Beskrovnyi
*/
@Data
public class CountryDto {

   @JsonProperty("Code")
   private String code;

   @JsonProperty("Name")
   private String name;
}
Where:
  • @Data is an annotation from the Lombok project that generates all getters, setters, overrides toString(), equals()and hashCode()methods. By doing this, it improves the readability of the code and speeds up the time of writing POJO objects;
  • @JsonProperty("Code") is an annotation from the Jackson Project that says which field will be assigned to this variable. That is, a field in JSON equal to Code will be assigned to the code variable.
A guide to creating a client for the Skyscanner API and publishing it to jCenter and Maven Central [Part 1] - 7

Step 4: Describe the interfaces and implementations for the client

PlacesClient :
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.place.PlaceSearchDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.place.PlacesPlaceDto;
import java.util.List;

/**
* Get a list of places that match a query string.
*
* @author Roman Beskrovnyi
* @since 0.1
*/
public interface PlacesClient {

   /**
    * Get a list of places that match a query string based on arguments.
    *
    * @param xRapidApiKey key for getting access to rapid api.
    * @param placeSearchDto {@link PlacesPlaceDto} object for search places
    * @return the collection of the {@link PlacesPlaceDto} objects.
    */
   List<PlacesPlaceDto> retrieveListPlaces(String xRapidApiKey, PlaceSearchDto placeSearchDto);
}
And the implementation of PlacesClientImpl :
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.PLACES_FORMAT;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.PLACES_KEY;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.get;

import com.fasterxml.jackson.core.type.TypeReference;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.place.PlaceSearchDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.place.PlacesPlaceDto;
import java.util.List;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;

/**
* {@inheritDoc}
*/
public class PlacesClientImpl implements PlacesClient {

   /**
    * {@inheritDoc}
    */
   public List<PlacesPlaceDto> retrieveListPlaces(String xRapidApiKey, PlaceSearchDto placeSearchDto) {
       HttpResponse<JsonNode> response = get(xRapidApiKey,
               String.format(PLACES_FORMAT, placeSearchDto.getCountry(), placeSearchDto.getCurrency(),
                       placeSearchDto.getLocale(), placeSearchDto.getPlaceName()));

       String jsonList = response.getBody().getObject().get(PLACES_KEY).toString();

       return UniRestUtil.readValueWrapper(jsonList, new TypeReference<List<PlacesPlaceDto>>() {
       });
   }
}
LocalisationClient:
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.exception.FlightSearchApiClientException;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CountryDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CurrencyDto;
import java.util.List;

/**
* Retrieve the market countries that we support. Most suppliers (airlines, travel agents and car hire dealers) set
* their fares based on the market (or country of purchase). It is therefore necessary to specify the market country in
* every query.
*
* @author Roman Beskrovnyi
* @since 0.1
*/
public interface LocalisationClient {

   /**
    * Retrieve the market countries that SkyScanner flight search API support. Most suppliers (airlines,
    * travel agents and car hire dealers) set their fares based on the market (or country of purchase).
    * It is therefore necessary to specify the market country in every query.
    *
    * @param locale locale of the response.
    * @return the collection of the {@link CountryDto} objects.
    */
   List<countrydto> retrieveCountries(String locale, String xRapidApiKey) throws FlightSearchApiClientException;

   /**
    * Retrieve the currencies that we ScyScanner flight search API.
    *
    * @return the collection of the {@link CurrencyDto} objects.
    */
   List<currencydto> retrieveCurrencies(String xRapidApiKey) throws FlightSearchApiClientException;

}
And the implementation of LocalizationClientImpl
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.COUNTRIES_FORMAT;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.COUNTRIES_KEY;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.CURRENCIES_FORMAT;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.CURRENCIES_KEY;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.get;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.readValueWrapper;

import com.fasterxml.jackson.core.type.TypeReference;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CountryDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CurrencyDto;
import java.util.List;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;

/**
* {@inheritDoc}
*/
public class LocalisationClientImpl implements LocalisationClient {

   /**
    * {@inheritDoc}
    */
   public List<CountryDto> retrieveCountries(String locale, String xRapidApiKey) {
       HttpResponse<JsonNode> response = get(xRapidApiKey, String.format(COUNTRIES_FORMAT, locale));
       String jsonList = response.getBody().getObject().get(COUNTRIES_KEY).toString();
       return readValueWrapper(jsonList, new TypeReference<List<CountryDto>>() {
       });
   }

   /**
    * {@inheritDoc}
    */
   public List<CurrencyDto> retrieveCurrencies(String xRapidApiKey) {
       HttpResponse<JsonNode> response = get(xRapidApiKey, CURRENCIES_FORMAT);
       String jsonList = response.getBody().getObject().get(CURRENCIES_KEY).toString();
       return readValueWrapper(jsonList, new TypeReference<List<CurrencyDto>>() {
       });
   }
}
BrowseFlightPricesClient:
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.BrowseFlightPricesResponseDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.BrowseSearchDto;

/**
* Retrieve the market countries that Skyscanner support. Most suppliers (airlines, travel agents and car hire dealers)
* set their fares based on the market (or country of purchase). It is therefore necessary to specify the market country
* in every query.
*
* @author Roman Beskrovnyi
* @since 0.1
*/
public interface BrowseFlightPricesClient {

   /**
    * Retrieve the cheapest quotes from our cache prices.
    *
    * @param xRapidApiKey key for getting access to rapid api.
    * @param searchDto {@link BrowseSearchDto} object for search.
    * @return {@link BrowseFlightPricesResponseDto} object with all the data related to provided search dto.
    */
   BrowseFlightPricesResponseDto browseQuotes(String xRapidApiKey, BrowseSearchDto searchDto);
}
And the implementation of BrowseFlightPricesClientImpl:
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.CURRENCIES_KEY;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.PLACES_KEY;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.get;
import static com.github.romankh3.skyscanner.api.flightsearchclient.v1.UniRestUtil.readValueWrapper;

import com.fasterxml.jackson.core.type.TypeReference;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.BrowseFlightPricesResponseDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.BrowsePlaceDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.BrowseSearchDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.CarrierDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.QuoteDto;
import com.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CurrencyDto;
import java.util.List;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;

/**
* {@inheritDoc}
*/
public class BrowseFlightPricesClientImpl implements BrowseFlightPricesClient {

   public static final String BROWSE_QUOTES_FORMAT = "/apiservices/browsequotes/v1.0/%s/%s/%s/%s/%s/%s";
   public static final String OPTIONAL_BROWSE_QUOTES_FORMAT = BROWSE_QUOTES_FORMAT + "?inboundpartialdate=%s";

   public static final String QUOTES_KEY = "Quotes";
   public static final String ROUTES_KEY = "Routes";
   public static final String DATES_KEY = "Dates";
   public static final String CARRIERS_KEY = "Carriers";

   @Override
   public BrowseFlightPricesResponseDto browseQuotes(String xRapidApiKey, BrowseSearchDto searchDto) {
       HttpResponse<JsonNode> response = searchDto.getInboundPartialDate() == null ?
               get(xRapidApiKey, String
                       .format(BROWSE_QUOTES_FORMAT, searchDto.getCountry(), searchDto.getCurrency(),
                               searchDto.getLocale(), searchDto.getOriginPlace(), searchDto.getDestinationPlace(),
                               searchDto.getOutboundPartialDate())) :
               get(xRapidApiKey, String
                       .format(OPTIONAL_BROWSE_QUOTES_FORMAT, searchDto.getCountry(), searchDto.getCurrency(),
                               searchDto.getLocale(), searchDto.getOriginPlace(), searchDto.getDestinationPlace(),
                               searchDto.getOutboundPartialDate(), searchDto.getInboundPartialDate()));

       return mapToObject(response);
   }

   private BrowseFlightPricesResponseDto mapToObject(HttpResponse<jsonnode> response) {
       BrowseFlightPricesResponseDto flightPricesDto = new BrowseFlightPricesResponseDto();
       flightPricesDto.setQuotas(readValueWrapper(response.getBody().getObject().get(QUOTES_KEY).toString(),
               new TypeReference<List<QuoteDto>>() {
               }));
       flightPricesDto.setCarriers(readValueWrapper(response.getBody().getObject().get(CARRIERS_KEY).toString(),
               new TypeReference<List<CarrierDto>>() {
               }));
       flightPricesDto
               .setCurrencies(readValueWrapper(response.getBody().getObject().get(CURRENCIES_KEY).toString(),
                       new TypeReference<List<CurrencyDto>>() {
                       }));
       flightPricesDto.setPlaces(readValueWrapper(response.getBody().getObject().get(PLACES_KEY).toString(),
               new TypeReference<List<BrowsePlaceDto>>() {
               }));
       return flightPricesDto;
   }
}
This completes our work on the client, the project is stored on GitHub . A guide to creating a client for the Skyscanner API and publishing it to jCenver and Maven Central [Part 2]
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION