diff --git a/examples/README.md b/examples/README.md index 248f07d..06edf03 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,14 +17,26 @@ Currently, the following scenarios are included: This example demonstrates how to search for properties with a location keyword with filters applied in Lodging Listings API. +- [`HotelIdsSearchEndToEndScenario.java`](src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/ListingsHotelIdsSearchScenario.java): + + This example demonstrates how to retrieve accessible property ids from SDP DownloadURL API and + then get the content and prices of these properties using the Lodging Listings API. + - [`QuotesQuickStartScenario.java`](src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/QuotesQuickStartScenario.java): - This example demonstrates how to search for property quotes with property IDs in Lodging Quotes API. + This example demonstrates how to search for property quotes with property IDs in + Lodging Quotes API. + +- [`VrboPropertySearchEndToEndScenario.java`](src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/VrboPropertySearchEndToEndScenario.java): + + This example demonstrates how to retrieve accessible Vrbo property ids and location content from + SDP DownloadURL API and then get the prices of these properties using the Lodging Quotes API. - [ `AvailabilityCalendarsQuickStartScenario.java`](src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/AvailabilityCalendarsQuickStartScenario.java): - This example demonstrates how to search for availability calendars with property IDs in Lodging Availability Calendar API. + This example demonstrates how to use Availability Calendar api with simple search. + In terms of how to get property ids, you can refer to `QuotesQuickStartScenario.java`. ### Car diff --git a/examples/pom.xml b/examples/pom.xml index ed6c5bb..bf44bb1 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -65,6 +65,12 @@ log4j-slf4j2-impl 2.23.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.1 + diff --git a/examples/src/main/java/com/expediagroup/sdk/xap/examples/XapSdkDemoTestRun.java b/examples/src/main/java/com/expediagroup/sdk/xap/examples/XapSdkDemoTestRun.java index 4813bd1..f2c985f 100644 --- a/examples/src/main/java/com/expediagroup/sdk/xap/examples/XapSdkDemoTestRun.java +++ b/examples/src/main/java/com/expediagroup/sdk/xap/examples/XapSdkDemoTestRun.java @@ -19,8 +19,9 @@ import com.expediagroup.sdk.xap.examples.scenarios.car.CarDetailsQuickStartScenario; import com.expediagroup.sdk.xap.examples.scenarios.car.CarListingsQuickStartScenario; import com.expediagroup.sdk.xap.examples.scenarios.lodging.AvailabilityCalendarsQuickStartScenario; +import com.expediagroup.sdk.xap.examples.scenarios.lodging.HotelIdsSearchEndToEndScenario; import com.expediagroup.sdk.xap.examples.scenarios.lodging.ListingsQuickStartScenario; -import com.expediagroup.sdk.xap.examples.scenarios.lodging.QuotesQuickStartScenario; +import com.expediagroup.sdk.xap.examples.scenarios.lodging.VrboPropertySearchEndToEndScenario; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,8 +47,13 @@ public static void main(String[] args) { ListingsQuickStartScenario listingsQuickStartScenario = new ListingsQuickStartScenario(); listingsQuickStartScenario.run(); - QuotesQuickStartScenario quotesQuickStartScenario = new QuotesQuickStartScenario(); - quotesQuickStartScenario.run(); + HotelIdsSearchEndToEndScenario hotelIdsSearchEndToEndScenario = + new HotelIdsSearchEndToEndScenario(); + hotelIdsSearchEndToEndScenario.run(); + + VrboPropertySearchEndToEndScenario vrboPropertySearchEndToEndScenario = + new VrboPropertySearchEndToEndScenario(); + vrboPropertySearchEndToEndScenario.run(); logger.info( "=============================== End of Lodging Scenarios =============================="); diff --git a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/AvailabilityCalendarsQuickStartScenario.java b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/AvailabilityCalendarsQuickStartScenario.java index 279fe7e..b439fdb 100644 --- a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/AvailabilityCalendarsQuickStartScenario.java +++ b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/AvailabilityCalendarsQuickStartScenario.java @@ -27,7 +27,9 @@ /** * This example demonstrates how to use Availability Calendar api with simple search. - * Note: this is a Vrbo scenario. You need a key that is enabled for Vrbo brand to run this. + * In terms of how to get property ids, you can refer to {@link VrboPropertySearchEndToEndScenario}. + * + *

