Skip to content

Amadeus Plugin Implementation

Overview

The tqamds module implements Amadeus Hotel Search functionality for the TQPro platform, providing a complete end-to-end hotel search system with session management, pagination, and full API exposure. It provides access to:

  • Hotel Reference Data: Search hotels by ID, city, geocode, or name
  • Hotel Offers Search: Search and retrieve hotel offers with pricing and availability

The implementation follows the same layered architecture as the flight search module, with native Amadeus entities, services, and database caching.

Architecture

Integration Flow

User Request
    |
HotelApi (REST endpoint, tqapi)
    |
HotelSearchFacade (session management, tqapp)
    |
EntityFacade (entity mapping, tqcommon)
    |
AmdHotelOfferSearchService (Amadeus client, tqamds)
    |
Amadeus SDK
    |
Amadeus Hotel Offers API v3

Layer 1: Native Amadeus Entities (tqamds)

Location: tqamds/src/main/java/com/perun/tlinq/client/amadeus/

Entities (entity/)

  1. AmdHotel.java - Hotel reference data entity
  2. Maps to com.amadeus.resources.Hotel
  3. Contains hotel details: ID, name, location, address, coordinates
  4. Key Methods: fromHotel(Hotel)

  5. AmdHotelOffer.java - Hotel offer with pricing and availability

  6. Maps to com.amadeus.resources.HotelOfferSearch
  7. Contains offer details, pricing, room info, and policies
  8. Key Methods: fromSearch(HotelOfferSearch), getTotalAmountValue(), getBaseAmountValue()

  9. AmdHotelRoom.java - Room details

  10. Room type, category, beds, description

  11. AmdHotelPrice.java - Pricing information

  12. Currency, total, base, taxes, price variations

  13. AmdHotelPolicy.java - Booking policies

  14. Cancellation, payment, guarantee, deposit policies
  15. Check-in/check-out times

Services (service/)

  1. AmdHotelRefDataService.java - Hotel reference data operations
List<AmdHotel> getHotelsByIds(RemoteEntityI notUsed)
List<AmdHotel> getHotelsByCity(RemoteEntityI notUsed)
List<AmdHotel> getHotelsByGeocode(RemoteEntityI notUsed)
List<AmdHotel> searchHotelsByName(RemoteEntityI notUsed)
  1. AmdHotelOfferSearchService.java - Hotel offers operations
List<AmdHotelOffer> searchHotelOffers(RemoteEntityI notUsed)
List<AmdHotelOffer> getHotelOfferById(RemoteEntityI notUsed)

Database (db/)

  1. HotelEntity.java - Hibernate entity for caching hotel reference data locally to reduce API calls
  2. Table: amd_hotels
  3. AmdDBSession.java - Updated to register HotelEntity via configuration.addAnnotatedClass(HotelEntity.class)

Layer 2: Common Entities (tqapp)

Location: tqapp/src/main/java/com/perun/tlinq/entity/hotel/

  1. CHotelSearch.java - Platform-agnostic search criteria
  2. Hotel IDs, city code, geocode search
  3. Date ranges (check-in/check-out)
  4. Guest criteria (adults, room quantity)
  5. Filters (ratings, currency, payment policy, board type)

  6. CHotelOffer.java - Platform-agnostic hotel offer

  7. Hotel information
  8. Room details
  9. Pricing information
  10. Policy information
  11. Cancellation details

  12. CHotelOfferSet.java - Paginated result set

  13. Search ID for session tracking
  14. Page ID for pagination
  15. Array of offers
  16. Total results count

Layer 3: Facade Layer (tqapp)

Location: tqapp/src/main/java/com/perun/tlinq/entity/hotel/

HotelSearchFacade.java

  • Session management with 30-minute expiry
  • Search execution and result persistence
  • Pagination support with configurable page size
  • Sorting: Price ASC/DESC, Name ASC
  • Integration with CPersistentSearch and CSearchResult

Key Methods: - executeSearch(CHotelSearch, resPerPage, sortMethod) - Execute search and return first page - getSearchResults(searchId, pageId, resPerPage) - Retrieve paginated results - executeBasicSearch(CHotelSearch, resPerPage) - Call Amadeus API via EntityFacade

Layer 4: API Layer (tqapi)

Location: tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java

