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/)¶
- AmdHotel.java - Hotel reference data entity
- Maps to
com.amadeus.resources.Hotel - Contains hotel details: ID, name, location, address, coordinates
-
Key Methods:
fromHotel(Hotel) -
AmdHotelOffer.java - Hotel offer with pricing and availability
- Maps to
com.amadeus.resources.HotelOfferSearch - Contains offer details, pricing, room info, and policies
-
Key Methods:
fromSearch(HotelOfferSearch),getTotalAmountValue(),getBaseAmountValue() -
AmdHotelRoom.java - Room details
-
Room type, category, beds, description
-
AmdHotelPrice.java - Pricing information
-
Currency, total, base, taxes, price variations
-
AmdHotelPolicy.java - Booking policies
- Cancellation, payment, guarantee, deposit policies
- Check-in/check-out times
Services (service/)¶
- 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)
- AmdHotelOfferSearchService.java - Hotel offers operations
List<AmdHotelOffer> searchHotelOffers(RemoteEntityI notUsed)
List<AmdHotelOffer> getHotelOfferById(RemoteEntityI notUsed)
Database (db/)¶
- HotelEntity.java - Hibernate entity for caching hotel reference data locally to reduce API calls
- Table:
amd_hotels - 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/
- CHotelSearch.java - Platform-agnostic search criteria
- Hotel IDs, city code, geocode search
- Date ranges (check-in/check-out)
- Guest criteria (adults, room quantity)
-
Filters (ratings, currency, payment policy, board type)
-
CHotelOffer.java - Platform-agnostic hotel offer
- Hotel information
- Room details
- Pricing information
- Policy information
-
Cancellation details
-
CHotelOfferSet.java - Paginated result set
- Search ID for session tracking
- Page ID for pagination
- Array of offers
- 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¶
- POST /hotel/searchOffers - Search hotel offers
- Required: hotelIds, checkInDate, adults
- Optional: checkOutDate, roomQuantity, currency, paymentPolicy, boardType, bestRateOnly, sortOrder
-
Returns: CHotelOfferSet with first page of results
-
POST /hotel/fetchOfferResults - Fetch paginated results
- Required: session, searchId, page
-
Returns: CHotelOfferSet for specified page
-
POST /hotel/searchByName - Hotel autocomplete
- Required: session, keyword
- Optional: countryCode, subType, max
-
Returns: List of AmdHotel
-
POST /hotel/getByCity - Get hotels by city
- Required: session, cityCode
- Optional: radius, ratings
- 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¶
- Entity Mapping Pattern: Annotation-based declarative field mapping with nested object support, type conversion helpers, and JSON compression for large objects
- Service Factory Pattern: Creating services via AmadeusServiceFactory
- Builder Pattern: Fluent API for entity construction
- Template Method Pattern: Base service class (
AmadeusClientService) with common functionality - DAO Pattern: Database entities separate from business entities
Technical Decisions¶
- String vs Numeric IDs: Hotels use string IDs (hotelId) instead of numeric IDs
- Price Storage: Prices stored as strings to preserve precision
- Offer Flattening: Each HotelOfferSearch can contain multiple offers; currently extracting first offer per hotel
- Caching Strategy: Using database for hotel reference data caching
- Test Framework: JUnit 5 with Mockito for mocking
Performance Considerations¶
- Database Caching: Hotel reference data cached in
amd_hotelstable - Search Sessions: Results cached for 30 minutes
- Pagination: Database queries use offset/limit for efficiency
- Connection Pooling: Hibernate manages database connections
- 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-hotelsGET /v1/reference-data/locations/hotels/by-cityGET /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-offersGET /v3/shopping/hotel-offers/{offerId}
Troubleshooting¶
Common Issues¶
- "Hotel search cannot be executed without session ID"
- Ensure session parameter is provided in request
-
Verify session is valid and not expired
-
"hotelIds parameter is required"
- Provide comma-separated hotel IDs for offer search
-
Use hotel autocomplete to find valid hotel IDs
-
"Date format incorrect"
- Use YYYY-MM-DD format for dates
-
Example: "2024-12-01"
-
"adults parameter must be between 1 and 9"
- Provide valid adults count
- Amadeus API limits to 9 adults per search
Debug Logging¶
Future Enhancements¶
- Additional Endpoints:
- Get hotels by geocode (REST API)
- Get hotels by IDs - bulk lookup (REST API)
-
Advanced filtering (amenities, star rating, etc.)
-
Features:
- Multi-currency support
- Room availability calendar
- Price alerts
-
Favorite hotels
-
Performance:
- Redis caching for search results
- Asynchronous search for multiple hotels
- Result clustering by location