Note: this is a Vrbo scenario. You need a key that is enabled for Vrbo brand to run this. */ public class AvailabilityCalendarsQuickStartScenario implements VrboScenario { @@ -56,7 +58,7 @@ public void run() { GetLodgingAvailabilityCalendarsOperationParams.builder() .partnerTransactionId(PARTNER_TRANSACTION_ID) // Set of Expedia Property IDs. - .propertyIds(new HashSet<>(Arrays.asList("87704892", "12410858"))) + .propertyIds(new HashSet<>(Arrays.asList("87704892", "36960201"))) .build(); XapClient xapClient = createClient(); diff --git a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/HotelIdsSearchEndToEndScenario.java b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/HotelIdsSearchEndToEndScenario.java new file mode 100644 index 0000000..168adfb --- /dev/null +++ b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/HotelIdsSearchEndToEndScenario.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2024 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.sdk.xap.examples.scenarios.lodging; + +import com.expediagroup.sdk.core.model.Response; +import com.expediagroup.sdk.xap.client.XapClient; +import com.expediagroup.sdk.xap.examples.scenarios.XapScenario; +import com.expediagroup.sdk.xap.models.Hotel; +import com.expediagroup.sdk.xap.models.HotelListingsResponse; +import com.expediagroup.sdk.xap.models.PresignedUrlResponse; +import com.expediagroup.sdk.xap.models.RoomType; +import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperation; +import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperationParams; +import com.expediagroup.sdk.xap.operations.GetLodgingListingsOperation; +import com.expediagroup.sdk.xap.operations.GetLodgingListingsOperationParams; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This example demonstrates how to retrieve accessible property ids from SDP DownloadURL API and + * then get the content and prices of these properties using the Lodging Listings API. + * + *

This is a common scenario for meta site partners. In practice, you can build a cache with the + * property id list, content and prices to improve respond time of your pages. + */ +public class HotelIdsSearchEndToEndScenario implements XapScenario { + + private final XapClient client = createClient(); + + private static final Logger LOGGER = + LoggerFactory.getLogger(HotelIdsSearchEndToEndScenario.class); + + /** + * This field limits the number of line to read from the SDP DownloadURL API Listings file to + * reduce time to run the example. + * If the first 20 properties from the file are not accessible OR available when you run this + * example, it may end with "No accessible property ids found." OR NO_RESULT_FOUND. In that case, + * you can adjust the property count to get more properties. + */ + private static final int SAMPLE_ITEMS_RESTRICTION = 20; + + public static void main(String[] args) { + new HotelIdsSearchEndToEndScenario().run(); + System.exit(0); + } + + @Override + public void run() { + LOGGER.info( + "======================== Running HotelIdsSearchEndToEndScenario ======================="); + + List propertyIds = getPropertyIdsFromDownloadUrl(); + HotelListingsResponse hotelListingsResponse = getPropertiesFromLodgingListings(propertyIds); + displayResult(hotelListingsResponse); + + LOGGER.info( + "========================== End HotelIdsSearchEndToEndScenario ========================="); + } + + /** + * Retrieve accessible property ids from SDP DownloadURL API. + * + * @return property ids + */ + private List getPropertyIdsFromDownloadUrl() { + LOGGER.info( + "==================== Executing Step I: getPropertyIdsFromDownloadUrl ==================="); + + GetFeedDownloadUrlOperationParams getPropertyIdListParams = + GetFeedDownloadUrlOperationParams.builder() + // Use the type LISTINGS to get the list of accessible property ids. + .type(GetFeedDownloadUrlOperationParams.Type.LISTINGS) + // Without any filters, this operation will return the information of all lodging + // properties in en_US by default. + .build(); + + Response downloadUrlListingsResponse = + client.execute(new GetFeedDownloadUrlOperation(getPropertyIdListParams)); + + if (downloadUrlListingsResponse.getData() == null + || downloadUrlListingsResponse.getData().getBestMatchedFile() == null) { + throw new IllegalStateException("No listings file found"); + } + + // The download URL points to a zip file containing various jsonl files. + // Each line in the jsonl files contains a json object representing a property. + // For demonstration purposes, we will only read a few properties from the file without + // downloading the entire file. + String listingsDownloadUrl = downloadUrlListingsResponse.getData() + .getBestMatchedFile() + .getDownloadUrl(); + LOGGER.info("Listings Download URL: {}", listingsDownloadUrl); + + // Read property ids from the file. + List propertyIds = getPropertyIdsFromListingsFile(listingsDownloadUrl); + + if (propertyIds.isEmpty()) { + throw new IllegalStateException("No accessible property ids found."); + } + LOGGER.info("Accessible Property Ids: {}", propertyIds); + + LOGGER.info( + "==================== Step I: getPropertyIdsFromDownloadUrl Executed ===================="); + return propertyIds; + } + + /** + * Get prices of the properties using the Lodging Listings API. + * + * @param propertyIds The property ids to get the prices. + * @return The response of the Lodging Listings API. + */ + private HotelListingsResponse getPropertiesFromLodgingListings(List propertyIds) { + LOGGER.info( + "================ Step II: Executing getPropertiesFromLodgingListings ==============="); + + GetLodgingListingsOperationParams getLodgingListingsOperationParams = + GetLodgingListingsOperationParams.builder() + .partnerTransactionId(PARTNER_TRANSACTION_ID) + // Use the property ids read from the file + .ecomHotelIds(new HashSet<>(propertyIds)) + // The links to return, WEB includes WS (Web Search Result Page) + // and WD (Web Details Page) + .links(Collections.singletonList(GetLodgingListingsOperationParams.Links.WEB)) + // Check-in 5 days from now + .checkIn(LocalDate.now().plusDays(5)) + // Check-out 10 days from now + .checkOut(LocalDate.now().plusDays(10)) + // Filter the properties that are available only + .availOnly(true) + // Use the default occupancy: 2 adults in one room + .build(); + + HotelListingsResponse hotelListingsResponse = + client.execute(new GetLodgingListingsOperation(getLodgingListingsOperationParams)) + .getData(); + + LOGGER.info( + "================ Step II: getPropertiesFromLodgingListings Executed ================"); + return hotelListingsResponse; + } + + /** + * Reads given number of property ids from the file pointed by the download URL. + * + * @param downloadUrl The download URL of the zip file containing the property information. + * @return A list of property ids read from the file. + */ + private List getPropertyIdsFromListingsFile(String downloadUrl) { + List propertyIds = new ArrayList<>(); + HttpURLConnection connection = null; + try { + // Open a connection to the URL + URL url = new URL(downloadUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + + try (ZipInputStream zipStream = new ZipInputStream(connection.getInputStream())) { + ZipEntry entry; + while ((entry = zipStream.getNextEntry()) != null) { + if (entry.getName().endsWith(".jsonl")) { + LOGGER.info("Reading property ids from file: {}", entry.getName()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipStream))) { + String line; + ObjectMapper objectMapper = new ObjectMapper(); + while ((line = reader.readLine()) != null + && propertyIds.size() < SAMPLE_ITEMS_RESTRICTION) { + // Parse the property id from the json object + // An example json line from the jsonl file: + /* + { + "propertyId": { + "expedia": "1234567", + "hcom": "123456789", + "vrbo": "123.1234567.7654321" + }, + "bookable": { + "expedia": true, + "hcom": true, + "vrbo": true + }, + "propertyType": { + "id": 16, + "name": "Apartment" + }, + "lastUpdated": "10-27-2024 13:41:16", + "country": "France", + "inventorySource": "vrbo", + "referencePrice": { + "value": "89.52", + "currency": "USD" + }, + "vrboPropertyType": { + "instantBook": true + } + } + */ + JsonNode jsonNode = objectMapper.readTree(line); + // Check if the property is accessible from Lodging Listings API + // (Vrbo properties that are not instantBookable are not accessible for now) + if (!jsonNode.get("propertyId").get("vrbo").asText().isEmpty() + && jsonNode.has("vrboPropertyType") + && !jsonNode.get("vrboPropertyType").get("instantBook").asBoolean() + ) { + // Skip the property if it is not an instant bookable Vrbo property + continue; + } else { + // Get the Expedia property id for the Lodging Listings API + propertyIds.add(jsonNode.get("propertyId").get("expedia").asText()); + } + } + } + } + } + } + + } catch (IOException e) { + LOGGER.error("Error reading property ids from download URL", e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + return propertyIds; + } + + /** + * Display the result of the operations. + * + * @param hotelListingsResponse The response of the Lodging Listings API. + */ + private static void displayResult(HotelListingsResponse hotelListingsResponse) { + LOGGER.info("====================== Executing Step III: DisplayResult ======================="); + if (hotelListingsResponse == null || hotelListingsResponse.getHotels() == null + || hotelListingsResponse.getHotels().isEmpty()) { + throw new IllegalStateException("No properties found."); + } + + // The HotelListingsResponse contains a transaction ID for troubleshooting + LOGGER.info("Transaction ID: {}", hotelListingsResponse.getTransactionId()); + + // To access the properties, iterate through the list of hotel properties + hotelListingsResponse.getHotels().forEach(hotel -> { + // Check if the property is available + if (Hotel.Status.AVAILABLE != hotel.getStatus()) { + LOGGER.info("Property {} is not available.", hotel.getId()); + return; + } + LOGGER.info( + "=================================== Property Start ==================================="); + // To get the property name + if (StringUtils.isNotEmpty(hotel.getName())) { + LOGGER.info("Property Name: {}", hotel.getName()); + } + // To get the property address + if (hotel.getLocation() != null) { + LOGGER.info("Property Address: {}", hotel.getLocation().getAddress()); + } + // To get the property thumbnail URL + if (StringUtils.isNotEmpty(hotel.getThumbnailUrl())) { + LOGGER.info("Thumbnail URL: {}", hotel.getThumbnailUrl()); + } + // To get the star rating of the property. The value is between 1.0 and 5.0 + // in a 0.5 increment. + if (hotel.getStarRating() != null) { + LOGGER.info("Star Rating: {}", hotel.getStarRating().getValue()); + } + // To get the guest rating of the property. The value is between 1.0 and 5.0 + // in a 0.1 increment. + if (StringUtils.isNotEmpty(hotel.getGuestRating())) { + LOGGER.info("Guest Rating: {}", hotel.getGuestRating()); + } + // To get the total number of reviews for the property + if (hotel.getGuestReviewCount() != null) { + LOGGER.info("Review Count: {}", hotel.getGuestReviewCount()); + } + if (hotel.getRoomTypes() != null && !hotel.getRoomTypes().isEmpty()) { + // To get the first room type information + RoomType roomType = hotel.getRoomTypes().get(0); + if (StringUtils.isNotEmpty(roomType.getDescription())) { + LOGGER.info("Room Type: {}", roomType.getDescription()); + } + if (roomType.getPrice() != null) { + // To get the total price of the room type + if (roomType.getPrice().getTotalPrice() != null) { + LOGGER.info("Price: {}, Currency: {}", + roomType.getPrice().getTotalPrice().getValue(), + roomType.getPrice().getTotalPrice().getCurrency()); + } + // To get the average nightly rate of the room type + if (roomType.getPrice().getAvgNightlyRate() != null) { + LOGGER.info("Average Nightly Rate: {}, Currency: {}", + roomType.getPrice().getAvgNightlyRate().getValue(), + roomType.getPrice().getAvgNightlyRate().getCurrency()); + } + } + // To get the free cancellation flag of the selected room + if (roomType.getRatePlans() != null && !roomType.getRatePlans().isEmpty() + && roomType.getRatePlans().get(0).getCancellationPolicy() != null) { + LOGGER.info("Free Cancellation: {}", + roomType.getRatePlans().get(0).getCancellationPolicy().getFreeCancellation()); + } + if (roomType.getLinks() != null) { + // To get the deeplink to the Expedia Web Search Result Page + if (roomType.getLinks().getWebSearchResult() != null) { + LOGGER.info("WebSearchResult Link: {}", + roomType.getLinks().getWebSearchResult().getHref()); + } + // To get the deeplink to the Expedia Web Details Page + if (roomType.getLinks().getWebDetails() != null) { + LOGGER.info("WebDetails Link: {}", roomType.getLinks().getWebDetails().getHref()); + } + } + } + LOGGER.info( + "==================================== Property End ===================================="); + }); + LOGGER.info("====================== Step III: DisplayResult Executed ========================"); + } +} diff --git a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/ListingsQuickStartScenario.java b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/ListingsQuickStartScenario.java index 2591d50..141eb76 100644 --- a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/ListingsQuickStartScenario.java +++ b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/ListingsQuickStartScenario.java @@ -128,13 +128,13 @@ public void run() { // To access the properties, iterate through the list of hotel properties hotelListingsResponse.getHotels().forEach(hotel -> { - LOGGER.info( - "=================================== Property Start ==================================="); // Check if the property is available if (Hotel.Status.AVAILABLE != hotel.getStatus()) { - LOGGER.info("Property is not available."); + LOGGER.info("Property {} is not available.", hotel.getId()); return; } + LOGGER.info( + "=================================== Property Start ==================================="); // To get the property name if (StringUtils.isNotEmpty(hotel.getName())) { LOGGER.info("Property Name: {}", hotel.getName()); diff --git a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/QuotesQuickStartScenario.java b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/QuotesQuickStartScenario.java index 8855780..7e1f807 100644 --- a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/QuotesQuickStartScenario.java +++ b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/QuotesQuickStartScenario.java @@ -32,7 +32,8 @@ import org.slf4j.LoggerFactory; /** - * This example demonstrates how to use quotes api with simple search. + * This example demonstrates how to search for property quotes with property IDs in + * Lodging Quotes API. * Note: this is a Vrbo scenario. You need a key that is enabled for Vrbo brand to run this. */ public class QuotesQuickStartScenario implements VrboScenario { @@ -73,7 +74,7 @@ public void run() { // Check-out 10 days from now .checkOut(LocalDate.now().plusDays(10)) // Set of Expedia Property IDs. - .propertyIds(new HashSet<>(Arrays.asList("87704892", "12410858"))) + .propertyIds(new HashSet<>(Arrays.asList("87704892", "36960201"))) // The links to return, WEB includes WS (Web Search Result Page) and // WD (Web Details Page) .links(Collections.singletonList(GetLodgingQuotesOperationParams.Links.WEB)) diff --git a/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/VrboPropertySearchEndToEndScenario.java b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/VrboPropertySearchEndToEndScenario.java new file mode 100644 index 0000000..e4396c7 --- /dev/null +++ b/examples/src/main/java/com/expediagroup/sdk/xap/examples/scenarios/lodging/VrboPropertySearchEndToEndScenario.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2024 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.sdk.xap.examples.scenarios.lodging; + +import com.expediagroup.sdk.core.model.Response; +import com.expediagroup.sdk.xap.client.XapClient; +import com.expediagroup.sdk.xap.models.LodgingQuotesResponse; +import com.expediagroup.sdk.xap.models.LodgingRoomType; +import com.expediagroup.sdk.xap.models.PresignedUrlResponse; +import com.expediagroup.sdk.xap.models.Property; +import com.expediagroup.sdk.xap.models.Room; +import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperation; +import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperationParams; +import com.expediagroup.sdk.xap.operations.GetLodgingQuotesOperation; +import com.expediagroup.sdk.xap.operations.GetLodgingQuotesOperationParams; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This example demonstrates how to retrieve accessible Vrbo property ids and location content from + * SDP DownloadURL API and then get the prices of these properties using the Lodging Quotes API. + * + *

This is a common scenario for Vrbo partners. In practice, you can cache the property id + * list along with content that does not change frequently (name, description, address, amenities, + * etc.) to reduce heavy API calls and get only the prices of these properties in real-time from + * the Lodging Quotes API. + * + *

Note: this is a Vrbo scenario. You need a key that is enabled for Vrbo brand to run this. + */ +public class VrboPropertySearchEndToEndScenario implements VrboScenario { + + private final XapClient client = createClient(); + + private static final Logger LOGGER = + LoggerFactory.getLogger(VrboPropertySearchEndToEndScenario.class); + + /** + * This field limits the number of line to read from the SDP DownloadURL API Listings file to + * reduce time to run the example. + * If the first 20 properties from the file are not accessible OR available when you run this + * example, it may end with "No accessible property ids found." OR NO_RESULT_FOUND. In that case, + * you can adjust the property count to get more properties. + */ + private static final int SAMPLE_ITEMS_RESTRICTION = 20; + + /** + * A property id to location map. This mocks a cache in this example to store the static content + * of the properties. + */ + private static final Map PROPERTY_ID_AND_LOCATION_CACHE = new HashMap<>(); + + public static void main(String[] args) { + new VrboPropertySearchEndToEndScenario().run(); + System.exit(0); + } + + @Override + public void run() { + LOGGER.info( + "====================== Running VrboPropertySearchEndToEndScenario ======================"); + + List propertyIds = getPropertyIdsFromDownloadUrl(); + cachePropertyLocationFromDownloadUrl(propertyIds); + LodgingQuotesResponse lodgingQuotesResponse = getPropertyPriceFromLodgingQuotes(propertyIds); + displayResult(lodgingQuotesResponse); + + LOGGER.info( + "======================= End VrboPropertySearchEndToEndScenario ========================="); + } + + private List getPropertyIdsFromDownloadUrl() { + LOGGER.info( + "==================== Executing Step I: getPropertyIdsFromDownloadUrl ==================="); + + GetFeedDownloadUrlOperationParams getPropertyIdListParams = + GetFeedDownloadUrlOperationParams.builder() + // Use the type VACATION_RENTAL to get the list of accessible Vrbo property ids. + .type(GetFeedDownloadUrlOperationParams.Type.VACATION_RENTAL) + // Without any filters, this operation will return the information of all Vrbo + // properties in en_US by default. + .build(); + + Response downloadUrlListingsResponse = + client.execute(new GetFeedDownloadUrlOperation(getPropertyIdListParams)); + + if (downloadUrlListingsResponse.getData() == null + || downloadUrlListingsResponse.getData().getBestMatchedFile() == null) { + throw new IllegalStateException("No vacation rental file found"); + } + + // The download URL points to a zip file containing various jsonl files. + // Each line in the jsonl files contains a json object representing a Vrbo property. + // For demonstration purposes, we will only read a few properties from the file without + // downloading the entire file. + String vacationRentalDownloadUrl = downloadUrlListingsResponse.getData() + .getBestMatchedFile() + .getDownloadUrl(); + LOGGER.info("Vacation Rental Download URL: {}", vacationRentalDownloadUrl); + + // Read property ids from the file. + List propertyIds = getPropertyIdsFromVacationRentalFile(vacationRentalDownloadUrl + ); + + if (propertyIds.isEmpty()) { + throw new IllegalStateException("No accessible Vrbo property ids found."); + } + LOGGER.info("Accessible Vrbo Property Ids: {}", propertyIds); + + LOGGER.info( + "==================== Step I: getPropertyIdsFromDownloadUrl Executed ===================="); + return propertyIds; + } + + /** + * Cache the location content from SDP DownloadURL API. + * + * @param propertyIds The property ids that need the location content. + */ + private void cachePropertyLocationFromDownloadUrl(List propertyIds) { + LOGGER.info( + "================ Executing Step II: CachePropertyLocationFromDownloadUrl ==============="); + GetFeedDownloadUrlOperationParams getPropertyLocationParams = + GetFeedDownloadUrlOperationParams.builder() + // Use the type LOCATIONS to get the address of accessible properties. + .type(GetFeedDownloadUrlOperationParams.Type.LOCATIONS) + // Filter the properties by brand. + .brand(GetFeedDownloadUrlOperationParams.Brand.VRBO) + .build(); + + Response downloadUrlLocationsResponse = + client.execute(new GetFeedDownloadUrlOperation(getPropertyLocationParams)); + + if (downloadUrlLocationsResponse.getData() == null + || downloadUrlLocationsResponse.getData().getBestMatchedFile() == null) { + throw new IllegalStateException("No location file found"); + } + + String locationsDownloadUrl = downloadUrlLocationsResponse.getData() + .getBestMatchedFile() + .getDownloadUrl(); + LOGGER.info("Locations Download URL: {}", locationsDownloadUrl); + + // Read and cache property locations from the file. + cachePropertyLocationFromLocationsFile(locationsDownloadUrl, propertyIds); + + LOGGER.info( + "================= Step II: CachePropertyLocationFromDownloadUrl Executed ==============="); + } + + /** + * Get prices of the properties using the Lodging Quotes API. + * + * @param propertyIds The property ids to get the prices. + * @return The response of the Lodging Quotes API. + */ + private LodgingQuotesResponse getPropertyPriceFromLodgingQuotes(List propertyIds) { + LOGGER.info( + "================= Executing Step III: GetPropertyPriceFromLodgingQuotes ================"); + + // Build the occupancy + ArrayList rooms = new ArrayList<>(); + // The first room, with 2 adult + rooms.add(Room.builder().adults(2L).childAges(null).build()); + + // Build the query parameters with GetLodgingQuotesOperationParams + GetLodgingQuotesOperationParams quotesOperationParams = + GetLodgingQuotesOperationParams.builder() + .partnerTransactionId(PARTNER_TRANSACTION_ID) + // Check-in 5 days from now + .checkIn(LocalDate.now().plusDays(5)) + // Check-out 10 days from now + .checkOut(LocalDate.now().plusDays(10)) + // Set of Expedia Property IDs. + .propertyIds(new HashSet<>(propertyIds)) + // The links to return, WEB includes WS (Web Search Result Page) and + // WD (Web Details Page) + .links(Collections.singletonList(GetLodgingQuotesOperationParams.Links.WEB)) + .rooms(rooms) + .build(); + + LodgingQuotesResponse lodgingQuotesResponse = + client.execute(new GetLodgingQuotesOperation(quotesOperationParams)) + .getData(); + + LOGGER.info( + "================= Step III: GetPropertyPriceFromLodgingQuotes Executed ================="); + return lodgingQuotesResponse; + } + + /** + * Reads given number of property ids from the file pointed by the download URL. + * + * @param downloadUrl The download URL of the zip file containing the property information. + * @return A list of property ids read from the file. + */ + private List getPropertyIdsFromVacationRentalFile(String downloadUrl) { + List propertyIds = new ArrayList<>(); + HttpURLConnection connection = null; + try { + // Open a connection to the URL + URL url = new URL(downloadUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + + try (ZipInputStream zipStream = new ZipInputStream(connection.getInputStream())) { + ZipEntry entry; + while ((entry = zipStream.getNextEntry()) != null) { + if (entry.getName().endsWith(".jsonl")) { + LOGGER.info("Reading property ids from file: {}", entry.getName()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipStream))) { + String line; + ObjectMapper objectMapper = new ObjectMapper(); + while ((line = reader.readLine()) != null + && propertyIds.size() < SAMPLE_ITEMS_RESTRICTION) { + // Parse the property id from the json object + // An example json line from the jsonl file: + /* + { + "propertyId": { + "expedia": "1234567", + "hcom": "987654321", + "vrbo": "123.1234567.7654321" + }, + "country": "France", + "propertySize": { + "measurement": 441, + "units": "SQUARE_FEET" + }, + "maxOccupancy": 4, + "bathrooms": { + "numberOfBathrooms": 1 + }, + "bedrooms": { + "numberOfBedrooms": 2 + }, + "houseRules": { + "partyOrEventRules": { + "partiesOrEventsPermitted": false, + "ownerPartyFreeText": "No events allowed" + }, + "smokingRules": { + "smokingPermitted": false, + "ownerSmokingFreeText": "Smoking is not permitted" + }, + "petRules": { + "petsPermitted": true, + "ownerPetsFreeText": "Pets allowed" + }, + "childRules": { + "childrenPermitted": true, + "ownerChildrenFreeText": "Children allowed: ages 0-17 " + } + }, + "propertyManager": { + "name": "RĂ©sidences Louis", + "hostType": "Professional" + }, + "premierHost": true, + "propertyLiveDate": "2022-05-31" + } + */ + JsonNode jsonNode = objectMapper.readTree(line); + propertyIds.add(jsonNode.get("propertyId").get("expedia").asText()); + } + } + } + } + } + + } catch (IOException e) { + LOGGER.error("Error reading property ids from download URL", e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + return propertyIds; + } + + /** + * Caches the location content of the properties from the file pointed by the download URL. + * + * @param locationsDownloadUrl The download URL of the zip file containing the property locations. + * @param propertyIds The property ids to get the location content. + */ + private void cachePropertyLocationFromLocationsFile(String locationsDownloadUrl, + List propertyIds) { + HttpURLConnection connection = null; + try { + // Open a connection to the URL + URL url = new URL(locationsDownloadUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + + try (ZipInputStream zipStream = new ZipInputStream(connection.getInputStream())) { + ZipEntry entry; + while ((entry = zipStream.getNextEntry()) != null) { + if (entry.getName().endsWith(".jsonl")) { + LOGGER.info("Reading property locations from file: {}", entry.getName()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipStream))) { + String line; + ObjectMapper objectMapper = new ObjectMapper(); + while ((line = reader.readLine()) != null + && PROPERTY_ID_AND_LOCATION_CACHE.size() < propertyIds.size()) { + // Parse the property location from the json object + // An example json line from the jsonl file: + /* + { + "propertyId": { + "expedia": "1234567", + "hcom": "987654321", + "vrbo": "123.1234567.1234567" + }, + "propertyType": { + "id": 16, + "name": "Apartment" + }, + "propertyName": "Vrbo Property Name", + "address1": "", + "address2": "", + "city": "Newark", + "province": "Delaware", + "country": "United States", + "postalCode": "19711", + "geoLocation": { + "latitude": "10.999999", + "longitude": "-10.999999", + "obfuscated": false + }, + "locationAttribute": { + "neighborhood": { + "id": "553248635976468695", + "name": "Westmoreland" + }, + "city": { + "id": "8946", + "name": "Newark" + }, + "region": { + "id": "6055689", + "name": "North Wilmington" + }, + "airport": { + "id": "6028579", + "code": "ILG", + "name": "Wilmington, DE (ILG-New Castle)", + "distance": "13.17", + "unit": "km" + }, + "distanceFromCityCenter": { + "distance": "1.24", + "unit": "km" + } + } + } + */ + JsonNode jsonNode = objectMapper.readTree(line); + // Check if the property id is in the list + if (propertyIds.contains(jsonNode.get("propertyId").get("expedia").asText())) { + // Get the location content of the property + String location = jsonNode.get("propertyName").asText() + ", " + + jsonNode.get("city").asText() + ", " + + jsonNode.get("province").asText() + ", " + + jsonNode.get("country").asText(); + // Store the location content in the cache + PROPERTY_ID_AND_LOCATION_CACHE.put( + jsonNode.get("propertyId") + .get("expedia") + .asText(), + location); + } + } + } + } + } + } + + } catch (IOException e) { + LOGGER.error("Error reading property locations from download URL", e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + /** + * Display the result of the operations. + * + * @param lodgingQuotesResponse The response of the Lodging Quotes API. + */ + private static void displayResult(LodgingQuotesResponse lodgingQuotesResponse) { + LOGGER.info("======================= Executing Step IV: DisplayResult ======================="); + if (lodgingQuotesResponse == null || lodgingQuotesResponse.getProperties() == null + || lodgingQuotesResponse.getProperties().isEmpty()) { + throw new IllegalStateException("No properties found."); + } + + // The HotelListingsResponse contains a transaction ID for troubleshooting + LOGGER.info("Transaction ID: {}", lodgingQuotesResponse.getTransactionId()); + + // To access the properties, iterate through the list of hotel properties + lodgingQuotesResponse.getProperties().forEach(property -> { + // Check if the property is available + if (Property.Status.AVAILABLE != property.getStatus()) { + LOGGER.info("Property {} is not available.", property.getId()); + return; + } + LOGGER.info( + "=================================== Property Start ==================================="); + String propertyId = property.getId(); + + // Get the location content of the property from the cache + LOGGER.info("Property Id: {}", propertyId); + LOGGER.info("Cached Property Location: {}", PROPERTY_ID_AND_LOCATION_CACHE.get(propertyId)); + + // Get the price of the property from the room type + if (property.getRoomTypes() != null && !property.getRoomTypes().isEmpty()) { + // To get the first room type information + LodgingRoomType roomType = property.getRoomTypes().get(0); + + if (roomType.getPrice() != null) { + // To get the total price of the room type + if (roomType.getPrice().getTotalPrice() != null) { + LOGGER.info("Price: {}, Currency: {}", + roomType.getPrice().getTotalPrice().getValue(), + roomType.getPrice().getTotalPrice().getCurrency()); + } + // To get the average nightly rate of the room type + if (roomType.getPrice().getAvgNightlyRate() != null) { + LOGGER.info("Average Nightly Rate: {}, Currency: {}", + roomType.getPrice().getAvgNightlyRate().getValue(), + roomType.getPrice().getAvgNightlyRate().getCurrency()); + } + } + // To get the free cancellation flag of the selected room + if (roomType.getRatePlans() != null && !roomType.getRatePlans().isEmpty() + && roomType.getRatePlans().get(0).getCancellationPolicy() != null) { + LOGGER.info("Free Cancellation: {}", + roomType.getRatePlans().get(0).getCancellationPolicy().getFreeCancellation()); + } + if (roomType.getLinks() != null) { + // To get the deeplink to the website Search Result Page + if (roomType.getLinks().getWebSearchResult() != null) { + LOGGER.info("WebSearchResult Link: {}", + roomType.getLinks().getWebSearchResult().getHref()); + } + // To get the deeplink to the website Details Page + if (roomType.getLinks().getWebDetails() != null) { + LOGGER.info("WebDetails Link: {}", roomType.getLinks().getWebDetails().getHref()); + } + } + } + LOGGER.info( + "==================================== Property End ===================================="); + }); + LOGGER.info("======================= Step IV: DisplayResult Executed ========================"); + } +}