Endpoints

  1. POST /hotel/searchOffers - Search hotel offers
  2. Required: hotelIds, checkInDate, adults
  3. Optional: checkOutDate, roomQuantity, currency, paymentPolicy, boardType, bestRateOnly, sortOrder
  4. Returns: CHotelOfferSet with first page of results

  5. POST /hotel/fetchOfferResults - Fetch paginated results

  6. Required: session, searchId, page
  7. Returns: CHotelOfferSet for specified page

  8. POST /hotel/searchByName - Hotel autocomplete

  9. Required: session, keyword
  10. Optional: countryCode, subType, max
  11. Returns: List of AmdHotel

  12. POST /hotel/getByCity - Get hotels by city

  13. Required: session, cityCode
  14. Optional: radius, ratings
  15. Returns: List of AmdHotel

Layer 5: Configuration

Amadeus Service Configuration

File: config/amadeus-client.xml

Six service configurations: - getHotelsByIds - getHotelsByCity - getHotelsByGeocode - searchHotelsByName - searchHotelOffers - getHotelOfferById

Example configuration:

<service name="getHotelsByIds">
    <class>com.perun.tlinq.client.amadeus.service.AmdHotelRefDataService</class>
    <method>getHotelsByIds</method>
</service>

<service name="searchHotelOffers">
    <class>com.perun.tlinq.client.amadeus.service.AmdHotelOfferSearchService</class>
    <method>searchHotelOffers</method>
</service>

Entity Mappings

File: config/entities/hotel-entities.xml

Three entity configurations: - HotelOffer (CHotelOffer <-> AmdHotelOffer) - HotelByName (AmdHotel <-> AmdHotel) - HotelByCity (AmdHotel <-> AmdHotel)

Mapping details: - Mapping Type: DirectMapping for most fields - Source: Native Amadeus entities (AmdHotel, etc.) - Target: Common entities (CHotel, etc.) - Factory: AmadeusServiceFactory

Complete File List

tqamds Module (11 files)

File Type
tqamds/src/main/java/com/perun/tlinq/client/amadeus/entity/AmdHotel.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/entity/AmdHotelOffer.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/entity/AmdHotelRoom.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/entity/AmdHotelPrice.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/entity/AmdHotelPolicy.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/service/AmdHotelRefDataService.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/service/AmdHotelOfferSearchService.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/db/HotelEntity.java New
tqamds/src/main/java/com/perun/tlinq/client/amadeus/db/AmdDBSession.java Modified
tqamds/src/test/java/com/perun/tlinq/client/amadeus/service/AmdHotelRefDataServiceTest.java New
tqamds/src/test/java/com/perun/tlinq/client/amadeus/service/AmdHotelOfferSearchServiceTest.java New

tqapp Module (4 files)

File Type
tqapp/src/main/java/com/perun/tlinq/entity/hotel/CHotelSearch.java New
tqapp/src/main/java/com/perun/tlinq/entity/hotel/CHotelOffer.java New
tqapp/src/main/java/com/perun/tlinq/entity/hotel/CHotelOfferSet.java New
tqapp/src/main/java/com/perun/tlinq/entity/hotel/HotelSearchFacade.java New

tqapi Module (1 file)

File Type
tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java Modified (4 endpoints added)

Configuration (2 files)

File Type
config/amadeus-client.xml Modified (6 services added)
config/entities/hotel-entities.xml Modified (3 entities added)

Search Session Flow

1. User initiates search via POST /hotel/searchOffers
2. HotelSearchFacade.executeSearch():
   - Calls Amadeus API via EntityFacade
   - Sorts results by price/name
   - Saves to database:
     * CPersistentSearch (search metadata)
     * CSearchResult (individual offers)
   - Returns first page (10 results)
3. User requests next page via POST /hotel/fetchOfferResults
4. HotelSearchFacade.getSearchResults():
   - Retrieves from database using searchId
   - Applies pagination offset
   - Returns requested page

Java Usage Examples

Get Hotels by IDs

AmadeusServiceFactory factory = AmadeusServiceFactory.getInstance();
AmdHotelRefDataService service = (AmdHotelRefDataService)
    factory.createService("getHotelsByIds", null);

service.addNamedParameter("hotelIds", "ADPAR001,ADPAR002,ADPAR003");

List<AmdHotel> hotels = service.getHotelsByIds(null);

for (AmdHotel hotel : hotels) {
    System.out.println("Hotel: " + hotel.getName());
    System.out.println("City: " + hotel.getCityName());
    System.out.println("Address: " + hotel.getAddressLines());
}

Get Hotels by City

AmdHotelRefDataService service = (AmdHotelRefDataService)
    factory.createService("getHotelsByCity", null);

