Ідея розробки клієнта як окремої бібліотеки прийшла в той момент, коли я писав статтю Створення системи моніторингу цін на авіаквитки : покрокове керівництво частина 3 ). Навіщо це потрібно? Наприклад, щоб можна було просто додати її як залежність до проекту, не думати, як і що потрібно робити, а просто використовувати створений API. Для статті потрібно мати уявлення про те, що таке:
система збирання проектів 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 : Переходимо в Gradle і тиснемо Next : Вибираємо назву skyscanner-flight-search-api-client , відкриваємо Artifact Coordinates та groupId, artifactId та version: Причому ось що потрібно мати на увазі:
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:
І, звичайно, для тестування не забудемо додати JUnit та Mockito. TestCompile означає, що залежність буде видно лише для тестів. Як у maven<scope>test</scope>
Ну ось, ми підготували всі необхідні залежності, тепер можна перейти до коду.
Крок 3: пишемо UniRestUnit та пакет з DTO об'єктами
Для надсилання запитів REST ми створюємо UniRestUtil зі статичними методами для запитів. Ця версія 0.1, і у неї будуть тільки ті запити, які вже реалізовані в попередній статті, тому буде один метод, getякий приймає необхідний для роботи з ключом Rapidapi і String path . Він буде сформований саме так, як необхідно для запиту. path створений у тому, щоб зробити цей спосіб універсальним. У ході написання клієнта було перероблено майже всі класи, внесено зміни. Власне, сам UniRestUtil:
Тут можна помітити, що я зробив оболонку для checked винятків за допомогою свого RuntimeException. Робиться це для того, щоб виключення checked не засмічували кодову базу у користувачів клієнта, а якщо і відбудеться виняткова ситуація, RuntimeException передасть всю інформацію користувачеві клієнта. Для цього я створив метод readValueWrapper, який виконує описану вище поведінку для читання з JSON POJO. Також за допомогою ідентифікаторів доступу були інкапсульовані класи, до яких не потрібен доступ ззовні, тому UniRestUtil стоїть package-private ідентифікатор. Власне, ось сам RuntimeException:
importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.model.validation.ValidationErrorDto;importjava.util.List;/**
* A {@link RuntimeException} that is thrown in case of an flight monitoring failures.
*
* @author Roman Beskrovnyi
* @since 0.1
*/publicfinalclassFlightSearchApiClientExceptionextendsRuntimeException{privateList<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.)
*/publicFlightSearchApiClientException(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.
*/publicFlightSearchApiClientException(String message,List<ValidationErrorDto> errors){super(message);this.validationErrorDtos = errors;}}
Найбільшим пакетом буде пакет model , який зберігає всі DTO (data transfer object) об'єкти. Як ті, які будуть потрібні для створення запиту, так і ті, які повертатимуть значення. Для більшої ясності та структури, дтошки будуть поділені на групи, в яких вони використовуються. Чому? Тому що об'єкт Place має різні поля та імена полів у різних групах. Тому є BrowsePlaceDto і PlacesPlaceDto , і, відповідно, вони розділені як показано на малюнку нижче: Щоб не вабовати на вас всі ці класи, я опишу два типи, а на інші дам посилання на GitHub. Перший тип DTO – Search, тобто ті, які використовуються для пошуку через клієнт. На них накладається валідація полів та вказується, які з них необхідні та які опціональні. Розглянемо BrowseSearchDto :
importjava.time.LocalDate;importlombok.Builder;importlombok.Getter;importlombok.NonNull;/**
* DTO object for search in Browse Flight Search calls.
*
* @since 0.1
* @author Roman Beskrovnyi
*/@Getter@Builder(builderMethodName ="hiddenBuilder")publicclassBrowseSearchDto{@NonNullprivateString country;@NonNullprivateString currency;@NonNullprivateString locale;@NonNullprivateString originPlace;@NonNullprivateString destinationPlace;@NonNullprivateLocalDate outboundPartialDate;privateLocalDate inboundPartialDate;}
Тут використовуються три анотації з Project Lombok:
@ Getter - генерує гетери для всіх полів;
@Builder - генерує всі дані, необхідні для використання патерну Builder . Виявилося, що @Builder'e немає гетерів, тому окремо додав @Getter;
@NotNull — каже Lombok, що ці поля повинні мати значення під час створення об'єкта. Це зроблено тому, що в пошуку ці поля є обов'язковими, і анотація валідує їх при створенні. Просто та швидко. Варто також зазначити, що поле восновномупартійномуДаті не має цієї анотації, оскільки воно опціональне в цьому запиті. Це можна побачити тут:
Другий тип DTO — це результат, що повертає. Їхня особливість полягає в тому, що потрібно додати інструкцію для Jackson Project. Нижче наведено CountryDto:
importcom.fasterxml.jackson.annotation.JsonProperty;importlombok.Data;/**
* Data transfer object for Country.
*
* @since 0.1
* @author Roman Beskrovnyi
*/@DatapublicclassCountryDto{@JsonProperty("Code")privateString code;@JsonProperty("Name")privateString name;}
Де:
@Data — інструкція з Lombok проекту , яка генерує всі гетери, сеттери, перевизначає toString(), equals()і hashCode()методи. Цим вона покращує читабельність коду та прискорює час написання POJO об'єктів;
@JsonProperty("Code") - це інструкція з Jackson Project, яка говорить, яке поле буде присвоюватися цій змінній. Тобто поле в JSON, що дорівнює Code, присвоюватиметься змінній code.
Крок 4: Описуємо інтерфейси та реалізації для клієнта
PlacesClient :
importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.model.place.PlaceSearchDto;importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.model.place.PlacesPlaceDto;importjava.util.List;/**
* Get a list of places that match a query string.
*
* @author Roman Beskrovnyi
* @since 0.1
*/publicinterfacePlacesClient{/**
* 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);}
importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.exception.FlightSearchApiClientException;importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CountryDto;importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.model.localisation.CurrencyDto;importjava.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
*/publicinterfaceLocalisationClient{/**
* 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)throwsFlightSearchApiClientException;/**
* Retrieve the currencies that we ScyScanner flight search API.
*
* @return the collection of the {@link CurrencyDto} objects.
*/List<currencydto>retrieveCurrencies(String xRapidApiKey)throwsFlightSearchApiClientException;}
importcom.github.romankh3.skyscanner.api.flightsearchclient.v1.model.browse.BrowseFlightPricesResponseDto;importcom.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
*/publicinterfaceBrowseFlightPricesClient{/**
* 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.
*/BrowseFlightPricesResponseDtobrowseQuotes(String xRapidApiKey,BrowseSearchDto searchDto);}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