Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions sql/cleardown-voyages.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DELETE FROM SIGHTING;
DELETE FROM VOYAGE_EVENT;
DELETE FROM VOYAGE;

DELETE FROM SQLITE_SEQUENCE WHERE name='SIGHTING';
DELETE FROM SQLITE_SEQUENCE WHERE name='VOYAGE_EVENT';
DELETE FROM SQLITE_SEQUENCE WHERE name='VOYAGE';
12 changes: 12 additions & 0 deletions src/ShippingRecorder.Api/Controllers/ExportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class ExportController : Controller
private readonly IBackgroundQueue<OperatorExportWorkItem> _operatorQueue;
private readonly IBackgroundQueue<VesselExportWorkItem> _vesselQueue;
private readonly IBackgroundQueue<VesselTypeExportWorkItem> _vesselTypeQueue;
private readonly IBackgroundQueue<VoyageExportWorkItem> _voyageQueue;
private readonly IBackgroundQueue<SightingExportWorkItem> _sightingQueue;

public ExportController(
Expand All @@ -24,13 +25,15 @@ public ExportController(
IBackgroundQueue<OperatorExportWorkItem> operatorQueue,
IBackgroundQueue<VesselExportWorkItem> vesselQueue,
IBackgroundQueue<VesselTypeExportWorkItem> vesselTypeQueue,
IBackgroundQueue<VoyageExportWorkItem> voyageQueue,
IBackgroundQueue<SightingExportWorkItem> sightingQueue)
{
_countryQueue = countryQueue;
_locationQueue = locationQueue;
_operatorQueue = operatorQueue;
_vesselQueue = vesselQueue;
_vesselTypeQueue = vesselTypeQueue;
_voyageQueue = voyageQueue;
_sightingQueue = sightingQueue;
}

Expand Down Expand Up @@ -87,5 +90,14 @@ public IActionResult ExportSightings([FromBody] SightingExportWorkItem item)
_sightingQueue.Enqueue(item);
return Accepted();
}

[HttpPost]
[Route("voyages")]
public IActionResult ExportVoyages([FromBody] VoyageExportWorkItem item)
{
item.JobName = "Voyage Export";
_voyageQueue.Enqueue(item);
return Accepted();
}
}
}
12 changes: 12 additions & 0 deletions src/ShippingRecorder.Api/Controllers/ImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class ImportController : Controller
private readonly IBackgroundQueue<PortImportWorkItem> _portQueue;
private readonly IBackgroundQueue<VesselImportWorkItem> _vesselQueue;
private readonly IBackgroundQueue<VesselTypeImportWorkItem> _vesselTypeQueue;
private readonly IBackgroundQueue<VoyageImportWorkItem> _voyageQueue;
private readonly IBackgroundQueue<SightingImportWorkItem> _sightingQueue;

public ImportController(
Expand All @@ -26,6 +27,7 @@ public ImportController(
IBackgroundQueue<PortImportWorkItem> portQueue,
IBackgroundQueue<VesselImportWorkItem> vesselQueue,
IBackgroundQueue<VesselTypeImportWorkItem> vesselTypeQueue,
IBackgroundQueue<VoyageImportWorkItem> voyageQueue,
IBackgroundQueue<SightingImportWorkItem> sightingQueue)
{
_countryQueue = countryQueue;
Expand All @@ -34,6 +36,7 @@ public ImportController(
_portQueue = portQueue;
_vesselQueue = vesselQueue;
_vesselTypeQueue = vesselTypeQueue;
_voyageQueue = voyageQueue;
_sightingQueue = sightingQueue;
}

Expand Down Expand Up @@ -99,5 +102,14 @@ public IActionResult ImportSightings([FromBody] SightingImportWorkItem item)
_sightingQueue.Enqueue(item);
return Accepted();
}

[HttpPost]
[Route("voyages")]
public IActionResult ImportVoyages([FromBody] VoyageImportWorkItem item)
{
item.JobName = "Voyage Import";
_voyageQueue.Enqueue(item);
return Accepted();
}
}
}
8 changes: 8 additions & 0 deletions src/ShippingRecorder.Api/Entities/VoyageExportWorkItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ShippingRecorder.Entities.Jobs;