service.addNamedParameter("cityCode", "PAR");
service.addNamedParameter("radius", 10);
service.addNamedParameter("radiusUnit", "KM");
service.addNamedParameter("ratings", "3,4,5");

List<AmdHotel> hotels = service.getHotelsByCity(null);

Get Hotels by Geocode

AmdHotelRefDataService service = (AmdHotelRefDataService)
    factory.createService("getHotelsByGeocode", null);

service.addNamedParameter("latitude", 48.8566);
service.addNamedParameter("longitude", 2.3522);
service.addNamedParameter("radius", 5);
service.addNamedParameter("hotelSource", "GDS");

List<AmdHotel> hotels = service.getHotelsByGeocode(null);

for (AmdHotel hotel : hotels) {
    System.out.println(hotel.getName() + " - " +
                       hotel.getDistance() + " " +
                       hotel.getDistanceUnit());
}

Search Hotels by Name (Autocomplete)

AmdHotelRefDataService service = (AmdHotelRefDataService)
    factory.createService("searchHotelsByName", null);

service.addNamedParameter("keyword", "PARI");
service.addNamedParameter("subType", "HOTEL_GDS");
service.addNamedParameter("countryCode", "FR");
service.addNamedParameter("lang", "EN");
service.addNamedParameter("max", 20);

List<AmdHotel> hotels = service.searchHotelsByName(null);

Search Hotel Offers

AmdHotelOfferSearchService service = (AmdHotelOfferSearchService)
    factory.createService("searchHotelOffers", null);

// Required parameters
service.addNamedParameter("hotelIds", "MCLONGHM,ADPAR001");
service.addNamedParameter("adults", 2);
service.addNamedParameter("checkInDate", "2024-12-01");

// Optional parameters
service.addNamedParameter("checkOutDate", "2024-12-05");
service.addNamedParameter("roomQuantity", 1);
service.addNamedParameter("currency", "EUR");
service.addNamedParameter("paymentPolicy", "NONE");
service.addNamedParameter("boardType", "BREAKFAST");
service.addNamedParameter("bestRateOnly", true);

List<AmdHotelOffer> offers = service.searchHotelOffers(null);

for (AmdHotelOffer offer : offers) {
    System.out.println("Hotel: " + offer.getHotelName());
    System.out.println("Price: " + offer.getCurrency() + " " + offer.getTotalAmount());
    System.out.println("Check-in: " + offer.getCheckInDate());
    System.out.println("Check-out: " + offer.getCheckOutDate());

    if (offer.getRoom() != null) {
        System.out.println("Room Type: " + offer.getRoom().getType());
        System.out.println("Category: " + offer.getRoom().getCategory());
    }

    if (offer.getPolicies() != null) {
        System.out.println("Cancellation: " +
            offer.getPolicies().getCancellationType());
        System.out.println("Deadline: " +
            offer.getPolicies().getCancellationDeadline());
    }
}

Get Hotel Offer by ID

AmdHotelOfferSearchService service = (AmdHotelOfferSearchService)
    factory.createService("getHotelOfferById", null);

service.addNamedParameter("offerId", "QF3MNOBDQ8");
service.addNamedParameter("lang", "EN");

List<AmdHotelOffer> offers = service.getHotelOfferById(null);

if (!offers.isEmpty()) {
    AmdHotelOffer offer = offers.get(0);
    // Process single offer...
}

REST API Usage Examples

Search Hotel Offers

curl -X POST http://localhost:11080/api/hotel/searchOffers \
  -H "Content-Type: application/json" \
  -d '{
    "session": "user-session-id",
    "hotelIds": "MCLONGHM,ADPAR001",
    "checkInDate": "2024-12-01",
    "checkOutDate": "2024-12-05",
    "adults": 2,
    "roomQuantity": 1,
    "currency": "EUR",
    "bestRateOnly": true,
    "sortOrder": 1
  }'

Response:

{
  "status": 0,
  "message": "Success",
  "data": {
    "searchId": "abc123...",
    "pageId": 1,
    "totalRes": 25,
    "offers": [
      {
        "offerId": "QF3MNOBDQ8",
        "hotelId": "MCLONGHM",
        "hotelName": "Hotel Example",
        "cityCode": "LON",
        "checkInDate": "2024-12-01",
        "checkOutDate": "2024-12-05",
        "roomType": "DOUBLE",
        "currencyCode": "EUR",
        "totalAmount": 450.00,
        "cancellationType": "FREE_CANCELLATION",
        "cancellationDeadline": "2024-11-30T18:00:00"
      }
    ]
  }
}

