using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.RegularExpressions; namespace PagerParser; public enum AlertLevel { Code1 = 1, Code3 = 3 } [Flags] public enum EmergencyService { Ambulance = 0x01, Fire = 0x02, Police = 0x04, Rescue = 0x08, SES = 0x10 } [Index(nameof(Message))] public class PagerMessage { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int PagerMessageId { get; set; } public DateTime Timestamp { get; set; } public string Message { get; set; } public virtual ParsedPagerMessage? ParsedMessage { get; set; } public override int GetHashCode() => (Timestamp, Message).GetHashCode(); } [Index(nameof(FirecomJobNo))] public class ParsedPagerMessage { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ParsedPagerMessageId { get; set; } public int FirecomJobNo { get; set; } public string AssignmentArea { get; set; } public string JobType { get; set; } public AlertLevel AlertLevel { get; set; } public string Description { get; set; } public int? MelwaysMapNo { get; set; } public string? MelwaysGrid { get; set; } public int? GridReference { get; set; } public EmergencyService AttendingServices { get; set; } public string? Note { get; set; } public int? FireGroundChannel { get; set; } public string PageDestination { get; set; } public virtual GpsPosition? GpsPosition { get; set; } [ForeignKey(nameof(PagerMessage))] public virtual PagerMessage OriginalMessage { get; set; } } public interface IPagerMessageParserService { public ParsedPagerMessage Parse(string message); public ParsedPagerMessage? TryParse(string message); } // We run the pager message parser as a service with a single instance // as this allows us to pre-compile the parsing regular expression // which will help when potentially processing 1000's of pager messages. // // Running a single instance of the service isn't an issue, as this // service is only used by the fetch service for parsing new messages. public class PagerMessageParserService : IPagerMessageParserService { private const string Pattern = @"^@@ALERT\s+([A-Z]*[0-9]+)\s+([A-Z&]+)C([13])\s+(\*\s+)?(.*)\s+M\s+(\d+)\s+([A-Z]\d+)\s+\((\d+)\)\s+(\*\s+[^*]+\*\s+)?([AFPRS]+)\s+(([A-Z0-9]+\s)+)F([0-9]+)\s+\[([A-Z]+)\]$"; private Regex pageMessageRegex = new Regex(Pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); public ParsedPagerMessage Parse(string message) { ParsedPagerMessage m = new(); var match = pageMessageRegex.Match(message); if(!match.Success) throw new ArgumentException(); // Parse non-optional message components m.AssignmentArea = match.Groups[1].Value; m.JobType = match.Groups[2].Value; m.Description = match.Groups[5].Value; m.FirecomJobNo = Int32.Parse(match.Groups[13].Value); m.PageDestination = match.Groups[14].Value; // Parse optional message components if(match.Groups[6].Success) m.MelwaysMapNo = Int32.Parse(match.Groups[6].Value); if(match.Groups[7].Success) m.MelwaysGrid = match.Groups[7].Value; if(match.Groups[8].Success) m.GridReference = Int32.Parse(match.Groups[8].Value); if(match.Groups[9].Success) m.Note = match.Groups[9].Value; // Attempt to extract the fire-ground channel as it can be mixed // into the list of paged services. m.FireGroundChannel = match.Groups[11].Value .Split(" ") .Select(x => Regex.Match(x, "^FGD([0-9]+)$")) .Where(m => m.Success) .Select(m => (int?) int.Parse(m.Groups[1].Value)) .DefaultIfEmpty(null) .First(); switch(match.Groups[3].Value) { case "1": m.AlertLevel = AlertLevel.Code1; break; case "3": m.AlertLevel = AlertLevel.Code3; break; default: throw new ArgumentException(); } // Handle each character to construct a bitmap of attending services foreach(char c in match.Groups[10].Value) { switch(c) { case 'A': m.AttendingServices |= EmergencyService.Ambulance; break; case 'F': m.AttendingServices |= EmergencyService.Fire; break; case 'P': m.AttendingServices |= EmergencyService.Police; break; case 'R': m.AttendingServices |= EmergencyService.Rescue; break; case 'S': m.AttendingServices |= EmergencyService.SES; break; } } return m; } public ParsedPagerMessage? TryParse(string message) { try { return Parse(message); } catch { return null; } } }