|
| 1 | +/* |
| 2 | + * Copyright (C) 2024 Expedia, Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package com.expediagroup.sdk.xap.examples.scenarios.lodging; |
| 18 | + |
| 19 | +import com.expediagroup.sdk.core.model.Response; |
| 20 | +import com.expediagroup.sdk.xap.client.XapClient; |
| 21 | +import com.expediagroup.sdk.xap.examples.scenarios.XapScenario; |
| 22 | +import com.expediagroup.sdk.xap.models.Hotel; |
| 23 | +import com.expediagroup.sdk.xap.models.HotelListingsResponse; |
| 24 | +import com.expediagroup.sdk.xap.models.PresignedUrlResponse; |
| 25 | +import com.expediagroup.sdk.xap.models.RoomType; |
| 26 | +import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperation; |
| 27 | +import com.expediagroup.sdk.xap.operations.GetFeedDownloadUrlOperationParams; |
| 28 | +import com.expediagroup.sdk.xap.operations.GetLodgingListingsOperation; |
| 29 | +import com.expediagroup.sdk.xap.operations.GetLodgingListingsOperationParams; |
| 30 | +import com.fasterxml.jackson.databind.JsonNode; |
| 31 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 32 | +import java.io.BufferedReader; |
| 33 | +import java.io.IOException; |
| 34 | +import java.io.InputStreamReader; |
| 35 | +import java.net.HttpURLConnection; |
| 36 | +import java.net.URL; |
| 37 | +import java.time.LocalDate; |
| 38 | +import java.util.ArrayList; |
| 39 | +import java.util.Collections; |
| 40 | +import java.util.HashMap; |
| 41 | +import java.util.HashSet; |
| 42 | +import java.util.List; |
| 43 | +import java.util.Map; |
| 44 | +import java.util.zip.ZipEntry; |
| 45 | +import java.util.zip.ZipInputStream; |
| 46 | +import org.apache.commons.lang3.StringUtils; |
| 47 | +import org.slf4j.Logger; |
| 48 | +import org.slf4j.LoggerFactory; |
| 49 | + |
| 50 | +/** |
| 51 | + * This example demonstrates how to retrieve accessible property ids from SDP DownloadURL API and |
| 52 | + * then get the content and prices of these properties using the Lodging Listings API. |
| 53 | + * |
| 54 | + * <p>This is a common scenario for meta site partners. In practice, you can build a cache with the |
| 55 | + * property id list, content and prices to improve respond time of your pages. |
| 56 | + */ |
| 57 | +public class HotelIdsSearchEndToEndScenario implements XapScenario { |
| 58 | + |
| 59 | + private final XapClient client = createClient(); |
| 60 | + |
| 61 | + private static final Logger LOGGER = |
| 62 | + LoggerFactory.getLogger(HotelIdsSearchEndToEndScenario.class); |
| 63 | + |
| 64 | + /** |
| 65 | + * This field limits the number of line to read from the SDP DownloadURL API Listings file to |
| 66 | + * reduce time to run the example. |
| 67 | + * If the first 20 properties from the file are not accessible OR available when you run this |
| 68 | + * example, it may end with "No accessible property ids found." OR NO_RESULT_FOUND. In that case, |
| 69 | + * you can adjust the property count to get more properties. |
| 70 | + */ |
| 71 | + private static final int SAMPLE_ITEMS_RESTRICTION = 20; |
| 72 | + |
| 73 | + public static void main(String[] args) { |
| 74 | + new HotelIdsSearchEndToEndScenario().run(); |
| 75 | + System.exit(0); |
| 76 | + } |
| 77 | + |
| 78 | + @Override |
| 79 | + public void run() { |
| 80 | + LOGGER.info( |
| 81 | + "======================== Running HotelIdsSearchEndToEndScenario ======================="); |
| 82 | + |
| 83 | + List<String> propertyIds = getPropertyIdsFromDownloadUrl(); |
| 84 | + HotelListingsResponse hotelListingsResponse = getPropertiesFromLodgingListings(propertyIds); |
| 85 | + displayResult(hotelListingsResponse); |
| 86 | + |
| 87 | + LOGGER.info( |
| 88 | + "========================== End HotelIdsSearchEndToEndScenario ========================="); |
| 89 | + } |
| 90 | + |
| 91 | + /** |
| 92 | + * Retrieve accessible property ids from SDP DownloadURL API. |
| 93 | + * |
| 94 | + * @return property ids |
| 95 | + */ |
| 96 | + private List<String> getPropertyIdsFromDownloadUrl() { |
| 97 | + LOGGER.info( |
| 98 | + "==================== Executing Step I: getPropertyIdsFromDownloadUrl ==================="); |
| 99 | + |
| 100 | + GetFeedDownloadUrlOperationParams getPropertyIdListParams = |
| 101 | + GetFeedDownloadUrlOperationParams.builder() |
| 102 | + // Use the type LISTINGS to get the list of accessible property ids. |
| 103 | + .type(GetFeedDownloadUrlOperationParams.Type.LISTINGS) |
| 104 | + // Without any filters, this operation will return the information of all lodging |
| 105 | + // properties in en_US by default. |
| 106 | + .build(); |
| 107 | + |
| 108 | + Response<PresignedUrlResponse> downloadUrlListingsResponse = |
| 109 | + client.execute(new GetFeedDownloadUrlOperation(getPropertyIdListParams)); |
| 110 | + |
| 111 | + if (downloadUrlListingsResponse.getData() == null |
| 112 | + || downloadUrlListingsResponse.getData().getBestMatchedFile() == null) { |
| 113 | + throw new IllegalStateException("No listings file found"); |
| 114 | + } |
| 115 | + |
| 116 | + // The download URL points to a zip file containing various jsonl files. |
| 117 | + // Each line in the jsonl files contains a json object representing a property. |
| 118 | + // For demonstration purposes, we will only read a few properties from the file without |
| 119 | + // downloading the entire file. |
| 120 | + String listingsDownloadUrl = downloadUrlListingsResponse.getData() |
| 121 | + .getBestMatchedFile() |
| 122 | + .getDownloadUrl(); |
| 123 | + LOGGER.info("Listings Download URL: {}", listingsDownloadUrl); |
| 124 | + |
| 125 | + // Read property ids from the file. |
| 126 | + List<String> propertyIds = getPropertyIdsFromListingsFile(listingsDownloadUrl); |
| 127 | + |
| 128 | + if (propertyIds.isEmpty()) { |
| 129 | + throw new IllegalStateException("No accessible property ids found."); |
| 130 | + } |
| 131 | + LOGGER.info("Accessible Property Ids: {}", propertyIds); |
| 132 | + |
| 133 | + LOGGER.info( |
| 134 | + "==================== Step I: getPropertyIdsFromDownloadUrl Executed ===================="); |
| 135 | + return propertyIds; |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Get prices of the properties using the Lodging Listings API. |
| 140 | + * |
| 141 | + * @param propertyIds The property ids to get the prices. |
| 142 | + * @return The response of the Lodging Listings API. |
| 143 | + */ |
| 144 | + private HotelListingsResponse getPropertiesFromLodgingListings(List<String> propertyIds) { |
| 145 | + LOGGER.info( |
| 146 | + "================ Step II: Executing getPropertiesFromLodgingListings ==============="); |
| 147 | + |
| 148 | + GetLodgingListingsOperationParams getLodgingListingsOperationParams = |
| 149 | + GetLodgingListingsOperationParams.builder() |
| 150 | + .partnerTransactionId(PARTNER_TRANSACTION_ID) |
| 151 | + // Use the property ids read from the file |
| 152 | + .ecomHotelIds(new HashSet<>(propertyIds)) |
| 153 | + // The links to return, WEB includes WS (Web Search Result Page) |
| 154 | + // and WD (Web Details Page) |
| 155 | + .links(Collections.singletonList(GetLodgingListingsOperationParams.Links.WEB)) |
| 156 | + // Check-in 5 days from now |
| 157 | + .checkIn(LocalDate.now().plusDays(5)) |
| 158 | + // Check-out 10 days from now |
| 159 | + .checkOut(LocalDate.now().plusDays(10)) |
| 160 | + // Filter the properties that are available only |
| 161 | + .availOnly(true) |
| 162 | + // Use the default occupancy: 2 adults in one room |
| 163 | + .build(); |
| 164 | + |
| 165 | + HotelListingsResponse hotelListingsResponse = |
| 166 | + client.execute(new GetLodgingListingsOperation(getLodgingListingsOperationParams)) |
| 167 | + .getData(); |
| 168 | + |
| 169 | + LOGGER.info( |
| 170 | + "================ Step II: getPropertiesFromLodgingListings Executed ================"); |
| 171 | + return hotelListingsResponse; |
| 172 | + } |
| 173 | + |
| 174 | + /** |
| 175 | + * Reads given number of property ids from the file pointed by the download URL. |
| 176 | + * |
| 177 | + * @param downloadUrl The download URL of the zip file containing the property information. |
| 178 | + * @return A list of property ids read from the file. |
| 179 | + */ |
| 180 | + private List<String> getPropertyIdsFromListingsFile(String downloadUrl) { |
| 181 | + List<String> propertyIds = new ArrayList<>(); |
| 182 | + HttpURLConnection connection = null; |
| 183 | + try { |
| 184 | + // Open a connection to the URL |
| 185 | + URL url = new URL(downloadUrl); |
| 186 | + connection = (HttpURLConnection) url.openConnection(); |
| 187 | + connection.setRequestMethod("GET"); |
| 188 | + connection.setDoInput(true); |
| 189 | + |
| 190 | + try (ZipInputStream zipStream = new ZipInputStream(connection.getInputStream())) { |
| 191 | + ZipEntry entry; |
| 192 | + while ((entry = zipStream.getNextEntry()) != null) { |
| 193 | + if (entry.getName().endsWith(".jsonl")) { |
| 194 | + LOGGER.info("Reading property ids from file: {}", entry.getName()); |
| 195 | + try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipStream))) { |
| 196 | + String line; |
| 197 | + ObjectMapper objectMapper = new ObjectMapper(); |
| 198 | + while ((line = reader.readLine()) != null |
| 199 | + && propertyIds.size() < SAMPLE_ITEMS_RESTRICTION) { |
| 200 | + // Parse the property id from the json object |
| 201 | + // An example json line from the jsonl file: |
| 202 | + /* |
| 203 | + { |
| 204 | + "propertyId": { |
| 205 | + "expedia": "1234567", |
| 206 | + "hcom": "123456789", |
| 207 | + "vrbo": "123.1234567.7654321" |
| 208 | + }, |
| 209 | + "bookable": { |
| 210 | + "expedia": true, |
| 211 | + "hcom": true, |
| 212 | + "vrbo": true |
| 213 | + }, |
| 214 | + "propertyType": { |
| 215 | + "id": 16, |
| 216 | + "name": "Apartment" |
| 217 | + }, |
| 218 | + "lastUpdated": "10-27-2024 13:41:16", |
| 219 | + "country": "France", |
| 220 | + "inventorySource": "vrbo", |
| 221 | + "referencePrice": { |
| 222 | + "value": "89.52", |
| 223 | + "currency": "USD" |
| 224 | + }, |
| 225 | + "vrboPropertyType": { |
| 226 | + "instantBook": true |
| 227 | + } |
| 228 | + } |
| 229 | + */ |
| 230 | + JsonNode jsonNode = objectMapper.readTree(line); |
| 231 | + // Check if the property is accessible from Lodging Listings API |
| 232 | + // (Vrbo properties that are not instantBookable are not accessible for now) |
| 233 | + if (!jsonNode.get("propertyId").get("vrbo").asText().isEmpty() |
| 234 | + && jsonNode.has("vrboPropertyType") |
| 235 | + && !jsonNode.get("vrboPropertyType").get("instantBook").asBoolean() |
| 236 | + ) { |
| 237 | + // Skip the property if it is a not instant bookable Vrbo property |
| 238 | + continue; |
| 239 | + } else { |
| 240 | + // Get the Expedia property id for the Lodging Listings API |
| 241 | + propertyIds.add(jsonNode.get("propertyId").get("expedia").asText()); |
| 242 | + } |
| 243 | + } |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + } catch (IOException e) { |
| 250 | + LOGGER.error("Error reading property ids from download URL", e); |
| 251 | + } finally { |
| 252 | + if (connection != null) { |
| 253 | + connection.disconnect(); |
| 254 | + } |
| 255 | + } |
| 256 | + return propertyIds; |
| 257 | + } |
| 258 | + |
| 259 | + /** |
| 260 | + * Display the result of the operations. |
| 261 | + * |
| 262 | + * @param hotelListingsResponse The response of the Lodging Listings API. |
| 263 | + */ |
| 264 | + private static void displayResult(HotelListingsResponse hotelListingsResponse) { |
| 265 | + LOGGER.info("====================== Executing Step III: DisplayResult ======================="); |
| 266 | + if (hotelListingsResponse == null || hotelListingsResponse.getHotels() == null |
| 267 | + || hotelListingsResponse.getHotels().isEmpty()) { |
| 268 | + throw new IllegalStateException("No properties found."); |
| 269 | + } |
| 270 | + |
| 271 | + // The HotelListingsResponse contains a transaction ID for troubleshooting |
| 272 | + LOGGER.info("Transaction ID: {}", hotelListingsResponse.getTransactionId()); |
| 273 | + |
| 274 | + // To access the properties, iterate through the list of hotel properties |
| 275 | + hotelListingsResponse.getHotels().forEach(hotel -> { |
| 276 | + // Check if the property is available |
| 277 | + if (Hotel.Status.AVAILABLE != hotel.getStatus()) { |
| 278 | + LOGGER.info("Property {} is not available.", hotel.getId()); |
| 279 | + return; |
| 280 | + } |
| 281 | + LOGGER.info( |
| 282 | + "=================================== Property Start ==================================="); |
| 283 | + // To get the property name |
| 284 | + if (StringUtils.isNotEmpty(hotel.getName())) { |
| 285 | + LOGGER.info("Property Name: {}", hotel.getName()); |
| 286 | + } |
| 287 | + // To get the property address |
| 288 | + if (hotel.getLocation() != null) { |
| 289 | + LOGGER.info("Property Address: {}", hotel.getLocation().getAddress()); |
| 290 | + } |
| 291 | + // To get the property thumbnail URL |
| 292 | + if (StringUtils.isNotEmpty(hotel.getThumbnailUrl())) { |
| 293 | + LOGGER.info("Thumbnail URL: {}", hotel.getThumbnailUrl()); |
| 294 | + } |
| 295 | + // To get the star rating of the property. The value is between 1.0 and 5.0 |
| 296 | + // in a 0.5 increment. |
| 297 | + if (hotel.getStarRating() != null) { |
| 298 | + LOGGER.info("Star Rating: {}", hotel.getStarRating().getValue()); |
| 299 | + } |
| 300 | + // To get the guest rating of the property. The value is between 1.0 and 5.0 |
| 301 | + // in a 0.1 increment. |
| 302 | + if (StringUtils.isNotEmpty(hotel.getGuestRating())) { |
| 303 | + LOGGER.info("Guest Rating: {}", hotel.getGuestRating()); |
| 304 | + } |
| 305 | + // To get the total number of reviews for the property |
| 306 | + if (hotel.getGuestReviewCount() != null) { |
| 307 | + LOGGER.info("Review Count: {}", hotel.getGuestReviewCount()); |
| 308 | + } |
| 309 | + if (hotel.getRoomTypes() != null && !hotel.getRoomTypes().isEmpty()) { |
| 310 | + // To get the first room type information |
| 311 | + RoomType roomType = hotel.getRoomTypes().get(0); |
| 312 | + if (StringUtils.isNotEmpty(roomType.getDescription())) { |
| 313 | + LOGGER.info("Room Type: {}", roomType.getDescription()); |
| 314 | + } |
| 315 | + if (roomType.getPrice() != null) { |
| 316 | + // To get the total price of the room type |
| 317 | + if (roomType.getPrice().getTotalPrice() != null) { |
| 318 | + LOGGER.info("Price: {}, Currency: {}", |
| 319 | + roomType.getPrice().getTotalPrice().getValue(), |
| 320 | + roomType.getPrice().getTotalPrice().getCurrency()); |
| 321 | + } |
| 322 | + // To get the average nightly rate of the room type |
| 323 | + if (roomType.getPrice().getAvgNightlyRate() != null) { |
| 324 | + LOGGER.info("Average Nightly Rate: {}, Currency: {}", |
| 325 | + roomType.getPrice().getAvgNightlyRate().getValue(), |
| 326 | + roomType.getPrice().getAvgNightlyRate().getCurrency()); |
| 327 | + } |
| 328 | + } |
| 329 | + // To get the free cancellation flag of the selected room |
| 330 | + if (roomType.getRatePlans() != null && !roomType.getRatePlans().isEmpty() |
| 331 | + && roomType.getRatePlans().get(0).getCancellationPolicy() != null) { |
| 332 | + LOGGER.info("Free Cancellation: {}", |
| 333 | + roomType.getRatePlans().get(0).getCancellationPolicy().getFreeCancellation()); |
| 334 | + } |
| 335 | + if (roomType.getLinks() != null) { |
| 336 | + // To get the deeplink to the Expedia Web Search Result Page |
| 337 | + if (roomType.getLinks().getWebSearchResult() != null) { |
| 338 | + LOGGER.info("WebSearchResult Link: {}", |
| 339 | + roomType.getLinks().getWebSearchResult().getHref()); |
| 340 | + } |
| 341 | + // To get the deeplink to the Expedia Web Details Page |
| 342 | + if (roomType.getLinks().getWebDetails() != null) { |
| 343 | + LOGGER.info("WebDetails Link: {}", roomType.getLinks().getWebDetails().getHref()); |
| 344 | + } |
| 345 | + } |
| 346 | + } |
| 347 | + LOGGER.info( |
| 348 | + "==================================== Property End ===================================="); |
| 349 | + }); |
| 350 | + LOGGER.info("====================== Step III: DisplayResult Executed ========================"); |
| 351 | + } |
| 352 | +} |
0 commit comments