Skip to content

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

  1. Search requestFlightApi.doSearchFlights()FlightSearchFacade.executeSearch()
  2. API callGFFlightSearchService.searchFlights() builds query params, calls RapidAPI
  3. Response parsingGoogleFlightsClientService.parseSearchResponse() deserializes JSON into GFSearchResponse DTO
  4. DTO → Native entityGFFlightOffer.fromRaw(GFRawItinerary) converts DTOs to native entities
  5. Native → CanonicalEntityTransformer uses XML field mappings (flight-entities.xml)
  6. Persistence → Results serialized as JSON, stored in nts.searchres.sourceObject
  7. PaginationFlightSearchFacade.getIndividualSearchResults() reads from nts.searchres by sortIdx range, deserializes back to CFlightOffer via 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.price may be nullCFlightOffer.grandTotal will be null
  • GFDuration.raw may be null → duration methods handle this gracefully
  • GFRawLayover.duration may be null → 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

  1. User searches with a return date → searchFlights called with return_date param
  2. API returns outbound results with next_token on each itinerary
  3. User selects an outbound flight → frontend calls flight/search/return with the searchId and offerIndex
  4. Backend extracts next_token from the stored offer, calls getNextFlights API
  5. Return results stored as a separate persistent search
  6. User selects return flight → both flights added to itinerary