Fetch Next Page

curl -X POST http://localhost:11080/api/hotel/fetchOfferResults \
  -H "Content-Type: application/json" \
  -d '{
    "session": "user-session-id",
    "searchId": "abc123...",
    "page": 2
  }'

Search Hotels by Name

curl -X POST http://localhost:11080/api/hotel/searchByName \
  -H "Content-Type: application/json" \
  -d '{
    "session": "user-session-id",
    "keyword": "PARI",
    "countryCode": "FR",
    "subType": "HOTEL_GDS",
    "max": 20
  }'

Get Hotels by City

curl -X POST http://localhost:11080/api/hotel/getByCity \
  -H "Content-Type: application/json" \
  -d '{
    "session": "user-session-id",
    "cityCode": "PAR",
    "radius": 10,
    "ratings": "3,4,5"
  }'

Parameter Reference

Hotel Reference Data Parameters

getHotelsByIds

  • hotelIds (required): Comma-separated hotel IDs (e.g., "ADPAR001,ADPAR002")

getHotelsByCity

  • cityCode (required): IATA city code (e.g., "PAR", "LON", "NYC")
  • radius (optional): Search radius (default depends on API)
  • radiusUnit (optional): "KM" or "MILE"
  • ratings (optional): Comma-separated ratings "1,2,3,4,5"
  • hotelSource (optional): "GDS" or "ALL"

getHotelsByGeocode

  • latitude (required): Latitude in decimal degrees
  • longitude (required): Longitude in decimal degrees
  • radius (optional): Search radius (default: 5 km)
  • radiusUnit (optional): "KM" or "MILE"
  • ratings (optional): Comma-separated ratings
  • hotelSource (optional): "GDS" or "ALL"

searchHotelsByName

  • keyword (required): Search keyword (min 3 characters)
  • subType (optional): "HOTEL_GDS" or "HOTEL_LEISURE"
  • countryCode (optional): ISO country code
  • lang (optional): Language code (e.g., "EN", "FR")
  • max (optional): Maximum results (default: 20)

Hotel Offers Parameters

searchHotelOffers

  • hotelIds (required): Comma-separated hotel IDs (max 250)
  • adults (required): Number of adults (1-9)
  • checkInDate (required): Check-in date (YYYY-MM-DD)
  • checkOutDate (optional): Check-out date (YYYY-MM-DD)
  • roomQuantity (optional): Number of rooms (1-9, default: 1)
  • priceRange (optional): Price range "min-max"
  • currency (optional): Currency code (e.g., "USD", "EUR")
  • paymentPolicy (optional): "NONE", "DEPOSIT", "GUARANTEE"
  • boardType (optional): "ROOM_ONLY", "BREAKFAST", "HALF_BOARD", "FULL_BOARD", "ALL_INCLUSIVE"
  • includeClosed (optional): Include closed hotels (default: false)
  • bestRateOnly (optional): Return only best rate (default: true)
  • view (optional): "FULL", "LIGHT", "NONE"
  • lang (optional): Language code

getHotelOfferById

  • offerId (required): Offer ID from search results
  • lang (optional): Language code

Database Schema

The HotelEntity table (amd_hotels) is created automatically by Hibernate:

CREATE TABLE amd_hotels (
    id INT AUTO_INCREMENT PRIMARY KEY,
    hotel_id VARCHAR(50) UNIQUE NOT NULL,
    chain_code VARCHAR(10),
    name VARCHAR(255),
    iata_code VARCHAR(10),
    city_code VARCHAR(10),
    city_name VARCHAR(100),
    country_code VARCHAR(10),
    state_code VARCHAR(10),
    postal_code VARCHAR(20),
    address_line1 VARCHAR(255),
    address_line2 VARCHAR(255),
    latitude DOUBLE,
    longitude DOUBLE,
    last_update VARCHAR(50)
);

Error Handling

All services throw TlinqClientException with appropriate error codes:

Error Code Meaning
TlinqErr.MISSING_PARAMETER A required parameter was not provided
TlinqErr.INVALID_PARAMETER A parameter value is invalid
TlinqErr.REMOTE_ERROR The Amadeus API returned an error
TlinqErr.INVALID_FORMAT Date or other format is incorrect

Example error handling:

