JavaRush/Java блог/Random UA/Гайд зі створення клієнта для Skyscanner API та його публ...
Roman Beekeeper
35 рівень

Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1]

Стаття з групи Random UA
учасників

Зміст:

Ідея розробки клієнта як окремої бібліотеки прийшла в той момент, коли я писав статтю Створення системи моніторингу цін на авіаквитки : покрокове керівництво частина 3 ). Навіщо це потрібно? Наприклад, щоб можна було просто додати її як залежність до проекту, не думати, як і що потрібно робити, а просто використовувати створений API. Для статті потрібно мати уявлення про те, що таке: Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1] - 1
  • система збирання проектів Gradle. Минулого разу ми використали Maven, цього разу для публікації візьмемо Gradle. Для швидкого ознайомлення вистачить і цієї статті .
  • groupId, artifactId, version. Це для опублікування клієнта.

План дій

Skyscanner API має чотири групи запитів:
  • Live Flight Search
  • Places
  • Browse Flight Prices
  • Localisation
Так ось ідея полягає в тому, щоб написати клієнт із чотирма інтерфейсами для роботи з цими групами, який вимагає лише передати токен для роботи з Rapid API та необхідні дані для запиту, а клієнт сам піклується про все інше. Користь від цього проекту реально відчутна, тому що після пошуку я не знайшов жодної реалізації клієнта для цього API (є два клієнти на GitHub, але вони використовують API, якого вже немає, тому навіть вони не валідні на цей момент). Як знайти та отримати дані для клієнта я докладно описав у статті, а саме – тут . Це перша частина, яку потрібно зробити. Друга частина не менш важлива - опублікувати клієнт у Maven Central та JCenter. Я вже стикався з цим, і скажу вам, що це не очевидна річ. Адже ми хочемо щось таке:git push mavenCentral , але насправді це не так. Тому друга частина буде саме про це - публікації клієнта в наймасштабніші сховища Maven Central і JCenter . Підсумком статті буде використання клієнта для проекту з моніторингу цін на авіаквитки. Якщо поведінка не зміниться після додавання клієнта, все зроблено правильно, і можна буде рухатися далі в напрямку версії 1.0-RELEASE.

Частина перша: пишемо Skyscanner API клієнт

Крок 1: створюємо порожній проект на основі Gradle

Через Intellij IDEA створюємо проект gradle . Вибираємо Create New Project : Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1] - 2Переходимо в Gradle і тиснемо Next : Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1] - 3Вибираємо назву skyscanner-flight-search-api-client , відкриваємо Artifact Coordinates та groupId, artifactId та version: Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1] - 4Причому ось що потрібно мати на увазі:
  • GroupId: можна уявити як ідентифікатор облікового запису, організації або package name, під яким поширюється бібліотека чи кілька бібліотек. GROUP_ID має бути у форматі Reverse FQDN;
  • ArtifactId: назва бібліотеки або в термінології Maven назва "артефакту";
  • Version: рекомендується використовувати патерн виду xyz, але допустиме використання будь-яких рядкових значень.
Примітка: при виборі GROUP_ID слід мати на увазі, що ви повинні належати вибраному домену. Інакше виникнуть проблеми при його реєстрації в Sonatype. З цього випливає, що потрібно вибирати GroupId таким, щоб це був ваш домен, наприклад, як мій com.github.romankh3 - обліковий запис на GitHub. Далі, перевикористовуємо безліч коду з невеликими змінами з flights-monitoring , на основі якого була створена стаття ( Створення системи моніторингу цін на авіаквитки: покрокове керівництво ).

Крок 2: додаємо необхідні залежності

Під час написання клієнта виявилося, що бібліотека Unirest переїхала в гітхаб і продовжує розвиватися, але вже під іншим groupId. Так що тепер додаємо в build.gradle наступну залежність до блоку dependencies:

compile 'com.konghq:unirest-java:3.2.00'
також нам для роботи знадобиться вже відомий за минулою статтею Lombok Project . Додаємо його так, щоб він працював при рантаймі:

runtime 'org.projectlombok:lombok:1.18.10'
Далі, для роботи з JSON файлуми, також використовуватимемо Jackson Project . Він має не одну залежність, нам потрібні анотації та databind:

