Google Flights Integration Plugin (tqgflights)¶
Overview¶
The tqgflights module integrates Google Flights data via the RapidAPI DataCrawler API. It provides flight search functionality for the TripMaker portal, including outbound search, return flight selection, and airport lookup.
API Provider: google-flights2.p.rapidapi.com (RapidAPI DataCrawler)
Module: tqgflights
Package: com.perun.tlinq.client.googleflights
Architecture¶
RapidAPI (Google Flights DataCrawler)
↓ JSON (HTTP GET)
DTO Layer (dto/)
↓ Gson deserialization
Native Entities (entity/)
↓ Entity Transformer (XML field mapping)
Canonical Entities (CFlightOffer, CItinerary, CItinerarySegment)
↓
FlightSearchFacade → Persistent Search (nts.searchbase / nts.searchres)
↓
FlightApi (REST)
Data Flow¶
- Search request →
FlightApi.doSearchFlights()→FlightSearchFacade.executeSearch() - API call →
GFFlightSearchService.searchFlights()builds query params, calls RapidAPI - Response parsing →
GoogleFlightsClientService.parseSearchResponse()deserializes JSON intoGFSearchResponseDTO - DTO → Native entity →
GFFlightOffer.fromRaw(GFRawItinerary)converts DTOs to native entities - Native → Canonical →
EntityTransformeruses XML field mappings (flight-entities.xml) - Persistence → Results serialized as JSON, stored in
nts.searchres.sourceObject - Pagination →
FlightSearchFacade.getIndividualSearchResults()reads fromnts.searchresbysortIdxrange, deserializes back toCFlightOffervia Gson
Package Structure¶
dto/ — JSON-serializable DTOs¶
Pure POJOs with @SerializedName annotations matching the API response field names. Deserialized directly via gson.fromJson(json, GFSearchResponse.class) — no manual JSON navigation.
| Class | Purpose | Key Fields |
|---|---|---|
GFSearchResponse |
Top-level response wrapper | status, message, data |
GFSearchResponse.Data |
Data container | itineraries |
GFSearchResponse.Itineraries |
Flight result arrays | topFlights[], otherFlights[] |
GFRawItinerary |
A single flight result | duration, flights[], layovers[], price, stops, airline_logo, next_token |
GFRawFlight |
A flight segment | departure_airport, arrival_airport, duration, airline, flight_number, aircraft |
GFAirport |
Airport with time | airport_name, airport_code, time (with getNormalizedTime() for ISO conversion) |
GFDuration |
Duration object | raw (minutes), text (display string), with toIsoDuration() converter |
GFRawLayover |
Layover info | airport_code, airport_name, duration (minutes), city |
entity/ — Native Entities¶
Bridge between DTOs and canonical entities. Each has a fromRaw(DTO) factory method and @TlinqEntityField annotations for the entity transformer.
| Class | Purpose | Mapped To |
|---|---|---|
GFFlightOffer |
Flight offer (one or two itineraries) | CFlightOffer |
GFItinerary |
An itinerary (outbound or return) | CItinerary |
GFSegment |
A flight segment within an itinerary | CItinerarySegment |
GFLocation |
Airport search result | CLocation |
service/ — Services¶
| Class | Purpose |
|---|---|
GoogleFlightsClientService |
Base service with HTTP execution and response parsing |
GFFlightSearchService |
Flight search, return flight search, offer pricing |
GFAirportSearchService |
Airport/location search by keyword |
GoogleFlightsServiceFactory |
Service factory, HTTP client, payload logging |
config/ — Configuration¶
| Class | Purpose |
|---|---|
GoogleFlightsClientConfig |
Reads googleflights-client.xml, provides API key, URLs, defaults |
API Response Format¶
searchFlights Response¶
{
"status": true,
"message": "Success",
"timestamp": 1772621755891,
"data": {
"itineraries": {
"topFlights": [ ... ],
"otherFlights": [ ... ]
}
}
}
Each itinerary in topFlights / otherFlights:
{
"departure_time": "03-04-2026 03:00 AM",
"arrival_time": "03-04-2026 11:35 AM",
"duration": { "raw": 635, "text": "10 hr 35 min" },
"flights": [
{
"departure_airport": { "airport_name": "...", "airport_code": "DXB", "time": "2026-4-3 03:00" },
"arrival_airport": { "airport_name": "...", "airport_code": "SAW", "time": "2026-4-3 07:00" },
"duration": { "raw": 300, "text": "5 hr 0 min" },
"airline": "AJet",
"airline_logo": "https://...",
"flight_number": "VF 144",
"aircraft": "Airbus A321neo"
}
],
"layovers": [
{ "airport_code": "SAW", "duration": 100, "duration_label": "1 hr 40 min", "city": "Istanbul" }
],
"price": 1033,
"stops": 0,
"airline_logo": "https://...",
"next_token": "W1sxLDEs..."
}
Key Data Transformations¶
| API Field | DTO Field | Native Entity Field | Transformation |
|---|---|---|---|
duration.raw (minutes) |
GFDuration.raw |
GFItinerary.duration / GFSegment.duration |
toIsoDuration() → "PT5H30M" |
departure_airport.time |
GFAirport.time |
GFSegment.departureTime |
getNormalizedTime() → "2026-04-03T03:00:00" |
flight_number ("VF 144") |
GFRawFlight.flightNumber |
GFSegment.carrierCode + flightNumber |
Split on first space |
| Entire itinerary JSON | — | GFFlightOffer.offerObject |
Compressed with TypeUtil.zip() |
| Route + times | — | GFFlightOffer.id |
MD5 hash of concatenated flight numbers + departure times |
Duration Fallback¶
If the itinerary-level duration is not present in the API response, GFItinerary.fromRaw() computes a fallback by summing individual segment durations. This is an approximation (excludes layover time) but ensures the duration column is never empty.
Lenient Number Parsing¶
The external API may return non-numeric strings for numeric fields (e.g., "price": "unavailable"). GoogleFlightsClientService.parseSearchResponse() configures Gson with custom TypeAdapter classes for Double and Integer that return null instead of throwing NumberFormatException when encountering non-numeric values. This means:
GFRawItinerary.pricemay benull→CFlightOffer.grandTotalwill benullGFDuration.rawmay benull→ duration methods handle this gracefullyGFRawLayover.durationmay benull→ layover duration will be absent
The frontend displays "N/A" for unavailable prices and disables action buttons for those flights.
Configuration¶
File: config/googleflights-client.xml
<GoogleFlightsPluginConfig>
<PluginProperties>
<property name="rapidapi.key" value="##gf.rapidapi.key"/>
<property name="rapidapi.host" value="google-flights2.p.rapidapi.com"/>
<property name="api.baseUrl" value="https://google-flights2.p.rapidapi.com/api/v1"/>
<property name="default.currency" value="AED"/>
<property name="default.country" value="AE"/>
<property name="default.language" value="en-US"/>
<property name="search.showHidden" value="1"/>
<property name="search.type" value="best"/>
<property name="search.maxReturnFetch" value="5"/>
<property name="airport.refresh-interval" value="72"/>
<property name="api.logPayloads" value="true"/>
<property name="api.logDir" value="logs/googleflights"/>
</PluginProperties>
...
</GoogleFlightsPluginConfig>
| Property | Description | Default |
|---|---|---|
rapidapi.key |
RapidAPI subscription key (secret-substituted) | — |
rapidapi.host |
RapidAPI host header | google-flights2.p.rapidapi.com |
api.baseUrl |
Base URL for API calls | https://google-flights2.p.rapidapi.com/api/v1 |
default.currency |
Currency sent to API when none specified | AED |
default.country |
Country code for search context | AE |
default.language |
Language code for results | en-US |
search.showHidden |
Include hidden/budget flights | 1 |
search.type |
Search optimization (best, cheapest) |
best |
search.maxReturnFetch |
Max outbound offers to fetch return legs for | 5 |
api.logPayloads |
Enable request/response payload logging | true |
api.logDir |
Directory for payload log files | logs/googleflights |
Payload Logging¶
When api.logPayloads=true, every API call writes two files to api.logDir:
- {operation}_{timestamp}_{random}_REQ.txt — the request URL
- {operation}_{timestamp}_{random}_RESP_{status}.json — the response body
This is invaluable for debugging API response format changes.
Entity Mapping (flight-entities.xml)¶
FlightOffer → CFlightOffer¶
<Factory name="GoogleFlightsServiceFactory"
nativeEntity="com.perun.tlinq.client.googleflights.entity.GFFlightOffer">
<FieldMappingList>
<FieldMapping targetField="id" sourceField="id" mapping="DirectMapping"/>
<FieldMapping targetField="source" sourceField="source" mapping="DirectMapping"/>
<FieldMapping targetField="oneWay" sourceField="oneWay" mapping="DirectMapping"/>
<FieldMapping targetField="currency" sourceField="currency" mapping="DirectMapping"/>
<FieldMapping targetField="totalAmount" sourceField="totalAmount" mapping="DirectMapping"/>
<FieldMapping targetField="grandTotal" sourceField="grandTotal" mapping="DirectMapping"/>
<FieldMapping targetField="airlineLogo" sourceField="airlineLogo" mapping="DirectMapping"/>
<FieldMapping targetField="nextToken" sourceField="nextToken" mapping="DirectMapping"/>
<FieldMapping targetField="itineraries" targetFieldEntity="Itinerary"
sourceField="foItinerary" mapping="ArrayMapping"/>
</FieldMappingList>
</Factory>
Itinerary → CItinerary¶
<Factory name="GoogleFlightsServiceFactory"
nativeEntity="com.perun.tlinq.client.googleflights.entity.GFItinerary">
<FieldMappingList>
<FieldMapping targetField="duration" sourceField="duration" mapping="DirectMapping"/>
<FieldMapping targetField="segments" targetFieldEntity="ItinerarySegment"
sourceField="flightSegments" mapping="ArrayMapping"/>
</FieldMappingList>
</Factory>
ItinerarySegment → CItinerarySegment¶
<Factory name="GoogleFlightsServiceFactory"
nativeEntity="com.perun.tlinq.client.googleflights.entity.GFSegment">
<FieldMappingList>
<FieldMapping targetField="carrierCode" sourceField="carrierCode" mapping="DirectMapping"/>
<FieldMapping targetField="carrierName" sourceField="carrierName" mapping="DirectMapping"/>
<FieldMapping targetField="flightNumber" sourceField="flightNumber" mapping="DirectMapping"/>
<FieldMapping targetField="departureAirportCode" sourceField="departureAirport" mapping="DirectMapping"/>
<FieldMapping targetField="departureAirportName" sourceField="departureAirportName" mapping="DirectMapping"/>
<FieldMapping targetField="departureTime" sourceField="departureTime" mapping="DirectMapping"/>
<FieldMapping targetField="arrivalAirportCode" sourceField="arrivalAirport" mapping="DirectMapping"/>
<FieldMapping targetField="arrivalAirportName" sourceField="arrivalAirportName" mapping="DirectMapping"/>
<FieldMapping targetField="arrivalTime" sourceField="arrivalTime" mapping="DirectMapping"/>
<FieldMapping targetField="duration" sourceField="duration" mapping="DirectMapping"/>
<FieldMapping targetField="numberOfStops" sourceField="numberOfStops" mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
Round-Trip Flow¶
- User searches with a return date →
searchFlightscalled withreturn_dateparam - API returns outbound results with
next_tokenon each itinerary - User selects an outbound flight → frontend calls
flight/search/returnwith thesearchIdandofferIndex - Backend extracts
next_tokenfrom the stored offer, callsgetNextFlightsAPI - Return results stored as a separate persistent search
- User selects return flight → both flights added to itinerary