コンテンツ:
- 目標を達成するための行動計画
- Skyscanner API の探索
- Spring Boot に基づいたアプリケーション フレームワークの作成
- プロジェクトで Skyscanner Flight Search API へのリクエスト用のクライアントを作成しています
- ローカリゼーションリクエスト用のクライアントサービスの作成
- REST と REST エンドポイントの構築方法。
- リレーショナルデータベース。
- Maven の作業 (特に、依存関係とは何か)。
- JSON オブジェクト。
- ロギングの原則。
- 特定の日付のフライトを選択し、その料金を追跡できます。ユーザーは電子メールアドレスによって識別されます。価格変更のサブスクリプションが行われるとすぐに、ユーザーは電子メールで通知を受け取ります。
- 30 分ごとに (この間隔は application.properties で構成されます)、すべてのサブスクリプションのフライトの最低価格が再計算されます。いずれかの値が低くなった場合、ユーザーは電子メールで通知を受け取ります。
- フライト日が古いサブスクリプションはすべて削除されます。
- REST API を使用すると、次のことが可能になります。
- サブスクリプションを作成します。
- 編集;
- すべての購読を電子メールで受け取ります。
- サブスクリプションを削除します。
目標を達成するための行動計画
フライトに関する情報はどこかから取得する必要があるという事実から始める必要があります。通常、Web サイトは情報を取得できるオープン REST API を提供します。
API (アプリケーション プログラミング インターフェイス) は、アプリケーションと対話できるインターフェイスです。これから、REST API とは何かへの橋渡しをすることができます。 REST API は、Web アプリケーションとの通信に使用できる REST リクエストのインターフェイスです。 |
Skyscanner API の探索
楽天apiへのリンクをたどってみましょう。まず登録する必要があります。 これはすべて、サイトを使用するための一意のキーを受け取り、そのサイトに掲載されているパブリック API にリクエストを行うために必要です。これらの API の 1 つは、必要なSkyscanner Flight Searchです。では、それがどのように機能するかを見てみましょう。GET List Places リクエストを見つけてみましょう。この図は、データを入力してTest Endpointを開始する必要があることを示しています。その結果、右側の JSON オブジェクトの形式で応答を受け取ります。 リクエストは次のように作成されます。
https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/autosuggest/v1.0/{country}/{currency}/{locale}/?query={query}
すべてのパラメータがこの式に代入され、次のようになります。
https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/autosuggest/v1.0/UK/GBP/en-GB/?query=Stockholm
2 つのヘッダーがこれらのリクエストに渡されます。
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", "sing-up-for-key"),
登録後に発行されるものsign-up-for-key
。価格下落を追跡するには、 Browse Quotesエンドポイントが必要です。自分で見つけてください:)
Spring Boot に基づいたアプリケーション フレームワークの作成
Spring Boot でプロジェクトを迅速かつ簡単に作成するには、Spring Initializr を使用できます。次のオプションを選択します。- Maven プロジェクト
- ジャワ
- 2.1.10
- グループ - 必要と思われるもの (ru.javarush など)
- アーティファクト - まったく同じ、たとえば、flights-monitoring
- 依存関係の検索では、以下を探します。
- スプリングウェブ
- Java メール送信者
- スプリングデータジャパン
- H2 データベース
- コントローラー - アプリケーションにログインします。REST APIについてはここで説明します
- SERVICE はビジネス ロジック層です。ここではアプリケーションのロジック全体を説明します。
- REPOSITORY - データベースを操作するためのレイヤー。
プロジェクトで Skyscanner Flight Search API へのリクエスト用のクライアントを作成しています
Skyscanner は、API の使用方法に関する記事を提供してくれました(アクティブなリクエストでセッションは作成されません)。「クライアントを書く」とはどういう意味ですか? 特定のパラメーターを使用して特定の URL へのリクエストを作成し、返送されるデータ用の DTO (データ転送オブジェクト) を準備する必要があります。サイトには 4 つのリクエスト グループがあります。- ライブフライト検索 - 現時点では不要とは考えておりません。
- 場所 - 書いてみましょう。
- 航空券の価格を参照 - すべての情報を取得できる 1 つのリクエストを使用します。
- ローカリゼーション - どのようなデータがサポートされているかを知るために追加しましょう。
ローカリゼーション リクエスト用のクライアント サービスを作成します。
計画は蒸しカブのように単純です。リクエストを作成し、パラメータを確認し、レスポンスを確認します。リスト マーカーと通貨という 2 つのクエリがあります。まずは通貨から始めましょう。この図は、これが追加フィールドのないリクエストであることを示しています。サポートされている通貨に関する情報を取得するために必要です。 応答は、同じオブジェクトのコレクションを含む JSON オブジェクトの形式です。次に例を示します。{
"Code":"LYD"
"Symbol":"د.ل."
"ThousandsSeparator":","
"DecimalSeparator":"."
"SymbolOnLeft":true
"SpaceBetweenAmountAndSymbol":false
"RoundingCoefficient":0
"DecimalDigits":3
}
このオブジェクトの CurrencyDto を作成しましょう。
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;
}
どこ:
- @Data はLombok プロジェクトのアノテーションで、すべてのゲッター、セッター、オーバーライドの toString()、equals()、および hashCode() メソッドを生成します。コードの可読性が向上し、 POJOオブジェクトの作成時間が短縮されます。
- @JsonProperty("Code") は、この変数にどのフィールドが割り当てられるかを示す Jackson Project のアノテーションです。つまり、Code に等しい JSON 内のフィールドがcode変数に割り当てられます。
<dependency>
<groupId>com.mashape.unirest</groupId>
<artifactId>unirest-java</artifactId>
<version>1.4.9</version>
</dependency>
次に、REST リクエストを実行するサービスを作成します。もちろん、クライアント/サービスごとにインターフェイスとその実装を作成します。
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);
}
そしてその実装:
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;
}
}
その本質は、関心のあるすべてのリクエストが GET リクエスト用に作成され、このサービスが既製のリクエストを受け入れ、次のような必要なヘッダーを追加することです。
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", xRapidApiKey)
プロパティからデータを取得するには、以下に示すように @Value アノテーションを使用します。
@Value("${x.rapid.api.key}")
private String xRapidApiKey;
application.properties には x.rapid.api.key という名前のプロパティがあり、この変数に注入する必要があると書かれています。ハードコードされた値を削除し、プログラム コードからこの変数の定義を導き出します。さらに、このアプリケーションを GitHub で公開するときは、このプロパティの値を追加しません。これはセキュリティ上の理由から行われます。REST リクエストで動作するサービスを作成しました。次は、ローカリゼーション用のサービスを作成します。OOP に基づいてアプリケーションを構築しているため、LocalizationClientインターフェイスとその実装LocalisationClientImplを作成します。
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();
}
LocalisationClientImpl の実装
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>>() {
});
}
}
どこ
- @Autowired は、オブジェクトをこのクラスに挿入し、オブジェクトを作成せずに、つまり新しい Object 操作を行わずに使用する必要があることを示すアノテーションです。
- @Component は、後で @Autowired アノテーションを使用して注入できるように、このオブジェクトをアプリケーション コンテキストに追加する必要があることを示すアノテーションです。
- ObjectMapper objectMapper は、これらすべてを Java オブジェクトに変換する Jackson Project のオブジェクトです。
- 通貨DTOと国DTO:
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;
}
プロジェクトの任意の部分に ObjectMapper を挿入するには、構成クラスを介してそれを作成し、ApplicationContext に追加します。
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;
}
}
@Configuration アノテーションは、このクラスにいくつかの構成が存在することを Spring に伝えます。このためだけに ObjectMapper を追加しました。同様に、PlacesClient と 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;
}
そして
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>>() {
});
}
}
PlacesDto の形式は次のとおりです。
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;
}
そして最後に、必要なデータに基づいてフライトの最低価格と必要なすべての情報を返すクライアント サービス、FlightPriceClient と FlightPriceClientImpl です。1 つのリクエスト、browseQuotes のみを実装します。フライト価格クライアント:
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);
}
フライト価格クライアントImpl
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;
}
}
FlightClientException は次のようになります。
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;
}
その結果、PlacesCl のデータによると、
GO TO FULL VERSION