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 ).
Why 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 :
Go to
Gradle and click
Next :
Select the name
skyscanner-flight-search-api-client , open
Artifact Coordinates and groupId, artifactId and version :
And 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
get
that 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;
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
readValueWrapper
that 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;
public final class FlightSearchApiClientException extends RuntimeException {
private List<ValidationErrorDto> validationErrorDtos;
public FlightSearchApiClientException(String message, Throwable throwable) {
super(message, throwable);
}
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:
In 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;
@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:
The 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
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.
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;
public interface PlacesClient {
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;
public class PlacesClientImpl implements PlacesClient {
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;
public interface LocalisationClient {
List<countrydto> retrieveCountries(String locale, String xRapidApiKey) throws FlightSearchApiClientException;
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;
public class LocalisationClientImpl implements LocalisationClient {
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>>() {
});
}
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;
public interface BrowseFlightPricesClient {
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;
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]
GO TO FULL VERSION