compile 'com.fasterxml.jackson.core:jackson-annotations:2.10.0'
compile 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
І, звичайно, для тестування не забудемо додати JUnit та Mockito. TestCompile означає, що залежність буде видно лише для тестів. Як у maven<scope>test</scope>

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.26.0'
Ну ось, ми підготували всі необхідні залежності, тепер можна перейти до коду.

Крок 3: пишемо UniRestUnit та пакет з DTO об'єктами

Для надсилання запитів REST ми створюємо UniRestUtil зі статичними методами для запитів. Ця версія 0.1, і у неї будуть тільки ті запити, які вже реалізовані в попередній статті, тому буде один метод, getякий приймає необхідний для роботи з ключом Rapidapi і String path . Він буде сформований саме так, як необхідно для запиту. path створений у тому, щоб зробити цей спосіб універсальним. У ході написання клієнта було перероблено майже всі класи, внесено зміни. Власне, сам UniRestUtil:
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);
       }
   }
}
Тут можна помітити, що я зробив оболонку для checked винятків за допомогою свого RuntimeException. Робиться це для того, щоб виключення checked не засмічували кодову базу у користувачів клієнта, а якщо і відбудеться виняткова ситуація, RuntimeException передасть всю інформацію користувачеві клієнта. Для цього я створив метод readValueWrapper, який виконує описану вище поведінку для читання з JSON POJO. Також за допомогою ідентифікаторів доступу були інкапсульовані класи, до яких не потрібен доступ ззовні, тому UniRestUtil стоїть package-private ідентифікатор. Власне, ось сам RuntimeException:
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;
   }
}
Найбільшим пакетом буде пакет model , який зберігає всі DTO (data transfer object) об'єкти. Як ті, які будуть потрібні для створення запиту, так і ті, які повертатимуть значення. Для більшої ясності та структури, дтошки будуть поділені на групи, в яких вони використовуються. Чому? Тому що об'єкт Place має різні поля та імена полів у різних групах. Тому є BrowsePlaceDto і PlacesPlaceDto , і, відповідно, вони розділені як показано на малюнку нижче: Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1] - 5Щоб не вабовати на вас всі ці класи, я опишу два типи, а на інші дам посилання на GitHub. Перший тип DTO – Search, тобто ті, які використовуються для пошуку через клієнт. На них накладається валідація полів та вказується, які з них необхідні та які опціональні. Розглянемо 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;
}
Тут використовуються три анотації з Project Lombok:
  • @ Getter - генерує гетери для всіх полів;
  • @Builder - генерує всі дані, необхідні для використання патерну Builder . Виявилося, що @Builder'e немає гетерів, тому окремо додав @Getter;
  • @NotNull — каже Lombok, що ці поля повинні мати значення під час створення об'єкта. Це зроблено тому, що в пошуку ці поля є обов'язковими, і анотація валідує їх при створенні. Просто та швидко. Варто також зазначити, що поле восновномупартійномуДаті не має цієї анотації, оскільки воно опціональне в цьому запиті. Це можна побачити тут:
Гайд зі створення клієнта для Skyscanner API та його публікації в jCenter та Maven Central [Частина 1] - 6Другий тип DTO — це результат, що повертає. Їхня особливість полягає в тому, що потрібно додати інструкцію для Jackson Project. Нижче наведено 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;
}
Де:
  • @Data — інструкція з Lombok проекту , яка генерує всі гетери, сеттери, перевизначає toString(), equals()і hashCode()методи. Цим вона покращує читабельність коду та прискорює час написання POJO об'єктів;
  • @JsonProperty("Code") - це інструкція з Jackson Project, яка говорить, яке поле буде присвоюватися цій змінній. Тобто поле в JSON, що дорівнює Code, присвоюватиметься змінній code.

Крок 4: Описуємо інтерфейси та реалізації для клієнта

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);
}
І реалізація 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;

}
І реалізація LocalisationClientImpl
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);
}
І реалізація 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;
   }
}
На цьому робота нам над клієнтом закінчена, проект зберігається на GitHub . Гайд зі створення клієнта для Skyscanner API та його публікації в jCenver та Maven Central [Частина 2]
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.