Content:
- Action Plan to Achieve the Goal
- Exploring the Skyscanner API
- Building an application framework based on Spring Boot
- We write a client in the project for requests to the Skyscanner Flight Search API
- Create a client service for the Localization request
- REST and how REST endpoints are built;
- relational databases;
- the work of maven (in particular, what is dependency — dependency);
- json object;
- logging principles.
- You can select a flight for a specific date and track the price for it. The user is identified by an email address. As soon as a subscription is made to a price change, the user receives an email notification.
- Every 30 minutes (this interval is configurable via application.properties), the minimum price per flight for all subscriptions is recalculated. If any of them has become lower, the user will receive a notification by mail.
- All subscriptions with an outdated flight date will be deleted.
- Through the REST API you can:
- create a subscription;
- edit;
- get all subscriptions by email;
- remove subscription.
Action Plan to Achieve the Goal
You need to start with the fact that information about flights needs to be taken from somewhere. Sites typically provide a public REST API that can be used to retrieve information.
An API (application programming interface) is an interface through which you can interact with an application. From this, you can throw a bridge to what a REST API is. REST API is an interface from REST requests with which you can communicate with a web application. |
Exploring the Skyscanner API
Let's follow the link to rakuten api . First you need to register All this is necessary to get a unique key to use their site and make requests to the public APIs that are posted on it. One of these APIs is the Skyscanner Flight Search we need . Now let's see how it works. Find the GET List Places request. The picture shows that you need to fill in the data and start the Test Endpoint , as a result of which we get a response in the form of a JSON object on the right: and the request will be created like this:
https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/autosuggest/v1.0/{country}/{currency}/{locale}/?query={query}
and all the parameters will be substituted into this formula, we get:
https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/autosuggest/v1.0/UK/GBP/en-GB/?query=Stockholm
and two headers will be passed to these requests:
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", "sing-up-for-key"),
where sign-up-for-key
issued after registration. We will need a Browse Quotes endpoint to track the price drop. Find it yourself :)
Building an application framework based on Spring Boot
To quickly and conveniently create a project with Spring Boot, you can use Spring Initializr . We select the following options:- maven project
- Java
- 2.1.10
- group - which one you think is necessary, for example com.codegym
- artifact - exactly the same, for example flights-monitoring
- in the search for dependency we are looking for the following:
- spring web
- Java Mail Sender
- Spring Data Jpa
- H2 Database
- CONTROLLER - entry into the application. REST API will be described here
- SERVICE - business logic layer. The entire logic of the application will be described here.
- REPOSITORY - layer of work with the database.
We write a client in the project for requests to the Skyscanner Flight Search API
Skyscanner kindly provided an article on how to use their API (we won't create a session with an active request). What does "write a client" mean? We need to create a request to a specific URL with specific parameters and prepare a DTO (data transfer object) for the data being sent back to us. There are four groups of requests on the site:- Live Flight Search - we will not consider it unnecessary at the moment.
- Places - we will write.
- Browse Flight Prices - use one query where you can get all the information.
- Localization - add it to know what data is supported.
Create a client service for the Localization request:
The plan is as simple as a steamed turnip: we create a request, we look at what parameters, we look at what answer. There are two requests List markers and Currencies. Let's start with Currencies. The figure shows that this is a request without additional fields: it is needed to get information about supported currencies: Response in the form of a JSON object, in which a collection of the same objects, for example:{
"Code":"LYD"
"Symbol":"د.ل."
"ThousandsSeparator":","
"DecimalSeparator":"."
"SymbolOnLeft":true
"SpaceBetweenAmountAndSymbol":false
"RoundingCoefficient":0
"DecimalDigits":3
}
Let's create a CurrencyDto for this object:
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Data transfer object for Currency.
*/
@Data
public class CurrencyDto {
@JsonProperty("Code")
private String code;
@JsonProperty("Symbol")
private String symbol;
@JsonProperty("ThousandsSeparator")
private String thousandsSeparator;
@JsonProperty("DecimalSeparator")
private String decimalSeparator;
@JsonProperty("SymbolOnLeft")
private boolean symbolOnLeft;
@JsonProperty("SpaceBetweenAmountAndSymbol")
private boolean spaceBetweenAmountAndSymbol;
@JsonProperty("RoundingCoefficient")
private int roundingCoefficient;
@JsonProperty("DecimalDigits")
private int decimalDigits;
}
Where:
- @Data is an annotation from the Lombok project and generates all getters, setters, overrides toString() , equals() and hashCode() methods. What 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 .
<dependency>
<groupId>com.mashape.unirest</groupId>
<artifactId>unirest-java</artifactId>
<version>1.4.9</version>
</dependency>
Next, we will write a service that will perform REST requests. Of course, for each client/service we will create an interface and its implementation:
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
/**
* Service, which is manipulating with Rest calls.
*/
public interface UniRestService {
/**
* Create GET request based on provided {@param path} with needed headers.
*
* @param path provided path with all the needed data
* @return {@link HttpResponse<jsonnode>} response object.
*/
HttpResponse<jsonnode> get(String path);
}
And its implementation:
import com.github.romankh3.flightsmonitoring.exception.FlightClientException;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Slf4j
@Service
public class UniRestServiceImpl implements UniRestService {
public static final String HOST = "https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com";
public static final String PLACES_FORMAT = "/apiservices/autosuggest/v1.0/%s/%s/%s/?query=%s";
public static final String CURRENCIES_FORMAT = "/apiservices/reference/v1.0/currencies";
public static final String COUNTRIES_FORMAT = "/apiservices/reference/v1.0/countries/%s";
public static final String PLACES_KEY = "Places";
public static final String CURRENCIES_KEY = "Currencies";
public static final String COUNTRIES_KEY = "Countries";
@Value("${x.rapid.api.key}")
private String xRapidApiKey;
/**
* {@inheritDoc}
*/
@Override
public HttpResponse<jsonnode> get(String path) {
HttpResponse<jsonnode> response = null;
try {
response = Unirest.get(HOST + path)
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", xRapidApiKey)
.asJson();
} catch (UnirestException e) {
throw new FlightClientException(String.format("Request failed, path=%s", HOST + path), e);
}
log.info("Response from Get request, on path={}, statusCode={}, response={}", path, response.getStatus(), response.getBody().toString());
return response;
}
}
Its essence is that all requests of interest to us are created for GET requests, and this service accepts a ready-made request, adds the necessary headers of the form to it:
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", xRapidApiKey)
To take data from properties, the @Value annotation is used, as shown below:
@Value("${x.rapid.api.key}")
private String xRapidApiKey;
She says that in application.properties there will be a property with the name x.rapid.api.key, which needs to be injected into this variable. We get rid of hardcoded values and derive the definition of this variable from the program code. Moreover, when I publish this application on GitHub, I do not add the value of this property. This is done for security reasons. We wrote a service that will work with REST requests, now it's time for the service for Localization. We are building an application based on OOP, so we create the LocalizationClient interface and its implementation LocalizationClientImpl :
import com.github.romankh3.flightsmonitoring.client.dto.CountryDto;
import com.github.romankh3.flightsmonitoring.client.dto.CurrencyDto;
import java.io.IOException;
import java.util.List;
/**
* Client for SkyScanner localisation.
*/
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.
*
* @throws IOException
*/
List<CountryDto> retrieveCountries(String locale);
/**
* Retrieve the currencies that we ScyScanner flight search API.
*
* @return the collection of the {@link CurrencyDto} objects.
*
* @throws IOException
*/
List<CurrencyDto> retrieveCurrencies();
}
and implementation of LocalizationClientImpl
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.COUNTRIES_FORMAT;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.COUNTRIES_KEY;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.CURRENCIES_FORMAT;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.CURRENCIES_KEY;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.flightsmonitoring.client.dto.CountryDto;
import com.github.romankh3.flightsmonitoring.client.dto.CurrencyDto;
import com.github.romankh3.flightsmonitoring.client.service.LocalisationClient;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.exceptions.UnirestException;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* {@inheritDoc}
*/
@Component
public class LocalisationClientImpl implements LocalisationClient {
@Autowired
private UniRestService uniRestService;
@Autowired
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public List<CountryDto> retrieveCountries(String locale) throws IOException {
HttpResponse<JsonNode> response = uniRestService.get(String.format(COUNTRIES_FORMAT, locale));
if (response.getStatus() != HttpStatus.SC_OK) {
return null;
}
String jsonList = response.getBody().getObject().get(COUNTRIES_KEY).toString();
return objectMapper.readValue(jsonList, new TypeReference<List<CountryDto>>() {
});
}
/**
* {@inheritDoc}
*/
@Override
public List<CurrencyDto> retrieveCurrencies() throws IOException, UnirestException {
HttpResponse<JsonNode> response = uniRestService.get(CURRENCIES_FORMAT);
if (response.getStatus() != HttpStatus.SC_OK) {
return null;
}
String jsonList = response.getBody().getObject().get(CURRENCIES_KEY).toString();
return objectMapper.readValue(jsonList, new TypeReference<List<CurrencyDto>>() {
});
}
}
Where
- @Autowired is an annotation that says that you need to inject an object into this class and use it without creating it, that is, without the new Object operation;
- @Component - an annotation that says that this object needs to be added to the Application Context so that it can be injected later using the @Autowired annotation;
- ObjectMapper objectMapper is an object from the Jackson Project that translates all of this into Java objects.
- CurrencyDTO and CountryDto:
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Data transfer object for Currency.
*/
@Data
public class CurrencyDto {
@JsonProperty("Code")
private String code;
@JsonProperty("Symbol")
private String symbol;
@JsonProperty("ThousandsSeparator")
private String thousandsSeparator;
@JsonProperty("DecimalSeparator")
private String decimalSeparator;
@JsonProperty("SymbolOnLeft")
private boolean symbolOnLeft;
@JsonProperty("SpaceBetweenAmountAndSymbol")
private boolean spaceBetweenAmountAndSymbol;
@JsonProperty("RoundingCoefficient")
private int roundingCoefficient;
@JsonProperty("DecimalDigits")
private int decimalDigits;
}
и
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Data transfer object for Country.
*/
@Data
public class CountryDto {
@JsonProperty("Code")
private String code;
@JsonProperty("Name")
private String name;
}
In order to inject the ObjectMapper into any part of the project, I added the creation and addition of it to the ApplicationContext through the configuration class.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link Configuration} class.
*/
@Configuration
public class Config {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
}
the @Configuration annotation tells Spring that there will be some configurations in this class. And just for this I added ObjectMapper. In the image and likeness, add PlacesClient and PlacesClientImpl:
import com.github.romankh3.flightsmonitoring.client.dto.PlaceDto;
import com.github.romankh3.flightsmonitoring.client.dto.PlacesDto;
import com.mashape.unirest.http.exceptions.UnirestException;
import java.io.IOException;
import java.util.List;
/**
* SkyScanner client.
*/
public interface PlacesClient {
/**
* Get a list of places that match a query string based on arguments.
*
* @param query the code of the city.
* @param country the code of the country.
* @param currency the code of the currency.
* @param locale the code of the locale.
* @return the collection of the {@link PlaceDto} objects.
*/
List<PlacesDto> retrieveListPlaces(String query, String country, String currency, String locale)
throws IOException, UnirestException;
}
and
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.PLACES_FORMAT;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.PLACES_KEY;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.flightsmonitoring.client.dto.PlacesDto;
import com.github.romankh3.flightsmonitoring.client.service.PlacesClient;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.exceptions.UnirestException;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Service
public class PlacesClientImpl implements PlacesClient {
@Autowired
private UniRestService uniRestService;
@Autowired
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public List<PlacesDto> retrieveListPlaces(String query, String country, String currency, String locale)
throws IOException, UnirestException {
HttpResponse<JsonNode> response = uniRestService
.get(String.format(PLACES_FORMAT, country, currency, locale, query));
if (response.getStatus() != HttpStatus.SC_OK) {
return null;
}
String jsonList = response.getBody().getObject().get(PLACES_KEY).toString();
return objectMapper.readValue(jsonList, new TypeReference<List<PlacesDto>>() {
});
}
}
where PlacesDto looks like:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.romankh3.flightsmonitoring.client.service.PlacesClient;
import lombok.Data;
/**
* Using for {@link PlacesClient}.
*/
@Data
public class PlacesDto {
@JsonProperty("PlaceId")
private String placeId;
@JsonProperty("PlaceName")
private String placeName;
@JsonProperty("CountryId")
private String countryId;
@JsonProperty("RegionId")
private String regionId;
@JsonProperty("CityId")
private String cityId;
@JsonProperty("CountryName")
private String countryName;
}
And finally, the client service, which will return the minimum price for the flight and all the necessary information according to the necessary data: FlightPriceClient and FlightPriceClientImpl. We will implement only one browseQuotes request. FlightPriceClient:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
/**
* Browse flight prices.
*/
public interface FlightPricesClient {
/**
* Browse quotes for current flight based on provided arguments. One-way ticket.
*
* @param country the country from
* @param currency the currency to get price
* @param locale locale for the response
* @param originPlace origin place
* @param destinationPlace destination place
* @param outboundPartialDate outbound date
* @return {@link FlightPricesDto} object.
*/
FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate);
/**
* Browse quotes for current flight based on provided arguments. Round trip ticket.
*
* @param country the country from
* @param currency the currency to get price
* @param locale locale for the response
* @param originPlace origin place
* @param destinationPlace destination place
* @param outboundPartialDate outbound date
* @param inboundPartialDate inbound date
* @return {@link FlightPricesDto} object.
*/
FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate, String inboundPartialDate);
}
FlightPriceClientImpl
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.CURRENCIES_KEY;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.PLACES_KEY;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.flightsmonitoring.client.dto.CarrierDto;
import com.github.romankh3.flightsmonitoring.client.dto.CurrencyDto;
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.client.dto.PlaceDto;
import com.github.romankh3.flightsmonitoring.client.dto.QuoteDto;
import com.github.romankh3.flightsmonitoring.client.dto.ValidationErrorDto;
import com.github.romankh3.flightsmonitoring.client.service.FlightPricesClient;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.github.romankh3.flightsmonitoring.exception.FlightClientException;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Service
public class FlightPricesClientImpl implements FlightPricesClient {
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";
public static final String VALIDATIONS_KEY = "ValidationErrors";
@Autowired
private UniRestService uniRestService;
@Autowired
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate) {
HttpResponse<JsonNode> response = uniRestService.get(String
.format(BROWSE_QUOTES_FORMAT, country, currency, locale, originPlace, destinationPlace,
outboundPartialDate));
return mapToObject(response);
}
public FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate, String inboundPartialDate) {
HttpResponse<JsonNode> response = uniRestService.get(String
.format(OPTIONAL_BROWSE_QUOTES_FORMAT, country, currency, locale, originPlace, destinationPlace,
outboundPartialDate, inboundPartialDate));
return mapToObject(response);
}
private FlightPricesDto mapToObject(HttpResponse<JsonNode> response) {
if (response.getStatus() == HttpStatus.SC_OK) {
FlightPricesDto flightPricesDto = new FlightPricesDto();
flightPricesDto.setQuotas(readValue(response.getBody().getObject().get(QUOTES_KEY).toString(),
new TypeReference<List<QuoteDto>>() {
}));
flightPricesDto.setCarriers(readValue(response.getBody().getObject().get(CARRIERS_KEY).toString(),
new TypeReference<List<CarrierDto>>() {
}));
flightPricesDto.setCurrencies(readValue(response.getBody().getObject().get(CURRENCIES_KEY).toString(),
new TypeReference<List<CurrencyDto>>() {
}));
flightPricesDto.setPlaces(readValue(response.getBody().getObject().get(PLACES_KEY).toString(),
new TypeReference<List<PlaceDto>>() {
}));
return flightPricesDto;
}
throw new FlightClientException(String.format("There are validation errors. statusCode = %s", response.getStatus()),
readValue(response.getBody().getObject().get(VALIDATIONS_KEY).toString(),
new TypeReference<List<ValidationErrorDto>>() {
}));
}
private <T> List<T> readValue(String resultAsString, TypeReference<List<T>> valueTypeRef) {
List<T> list;
try {
list = objectMapper.readValue(resultAsString, valueTypeRef);
} catch (IOException e) {
throw new FlightClientException("Object Mapping failure.", e);
}
return list;
}
}
where FlightClientException looks like:
import com.github.romankh3.flightsmonitoring.client.dto.ValidationErrorDto;
import java.util.List;
/**
* A {@link RuntimeException} that is thrown in case of an flight monitoring failures.
*/
public final class FlightClientException extends RuntimeException {
public FlightClientException(String message) {
super(message);
}
public FlightClientException(String message, Throwable throwable) {
super(message, throwable);
}
public FlightClientException(String message, List<ValidationErrorDto> errors) {
super(message);
this.validationErrorDtos = errors;
}
private List<ValidationErrorDto> validationErrorDtos;
}
As a result, according to data from PlacesCl
GO TO FULL VERSION