-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from davewalker5/FR-99-External-Aircraft-Looku…
…p-API FR-99 AeroDataBox API Integration
- Loading branch information
Showing
32 changed files
with
1,065 additions
and
157 deletions.
There are no files selected for viewing
205 changes: 205 additions & 0 deletions
205
src/FlightRecorder.BusinessLogic/Api/AeroDataBox/AeroDataBoxAircraftApi.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
using FlightRecorder.Entities.Api; | ||
using FlightRecorder.Entities.Interfaces; | ||
using FlightRecorder.Entities.Logging; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
using System.Threading.Tasks; | ||
|
||
namespace FlightRecorder.BusinessLogic.Api.AeroDataBox | ||
{ | ||
public partial class AeroDataBoxAircraftApi : ExternalApiBase, IAircraftApi | ||
{ | ||
private const string DateFormat = "yyyy-MM-dd"; | ||
private const double DaysPerYear = 365.2425; | ||
|
||
private readonly string _baseAddress; | ||
private readonly string _host; | ||
private readonly string _key; | ||
|
||
public AeroDataBoxAircraftApi( | ||
IFlightRecorderLogger logger, | ||
IFlightRecorderHttpClient client, | ||
string url, | ||
string key) | ||
: base(logger, client) | ||
{ | ||
_baseAddress = url; | ||
_key = key; | ||
|
||
// The URL contains the protocol, host and base route (if any), but we need to extract the host name only | ||
// to pass in the headers as the RapidAPI host, so capture the host and the full URL | ||
Uri uri = new(url); | ||
_host = uri.Host; | ||
} | ||
|
||
/// <summary> | ||
/// Look up aircraft details given a registration number | ||
/// </summary> | ||
/// <param name="registration"></param> | ||
/// <returns></returns> | ||
public async Task<Dictionary<ApiPropertyType, string>> LookupAircraftByRegistration(string registration) | ||
{ | ||
Logger.LogMessage(Severity.Info, $"Looking up aircraft with registration number {registration}"); | ||
var properties = await MakeApiRequest($"reg/{registration}"); | ||
return properties; | ||
} | ||
|
||
/// <summary> | ||
/// Look up aircraft details given a 24-bit ICAO address | ||
/// </summary> | ||
/// <param name="address"></param> | ||
/// <returns></returns> | ||
public async Task<Dictionary<ApiPropertyType, string>> LookupAircraftByICAOAddress(string address) | ||
{ | ||
Logger.LogMessage(Severity.Info, $"Looking up aircraft with 24-bit ICAO address {address}"); | ||
var properties = await MakeApiRequest($"icao24/{address}"); | ||
return properties; | ||
} | ||
|
||
/// <summary> | ||
/// Make a request for flight details using the specified parameters | ||
/// </summary> | ||
/// <param name="parameters"></param> | ||
/// <returns></returns> | ||
private async Task<Dictionary<ApiPropertyType, string>> MakeApiRequest(string parameters) | ||
{ | ||
Dictionary<ApiPropertyType, string> properties = null; | ||
|
||
// Define the properties to be extracted from the response | ||
List<ApiPropertyDefinition> definitions = new() | ||
{ | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftRegistration, JsonPath = "reg" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftICAOAddress, JsonPath = "hexIcao" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftSerialNumber, JsonPath = "serial" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftRegistrationDate, JsonPath = "registrationDate" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftModel, JsonPath = "model" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftModelCode, JsonPath = "modelCode" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftType, JsonPath = "typeName" }, | ||
new ApiPropertyDefinition{ PropertyType = ApiPropertyType.AircraftProductionLine, JsonPath = "productionLine" }, | ||
}; | ||
|
||
// Set the headers | ||
SetHeaders(new Dictionary<string, string> | ||
{ | ||
{ "X-RapidAPI-Host", _host }, | ||
{ "X-RapidAPI-Key", _key } | ||
}); | ||
|
||
// Make a request for the data from the API | ||
var url = $"{_baseAddress}{parameters}"; | ||
var node = await SendRequest(url); | ||
|
||
if (node != null) | ||
{ | ||
try | ||
{ | ||
// Extract the required properties from the response | ||
properties = GetPropertyValuesFromResponse(node, definitions); | ||
|
||
// Calculate the age of the aircraft and add it to the properties | ||
var age = CalculateAircraftAge(properties[ApiPropertyType.AircraftRegistrationDate]); | ||
properties.Add(ApiPropertyType.AircraftAge, age?.ToString() ?? ""); | ||
|
||
// Determine the manufacturer from the type name and model code | ||
var manufacturer = DetermineManufacturer(properties); | ||
properties.Add(ApiPropertyType.ManufacturerName, manufacturer); | ||
|
||
// Log the properties dictionary | ||
LogProperties(properties!); | ||
} | ||
catch (Exception ex) | ||
{ | ||
var message = $"Error processing response: {ex.Message}"; | ||
Logger.LogMessage(Severity.Error, message); | ||
Logger.LogException(ex); | ||
properties = null; | ||
} | ||
} | ||
|
||
return properties; | ||
} | ||
|
||
/// <summary> | ||
/// Calculate the age of an aircraft from its registration date | ||
/// </summary> | ||
/// <param name="yearOfRegistration"></param> | ||
/// <returns></returns> | ||
private static int? CalculateAircraftAge(string yearOfRegistration) | ||
{ | ||
int? age = null; | ||
|
||
try | ||
{ | ||
// Convert the registration date string to a date then calculate the number of years between then and now | ||
var registered = DateTime.ParseExact(yearOfRegistration, DateFormat, null); | ||
Check warning on line 136 in src/FlightRecorder.BusinessLogic/Api/AeroDataBox/AeroDataBoxAircraftApi.cs
|
||
age = (int)Math.Round((DateTime.Now - registered).TotalDays / DaysPerYear, 0, MidpointRounding.AwayFromZero); | ||
Check warning on line 137 in src/FlightRecorder.BusinessLogic/Api/AeroDataBox/AeroDataBoxAircraftApi.cs
|
||
} | ||
catch | ||
{ | ||
// Malformed year of registration, so we can't return an age | ||
} | ||
|
||
return age; | ||
} | ||
|
||
/// <summary> | ||
/// Determine the aircraft manufacturer's name given the model type name and the production line name | ||
/// </summary> | ||
/// <param name="properties"></param> | ||
/// <returns></returns> | ||
private static string DetermineManufacturer(Dictionary<ApiPropertyType, string> properties) | ||
{ | ||
var builder = new StringBuilder(); | ||
var numbers = MyRegex(); | ||
|
||
// Get the properties of interest from the properties returned by the API | ||
var modelTypeName = properties[ApiPropertyType.AircraftType]; | ||
var productionLine = properties[ApiPropertyType.AircraftProductionLine]; | ||
|
||
// Check the strings have some content | ||
if (!string.IsNullOrEmpty(modelTypeName) && !string.IsNullOrEmpty(productionLine)) | ||
{ | ||
// The manufacturer can be inferred from the properties returned from the API: | ||
// | ||
// Type Name: Boeing 737-800 | ||
// Production Line: Boeing 737 NG | ||
// | ||
// It's the (trimmed) part of the two strings that are identical and (from other | ||
// examples) don't contain numbers, which are unlikely (though not impossible) in | ||
// a manufacturer name | ||
|
||
// Split the two into words | ||
var typeWords = modelTypeName.Split(' '); | ||
var lineWords = productionLine.Split(' '); | ||
|
||
// Use a string builder to build up a string containing only the parts where the words match | ||
for (int i = 0; i < typeWords.Length; i++) | ||
{ | ||
// Compare the word at the current position in the type and production line strings | ||
if (typeWords[i].Equals(lineWords[i], StringComparison.OrdinalIgnoreCase) && !numbers.Match(typeWords[i]).Success) | ||
{ | ||
// The same and not containing numbers, so add this word to the builder (with a preceding | ||
// space if it's not the first word) | ||
if (builder.Length > 0) builder.Append(' '); | ||
builder.Append(typeWords[i]); | ||
} | ||
else | ||
{ | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
|
||
/// <summary> | ||
/// Regular expression to find numbers | ||
/// </summary> | ||
/// <returns></returns> | ||
[GeneratedRegex("[0-9]", RegexOptions.Compiled)] | ||
private static partial Regex MyRegex(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
using System.IO; | ||
|
||
namespace FlightRecorder.BusinessLogic.Config | ||
{ | ||
public static class ApiKeyResolver | ||
{ | ||
/// <summary> | ||
/// Resolve an API key given the value from the configuration file | ||
/// </summary> | ||
/// <param name="configValue"></param> | ||
/// <returns></returns> | ||
public static string ResolveApiKey(string configValue) | ||
{ | ||
string apiKey; | ||
|
||
// If the value from the configuration file is a valid file path, the keys are | ||
// stored separately. This separation allows the API keys not to be published | ||
// as part of the API Docker container image but read from a volume mount | ||
if (File.Exists(configValue)) | ||
{ | ||
apiKey = File.ReadAllText(configValue); | ||
} | ||
else | ||
{ | ||
// Not a path to a file, so just return the configuration value as the key | ||
apiKey = configValue; | ||
} | ||
|
||
return apiKey; | ||
} | ||
|
||
/// <summary> | ||
/// Resolve all the API key definitions in the supplied application settings | ||
/// </summary> | ||
/// <param name="settings"></param> | ||
public static void ResolveAllApiKeys(FlightRecorderApplicationSettings settings) | ||
{ | ||
|
||
// Iterate over the service API key definitions | ||
foreach (var service in settings.ApiServiceKeys) | ||
{ | ||
// Resolve the key for the current service | ||
service.Key = ResolveApiKey(service.Key); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using FlightRecorder.Entities.Interfaces; | ||
using Microsoft.Extensions.Configuration; | ||
|
||
namespace FlightRecorder.BusinessLogic.Config | ||
{ | ||
public abstract class ConfigReader<T> : IConfigReader<T> where T : class | ||
{ | ||
/// <summary> | ||
/// Load and return the application settings from the named JSON-format application settings file | ||
/// </summary> | ||
/// <param name="jsonFileName"></param> | ||
/// <param name="sectionName"></param> | ||
/// <returns></returns> | ||
public virtual T Read(string jsonFileName, string sectionName) | ||
{ | ||
// Set up the configuration reader | ||
IConfiguration configuration = new ConfigurationBuilder() | ||
.AddJsonFile(jsonFileName) | ||
.Build(); | ||
|
||
// Read the application settings section | ||
IConfigurationSection section = configuration.GetSection(sectionName); | ||
var settings = section.Get<T>(); | ||
|
||
return settings; | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/FlightRecorder.BusinessLogic/Config/FlightRecorderApplicationSettings.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using FlightRecorder.Entities.Config; | ||
using FlightRecorder.Entities.Logging; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace FlightRecorder.BusinessLogic.Config | ||
{ | ||
[ExcludeFromCodeCoverage] | ||
public class FlightRecorderApplicationSettings | ||
{ | ||
public string Secret { get; set; } | ||
public int TokenLifespanMinutes { get; set; } | ||
public string LogFile { get; set; } | ||
public Severity MinimumLogLevel { get; set; } | ||
public string SightingsExportPath { get; set; } | ||
public string AirportsExportPath { get; set; } | ||
public string ReportsExportPath { get; set; } | ||
public List<ApiEndpoint> ApiEndpoints { get; set; } | ||
public List<ApiServiceKey> ApiServiceKeys { get; set; } | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/FlightRecorder.BusinessLogic/Config/FlightRecorderConfigReader.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using FlightRecorder.Entities.Interfaces; | ||
|
||
namespace FlightRecorder.BusinessLogic.Config | ||
{ | ||
public class FlightRecorderConfigReader : ConfigReader<FlightRecorderApplicationSettings>, IConfigReader<FlightRecorderApplicationSettings> | ||
{ | ||
/// <summary> | ||
/// Load and return the application settings from the named JSON-format application settings file | ||
/// </summary> | ||
/// <param name="jsonFileName"></param> | ||
/// <param name="sectionName"></param> | ||
/// <returns></returns> | ||
public override FlightRecorderApplicationSettings Read(string jsonFileName, string sectionName) | ||
{ | ||
// Read the settings | ||
var settings = base.Read(jsonFileName, sectionName); | ||
if (settings != null) | ||
{ | ||
// Resolve all the API keys for services where the key is held in a separate file | ||
ApiKeyResolver.ResolveAllApiKeys(settings); | ||
} | ||
|
||
return settings; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.