try {
    List<AmdHotel> hotels = service.getHotelsByCity(null);
} catch (TlinqClientException ex) {
    switch (ex.getErrorCode()) {
        case TlinqErr.MISSING_PARAMETER:
            // Handle missing required parameter
            break;
        case TlinqErr.INVALID_PARAMETER:
            // Handle invalid parameter value
            break;
        case TlinqErr.REMOTE_ERROR:
            // Handle Amadeus API error
            break;
        default:
            // Handle other errors
            break;
    }
    logger.error("Error: " + ex.getMessage(), ex);
}

Errors are logged and returned with appropriate HTTP status codes.

Testing

Running Tests

# Test hotel reference data service
./gradlew test --tests "com.perun.tlinq.client.amadeus.service.AmdHotelRefDataServiceTest"

# Test hotel offer search service
./gradlew test --tests "com.perun.tlinq.client.amadeus.service.AmdHotelOfferSearchServiceTest"

# Run all Amadeus hotel tests
./gradlew test --tests "com.perun.tlinq.client.amadeus.service.AmdHotel*"

Test Coverage

  • AmdHotelRefDataServiceTest: 10 tests covering all methods and error scenarios
  • Success scenarios for all methods
  • Missing parameter validation
  • API error handling
  • Empty result handling

  • AmdHotelOfferSearchServiceTest: 12 tests covering all methods, validation, and edge cases

  • Success scenarios with required and optional parameters
  • Missing parameter validation (hotelIds, adults, checkInDate)
  • Invalid parameter validation (adults count)
  • API error handling
  • Offer not found scenarios

Design Patterns

  1. Entity Mapping Pattern: Annotation-based declarative field mapping with nested object support, type conversion helpers, and JSON compression for large objects
  2. Service Factory Pattern: Creating services via AmadeusServiceFactory
  3. Builder Pattern: Fluent API for entity construction
  4. Template Method Pattern: Base service class (AmadeusClientService) with common functionality
  5. DAO Pattern: Database entities separate from business entities

Technical Decisions

  1. String vs Numeric IDs: Hotels use string IDs (hotelId) instead of numeric IDs
  2. Price Storage: Prices stored as strings to preserve precision
  3. Offer Flattening: Each HotelOfferSearch can contain multiple offers; currently extracting first offer per hotel
  4. Caching Strategy: Using database for hotel reference data caching
  5. Test Framework: JUnit 5 with Mockito for mocking

Performance Considerations

  1. Database Caching: Hotel reference data cached in amd_hotels table
  2. Search Sessions: Results cached for 30 minutes
  3. Pagination: Database queries use offset/limit for efficiency
  4. Connection Pooling: Hibernate manages database connections
  5. API Rate Limiting: Handled by Amadeus SDK

Security

  • Session validation required for all endpoints
  • SQL injection prevention via parameterized queries
  • Input validation for all parameters
  • Proper exception handling to prevent information leakage

Dependencies

All required dependencies are already present in the project: - Amadeus Java SDK v9.1.0 - Hibernate for ORM - Gson for JSON processing - JUnit 5 for testing - Mockito for mocking

Amadeus API Endpoints Consumed

Hotel List API

  • GET /v1/reference-data/locations/hotels/by-hotels
  • GET /v1/reference-data/locations/hotels/by-city
  • GET /v1/reference-data/locations/hotels/by-geocode

Hotel Search API

  • GET /v1/reference-data/locations/hotel

Hotel Offers Search API v3

  • GET /v3/shopping/hotel-offers
  • GET /v3/shopping/hotel-offers/{offerId}

Troubleshooting

Common Issues

  1. "Hotel search cannot be executed without session ID"
  2. Ensure session parameter is provided in request
  3. Verify session is valid and not expired

  4. "hotelIds parameter is required"

  5. Provide comma-separated hotel IDs for offer search
  6. Use hotel autocomplete to find valid hotel IDs

  7. "Date format incorrect"

  8. Use YYYY-MM-DD format for dates
  9. Example: "2024-12-01"

  10. "adults parameter must be between 1 and 9"

  11. Provide valid adults count
  12. Amadeus API limits to 9 adults per search

Debug Logging

logging.level.com.perun.tlinq.client.amadeus=DEBUG
logging.level.com.perun.tlinq.entity.hotel=DEBUG

Future Enhancements

  1. Additional Endpoints:
  2. Get hotels by geocode (REST API)
  3. Get hotels by IDs - bulk lookup (REST API)
  4. Advanced filtering (amenities, star rating, etc.)

  5. Features:

  6. Multi-currency support
  7. Room availability calendar
  8. Price alerts
  9. Favorite hotels

  10. Performance:

  11. Redis caching for search results
  12. Asynchronous search for multiple hotels
  13. Result clustering by location

References