namespace ShippingRecorder.Api.Entities
{
public class VoyageExportWorkItem : ExportWorkItem
{
}
}
8 changes: 8 additions & 0 deletions src/ShippingRecorder.Api/Entities/VoyageImportWorkItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ShippingRecorder.Entities.Jobs;

namespace ShippingRecorder.Api.Entities
{
public class VoyageImportWorkItem : ImportWorkItem
{
}
}
8 changes: 8 additions & 0 deletions src/ShippingRecorder.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ public static void Main(string[] args)
builder.Services.AddSingleton<IBackgroundQueue<PortImportWorkItem>, BackgroundQueue<PortImportWorkItem>>();
builder.Services.AddHostedService<PortImportService>();

// Add the voyage importer hosted service
builder.Services.AddSingleton<IBackgroundQueue<VoyageImportWorkItem>, BackgroundQueue<VoyageImportWorkItem>>();
builder.Services.AddHostedService<VoyageImportService>();

// Add the voyage exporter hosted service
builder.Services.AddSingleton<IBackgroundQueue<VoyageExportWorkItem>, BackgroundQueue<VoyageExportWorkItem>>();
builder.Services.AddHostedService<VoyageExportService>();

// Configure JWT
byte[] key = Encoding.ASCII.GetBytes(settings!.Secret);
builder.Services.AddAuthentication(x =>
Expand Down
46 changes: 46 additions & 0 deletions src/ShippingRecorder.Api/Services/VoyageExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using ShippingRecorder.Api.Entities;
using ShippingRecorder.Api.Interfaces;
using ShippingRecorder.DataExchange.Export;
using Microsoft.Extensions.Options;
using ShippingRecorder.Entities.Interfaces;
using ShippingRecorder.Entities.Config;

namespace ShippingRecorder.Api.Services
{
public class VoyageExportService : BackgroundQueueProcessor<VoyageExportWorkItem>
{
private readonly ShippingRecorderApplicationSettings _settings;

public VoyageExportService(
ILogger<BackgroundQueueProcessor<VoyageExportWorkItem>> logger,
IBackgroundQueue<VoyageExportWorkItem> queue,
IServiceScopeFactory serviceScopeFactory,
IOptions<ShippingRecorderApplicationSettings> settings)
: base(logger, queue, serviceScopeFactory)
{
_settings = settings.Value;
}

/// <summary>
/// Export the data
/// </summary>
/// <param name="item"></param>
/// <param name="factory"></param>
/// <returns></returns>
protected override async Task ProcessWorkItemAsync(VoyageExportWorkItem item, IShippingRecorderFactory factory)
{
// Get the list of items to export
MessageLogger.LogInformation("Retrieving voyages for export");
var voyages = await factory.Voyages.ListAsync(x => true, 1, int.MaxValue).ToListAsync();

// Get the full path to the export file
var filePath = Path.Combine(_settings.ExportPath, item.FileName);

// Export the items
MessageLogger.LogInformation($"Exporting {voyages.Count} voyages to {filePath}");
var exporter = new VoyageExporter(factory);
await exporter.ExportAsync(voyages, filePath);
MessageLogger.LogInformation("Voyage export completed");
}
}
}
46 changes: 46 additions & 0 deletions src/ShippingRecorder.Api/Services/VoyageImportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using ShippingRecorder.Api.Entities;
using ShippingRecorder.Api.Interfaces;
using ShippingRecorder.DataExchange.Import;
using ShippingRecorder.Entities.Interfaces;
using ShippingRecorder.DataExchange.Entities;

namespace ShippingRecorder.Api.Services
{
public class VoyageImportService : BackgroundQueueProcessor<VoyageImportWorkItem>
{
public VoyageImportService(
ILogger<BackgroundQueueProcessor<VoyageImportWorkItem>> logger,
IBackgroundQueue<VoyageImportWorkItem> queue,
IServiceScopeFactory serviceScopeFactory)
: base(logger, queue, serviceScopeFactory)
{
}

/// <summary>
/// Import from the data specified in the work item
/// </summary>
/// <param name="item"></param>
/// <param name="factory"></param>
/// <returns></returns>
protected override async Task ProcessWorkItemAsync(VoyageImportWorkItem item, IShippingRecorderFactory factory)
{
MessageLogger.LogInformation("Voyage import started");
var records = item.Content.Split('\n', StringSplitOptions.RemoveEmptyEntries);

var count = records.Length - 1;
if (count > 0)
{
var messageEnding = (count > 1) ? "s" : "";
MessageLogger.LogInformation($"Importing {records.Count() - 1} voyage{messageEnding}");
var importer = new VoyageImporter(factory, ExportableVoyage.CsvRecordPattern);
await importer.ImportAsync(records);
}
else
{
MessageLogger.LogWarning("No records found to import");
}

MessageLogger.LogInformation("Voyage import completed");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ public ManagerCommandLineParser(IHelpGenerator generator) : base(generator)
Add(CommandLineOptionType.ImportOperators, false, "--import-operators", "-io", "Import operators from a CSV file", 1, 1);
Add(CommandLineOptionType.ImportVesselTypes, false, "--import-vessel-types", "-ivt", "Import vessel types from a CSV file", 1, 1);
Add(CommandLineOptionType.ImportVessels, false, "--import-vessels", "-iv", "Import vessels from a CSV file", 1, 1);
Add(CommandLineOptionType.ImportVoyages, false, "--import-voyages", "-ivo", "Import voyages from a CSV file", 1, 1);
Add(CommandLineOptionType.ImportSightings, false, "--import-sightings", "-is", "Import sightings from a CSV file", 1, 1);
Add(CommandLineOptionType.ExportCountries, false, "--export-countries", "-ec", "Export countries to a CSV file", 1, 1);
Add(CommandLineOptionType.ExportLocations, false, "--export-locations", "-el", "Export locations to a CSV file", 1, 1);
Add(CommandLineOptionType.ExportOperators, false, "--export-operators", "-eo", "Export operators to a CSV file", 1, 1);
Add(CommandLineOptionType.ExportVesselTypes, false, "--export-vessel-typess", "-evt", "Export vessel types to a CSV file", 1, 1);
Add(CommandLineOptionType.ExportVessels, false, "--export-vessels", "-ev", "Export vessels to a CSV file", 1, 1);
Add(CommandLineOptionType.ExportVoyages, false, "--export-voyages", "-evo", "Export voyages to a CSV file", 1, 1);
Add(CommandLineOptionType.ExportSightings, false, "--export-sightings", "-es", "Export sightings to a CSV file", 1, 1);
Add(CommandLineOptionType.AddUser, false, "--add-user", "-au", "Add a user to the database", 2, 2);
Add(CommandLineOptionType.SetPassword, false, "--set-password", "-sp", "Set the password for an existing user", 2, 2);
Expand Down
36 changes: 36 additions & 0 deletions src/ShippingRecorder.Client/ApiClient/VoyageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
using System.Net.Http;
using System.Linq;
using System.Collections.Generic;
using System.IO;

namespace ShippingRecorder.Client.ApiClient
{
public class VoyageClient : ShippingRecorderClientBase, IVoyageClient
{
private const string RouteKey = "Voyage";
private const string ImportRouteKey = "ImportVoyage";
private const string ExportRouteKey = "ExportVoyage";

public VoyageClient(
IShippingRecorderHttpClient client,
Expand Down Expand Up @@ -116,5 +119,38 @@ public async Task<List<Voyage>> ListAsync(long operatorId, int pageNumber, int p
List<Voyage> countries = Deserialize<List<Voyage>>(json);
return countries;
}

/// <summary>
/// Request an import of voyages from the content of a file
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public async Task ImportFromFileContentAsync(string content)
{
dynamic data = new{ Content = content };
var json = Serialize(data);
await SendIndirectAsync(ImportRouteKey, json, HttpMethod.Post);
}

/// <summary>
/// Request an import of voyages given the path to a file
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public async Task ImportFromFileAsync(string filePath)
=> await ImportFromFileContentAsync(File.ReadAllText(filePath));

/// <summary>
/// Request an export of voyages to a named file in the export voyage
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task ExportAsync(string fileName)
{
dynamic data = new{ FileName = fileName };
var json = Serialize(data);
await SendIndirectAsync(ExportRouteKey, json, HttpMethod.Post);
}
}
}
2 changes: 1 addition & 1 deletion src/ShippingRecorder.Client/Interfaces/IVoyageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace ShippingRecorder.Client.Interfaces
{
public interface IVoyageClient
public interface IVoyageClient : IImporter, IExporter
{
Task<Voyage> GetAsync(long id);
Task<Voyage> AddAsync(long operatorId, long vesselId, string number);
Expand Down
10 changes: 7 additions & 3 deletions src/ShippingRecorder.DataExchange/Entities/ExportableSighting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace ShippingRecorder.DataExchange.Entities
[ExcludeFromCodeCoverage]
public class ExportableSighting : ExportableEntityBase
{
public const string CsvRecordPattern = @"^""[0-9]+-[A-Za-z]+-[0-9]+"",""(?!\s*"")[\s\S]*"",""\d{7}"",""True|False"".?$";
public const string CsvRecordPattern = @"^""[0-9]+-[A-Za-z]+-[0-9]+"",""(?!\s*"")[\s\S]*"",""\d{7}"","".*"",""True|False"".?$";

[Export("Date", 1)]
public DateTime Date { get; set; }
Expand All @@ -20,7 +20,10 @@ public class ExportableSighting : ExportableEntityBase
[Export("IMO", 3)]
public string IMO { get; set; }

[Export("My Voyage", 4)]
[Export("Voyage", 4)]
public string VoyageNumber { get; set; }

[Export("My Voyage", 5)]
public bool IsMyVoyage { get; set; }

public static ExportableSighting FromCsv(string record)
Expand All @@ -31,7 +34,8 @@ public static ExportableSighting FromCsv(string record)
Date = DateTime.ParseExact(words[0].Replace("\"", "").Trim(), DateFormat, CultureInfo.CurrentCulture),
Location = words[1].Replace("\"", "").Trim(),
IMO = words[2].Replace("\"", "").Trim().CleanCode(),
IsMyVoyage = words[3].Replace("\"", "").Trim().Equals("True", StringComparison.OrdinalIgnoreCase)
VoyageNumber = words[3].Replace("\"", "").Trim().Clean(),
IsMyVoyage = words[4].Replace("\"", "").Trim().Equals("True", StringComparison.OrdinalIgnoreCase)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ExportableVoyage : ExportableEntityBase
[Export("Event Type", 3)]
public string EventType { get; set; }

[Export("Callsign", 4)]
[Export("Port", 4)]
public string Port { get; set; }

[Export("Date", 5)]
Expand Down
62 changes: 62 additions & 0 deletions src/ShippingRecorder.DataExchange/Export/VoyageExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using ShippingRecorder.DataExchange.Entities;
using ShippingRecorder.DataExchange.Interfaces;
using ShippingRecorder.DataExchange.Extensions;
using ShippingRecorder.Entities.Interfaces;
using System;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using ShippingRecorder.Entities.Db;

namespace ShippingRecorder.DataExchange.Export
{
public class VoyageExporter : IVoyageExporter
{
private readonly IShippingRecorderFactory _factory;

public event EventHandler<ExportEventArgs<ExportableVoyage>> RecordExport;

public VoyageExporter(IShippingRecorderFactory factory)
=> _factory = factory;

/// <summary>
/// Export the voyages to a CSV file
/// </summary>
/// <param name="file"></param>
public async Task ExportAsync(string file)
{
var voyages = await _factory.Voyages.ListAsync(x => true, 1, int.MaxValue).ToListAsync();
await ExportAsync(voyages, file);
}

/// <summary>
/// Export a collection of voyages to a CSV file
/// </summary>
/// <param name="voyages"></param>
/// <param name="file"></param>
#pragma warning disable CS1998
public async Task ExportAsync(IEnumerable<Voyage> voyages, string file)
{
// Convert the voyages to exportable (flattened hierarchy) voyages
var exportable = voyages.ToExportable();

// Configure an exporter to export them
var exporter = new CsvExporter<ExportableVoyage>(ExportableEntityBase.DateFormat);
exporter.RecordExport += OnRecordExported;

// Export the records
exporter.Export(exportable, file, ',');
}
#pragma warning restore CS1998

/// <summary>
/// Handler for voyage export notifications
/// </summary>
/// <param name="_"></param>
/// <param name="e"></param>
private void OnRecordExported(object _, ExportEventArgs<ExportableVoyage> e)
{
RecordExport?.Invoke(this, e);
}
}
}
Loading