Files
go-caatsm/internal/adapter/parser/aviation.go
T

409 lines
12 KiB
Go
Raw Normal View History

package parser
import (
"caatsm/internal/domain"
"caatsm/internal/adapter/dto"
"errors"
2024-07-20 22:21:12 +08:00
"fmt"
2024-07-20 22:33:22 +08:00
"regexp"
"strings"
2024-07-24 14:21:02 +08:00
"sync"
2024-07-20 22:25:28 +08:00
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
2024-07-20 18:55:28 +08:00
const (
SSR = "ssr"
DepartureCode = "dep"
DepartureTime = "dep_time"
ArrivalCode = "arr"
ArrivalTime = "arr_time"
DestinationCode = "dest"
OtherInfo = "other"
2024-07-26 09:59:38 +08:00
ReferenceData = "reference_data"
Aircraft = "aircraft"
CategorySurveillance = "surve"
Indicator = "indicator"
Other = "other"
AircraftID = "aircraft"
Surveillance = "surve"
Speed = "speed"
Level = "level"
Route = "route"
EstimatedTime = "estt"
AlternateAirport = "alter"
PBN = "pbn"
NavigationEquipment = "nav"
EstimatedElapsedTime = "eet"
SELCALCode = "sel"
PerformanceCategory = "per"
RerouteInformation = "rif"
Remarks = "remark"
2024-07-20 18:55:28 +08:00
)
2024-07-22 08:12:18 +08:00
var (
otherPatterns = []*regexp.Regexp{navPattern,
remarkPattern,
selPattern,
pbnPattern,
eetPattern,
performancePattern,
regPattern,
reroutePattern}
// ErrHeaderParse indicates an invalid header section.
ErrHeaderParse = errors.New("invalid telegram header")
// ErrBodyParse indicates a failure matching the telegram body.
ErrBodyParse = errors.New("invalid telegram body")
2024-07-22 08:12:18 +08:00
)
2024-07-20 22:21:12 +08:00
type BodyParser struct {
body string
bodyPatterns map[string]BodyConfig
mu sync.Mutex
2024-07-20 22:21:12 +08:00
}
func NewBodyParser(body string) *BodyParser {
return &BodyParser{
bodyPatterns: bodyPatterns,
body: body,
}
2024-07-20 22:21:12 +08:00
}
func (parser *BodyParser) GetBodyPatterns() map[string]BodyConfig {
parser.mu.Lock()
defer parser.mu.Unlock()
return parser.bodyPatterns
2024-07-20 22:21:12 +08:00
}
func (parser *BodyParser) SetBodyPatterns(patterns map[string]BodyConfig) {
parser.mu.Lock()
defer parser.mu.Unlock()
parser.bodyPatterns = patterns
2024-07-20 22:21:12 +08:00
}
func (parser *BodyParser) Parse() (string, interface{}, error) {
parser.mu.Lock()
defer parser.mu.Unlock()
parser.body = strings.TrimSpace(parser.body)
category := findCategory(parser.body)
if category == "" {
2024-07-23 16:45:03 +08:00
return "", nil, fmt.Errorf("no category found in body text")
}
2024-07-23 16:45:03 +08:00
if patternConfig, exists := parser.bodyPatterns[category]; exists && patternConfig.Patterns != nil {
2024-07-24 10:33:12 +08:00
for _, p := range patternConfig.Patterns {
if data := extract(parser.body, p.Expression); data != nil {
return parser.createBodyData(data)
2024-07-20 22:21:12 +08:00
}
}
}
return "", nil, fmt.Errorf("no matching pattern found for body: %s", parser.body)
}
func findCategory(body string) string {
2024-07-24 10:33:12 +08:00
if match := categoryRegex.FindStringSubmatch(body); match != nil {
for i, name := range categoryRegex.SubexpNames() {
2024-07-22 12:42:30 +08:00
if i != 0 && name == "category" {
return match[i]
}
}
}
return ""
2024-07-20 22:21:12 +08:00
}
func extract(data string, exp *regexp.Regexp) map[string]string {
match := exp.FindStringSubmatch(data)
if len(match) > 0 {
return extractData(match, exp)
}
return nil
}
2024-07-20 22:33:22 +08:00
func extractData(match []string, re *regexp.Regexp) map[string]string {
data := make(map[string]string)
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
2024-07-24 15:20:00 +08:00
data[name] = strings.TrimSpace(match[i])
2024-07-20 22:33:22 +08:00
}
}
return data
}
func (parser *BodyParser) createBodyData(data map[string]string) (string, interface{}, error) {
2024-07-24 10:33:12 +08:00
switch category := data["category"]; category {
2024-07-26 09:59:38 +08:00
case CategoryArrival:
2024-07-23 16:45:03 +08:00
return category, &domain.ARR{
2024-07-26 09:59:38 +08:00
Category: data[Category],
AircraftID: data[FlightNumber],
SSRModeAndCode: data[SSR],
2024-07-29 15:39:59 +08:00
DepartureAirport: data[DepartureCode],
ArrivalAirport: data[ArrivalCode],
2024-07-26 09:59:38 +08:00
ArrivalTime: data[ArrivalTime],
2024-07-20 22:21:12 +08:00
}, nil
2024-07-26 09:59:38 +08:00
case CategoryDeparture:
2024-07-23 16:45:03 +08:00
return category, &domain.DEP{
2024-07-26 09:59:38 +08:00
Category: data[Category],
AircraftID: data[FlightNumber],
SSRModeAndCode: data[SSR],
2024-07-29 15:39:59 +08:00
DepartureAirport: data[DepartureCode],
2024-07-26 09:59:38 +08:00
DepartureTime: data[DepartureTime],
2024-07-29 15:39:59 +08:00
Destination: data[ArrivalCode],
2024-07-20 22:21:12 +08:00
}, nil
2024-07-26 09:59:38 +08:00
case CategoryCancellation:
return category, &domain.CNL{
2024-07-26 09:59:38 +08:00
Category: data[category],
AircraftID: data[FlightNumber],
2024-07-29 15:39:59 +08:00
DepartureAirport: data[DepartureCode],
DestinationAirport: data[ArrivalCode],
}, nil
2024-07-26 09:59:38 +08:00
case CategoryDelay:
return category, &domain.DLA{
2024-07-26 09:59:38 +08:00
Category: data[Category],
AircraftID: data[FlightNumber],
2024-07-29 15:39:59 +08:00
DepartureAirport: data[DepartureCode],
2024-07-26 09:59:38 +08:00
NewDepartureTime: data[DepartureTime],
2024-07-29 15:39:59 +08:00
ArrivalAirport: data[ArrivalCode],
2024-07-26 09:59:38 +08:00
ArrivalTime: data[ArrivalTime],
}, nil
2024-07-26 09:59:38 +08:00
case CategoryFlightPlan:
otherData := parseOther(data[OtherInfo])
2024-07-23 16:45:03 +08:00
return category, &domain.FPL{
2024-07-26 09:59:38 +08:00
Category: data[Category],
FlightNumber: data[FlightNumber],
ReferenceData: data[ReferenceData],
AircraftID: data[AircraftID],
SSRModeAndCode: data[Surveillance],
FlightRulesAndType: data[Indicator],
CruisingSpeedAndLevel: data[Speed] + data[Level],
2024-07-29 15:39:59 +08:00
DepartureAirport: data[DepartureCode],
2024-07-26 09:59:38 +08:00
DepartureTime: data[DepartureTime],
Route: data[Route],
2024-07-29 15:39:59 +08:00
DestinationAndTotalTime: data[DestinationCode] + data[EstimatedTime],
2024-07-26 09:59:38 +08:00
AlternateAirport: data[AlternateAirport],
OtherInfo: data[OtherInfo],
Register: otherData[Register],
EstimatedArrivalTime: data[EstimatedTime],
PBN: otherData[PBN],
NavigationEquipment: otherData[NavigationEquipment],
EstimatedElapsedTime: otherData[EstimatedElapsedTime],
SELCALCode: otherData[SELCALCode],
PerformanceCategory: otherData[PerformanceCategory],
RerouteInformation: otherData[RerouteInformation],
Remarks: otherData[Remarks],
2024-07-20 22:21:12 +08:00
}, nil
default:
return category, nil, fmt.Errorf("invalid message type: %s", category)
2024-07-20 22:21:12 +08:00
}
}
func Parse(rawText string) (*dto.ParsedTelegram, error) {
header, err := ParseHeader(rawText)
2024-07-20 22:21:12 +08:00
if err != nil {
msg := dto.NewParsedTelegram()
msg.Content = rawText
msg.Comments = err.Error()
msg.ErrorReason = err.Error()
msg.Status = dto.MessageStatusHeaderError
return msg, fmt.Errorf("%w: %w", ErrHeaderParse, err)
2024-07-20 22:21:12 +08:00
}
bodyParser := NewBodyParser(header.Body)
category, bodyData, bodyErr := bodyParser.Parse()
header.Category = category
header.ParsedAt = time.Now()
2024-07-23 16:45:03 +08:00
if bodyErr != nil {
return &dto.ParsedTelegram{
MessageID: header.MessageID,
DateTime: header.DateTime,
PriorityIndicator: header.PriorityIndicator,
PrimaryAddress: header.PrimaryAddress,
SecondaryAddresses: header.SecondaryAddresses,
Originator: header.Originator,
OriginatorDateTime: header.OriginatorDateTime,
Category: header.Category,
Body: header.Body,
Content: header.Content,
ReceivedAt: header.ReceivedAt,
ParsedAt: header.ParsedAt,
Parsed: false,
Comments: bodyErr.Error(),
Status: dto.MessageStatusBodyError,
ErrorReason: bodyErr.Error(),
}, fmt.Errorf("%w: %w", ErrBodyParse, bodyErr)
2024-07-20 22:21:12 +08:00
}
parsed := &dto.ParsedTelegram{
MessageID: header.MessageID,
DateTime: header.DateTime,
PriorityIndicator: header.PriorityIndicator,
PrimaryAddress: header.PrimaryAddress,
SecondaryAddresses: header.SecondaryAddresses,
Originator: header.Originator,
OriginatorDateTime: header.OriginatorDateTime,
Category: header.Category,
Body: header.Body,
Content: header.Content,
BodyData: bodyData,
ReceivedAt: header.ReceivedAt,
ParsedAt: header.ParsedAt,
Parsed: true,
Status: dto.MessageStatusParsed,
ErrorReason: "",
}
parsed.Uuid = uuid.New().String()
return parsed, nil
2024-07-20 22:21:12 +08:00
}
func cleanMessage(text string) string {
cleanedText := emptyLineRemove.ReplaceAllString(text, "")
cleanText := strings.ReplaceAll(cleanedText, "\n\n", "\n")
2024-07-24 10:33:12 +08:00
if match := bodyOnly.FindStringSubmatch(cleanText); len(match) > 1 {
bodyContent := match[2]
if bodyContent[len(bodyContent)-1] == '\n' {
return bodyContent[:len(bodyContent)-1]
}
2024-07-24 10:33:12 +08:00
return bodyContent
2024-07-22 08:12:18 +08:00
}
return ""
}
// ParseHeader parses only the header portion of the message and returns a lightweight struct
// with header fields and body content. It is used internally by the aviation parser.
type Header struct {
MessageID string
DateTime string
PriorityIndicator string
PrimaryAddress string
SecondaryAddresses string
Originator string
OriginatorDateTime string
Category string
Content string
Body string
ReceivedAt time.Time
ParsedAt time.Time
}
func ParseHeader(fullMessage string) (Header, error) {
log := zap.S()
cleaned := cleanMessage(fullMessage)
lines := strings.Split(cleaned, "\n")
2024-07-24 14:21:02 +08:00
if len(lines) < 3 {
log.Warnf("invalid message format: %s", fullMessage)
return Header{Content: fullMessage}, fmt.Errorf("invalid message format: %s", fullMessage)
2024-07-24 14:21:02 +08:00
}
_, messageID, dateTime, err := parseStartIndicator(lines[0])
2024-07-20 18:55:28 +08:00
if err != nil {
return Header{Content: fullMessage}, err
}
priorityIndicator, primaryAddress := parsePriorityAndPrimary(lines[1])
secondaryAddresses, originator, originatorDateTime, body := parseRemainingLines(lines[2:])
2024-07-20 18:55:28 +08:00
return Header{
MessageID: messageID,
DateTime: dateTime,
PriorityIndicator: priorityIndicator,
PrimaryAddress: primaryAddress,
SecondaryAddresses: secondaryAddresses,
Originator: originator,
OriginatorDateTime: originatorDateTime,
Content: fullMessage,
Body: body,
2024-07-20 22:25:28 +08:00
ReceivedAt: time.Now(),
2024-07-20 18:55:28 +08:00
}, nil
}
func parseStartIndicator(line string) (string, string, string, error) {
2024-07-20 22:33:22 +08:00
parts := strings.Fields(line)
if len(parts) >= 3 && strings.HasPrefix(parts[0], StartIndicatorPrefix) {
return parts[0], parts[1], parts[2], nil
}
zap.S().Warnf("invalid start indicator line format: %s", line)
2024-07-20 22:33:22 +08:00
return "", "", "", fmt.Errorf("invalid start indicator line format: %s", line)
2024-07-20 18:55:28 +08:00
}
func parsePriorityAndPrimary(line string) (string, string) {
2024-07-20 18:55:28 +08:00
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[0], parts[1]
2024-07-20 18:55:28 +08:00
}
zap.S().Warnf("invalid priority and primary address line format: %s", line)
return "", ""
2024-07-20 18:55:28 +08:00
}
func parseRemainingLines(lines []string) (string, string, string, string) {
2024-07-20 18:55:28 +08:00
var (
secondaryAddresses string
2024-07-20 18:55:28 +08:00
originator string
originatorDateTime string
bodyAndFooter strings.Builder
headerEnded bool
)
for _, line := range lines {
line = strings.TrimSpace(line)
if headerEnded {
bodyAndFooter.WriteString(line + "\n")
} else {
switch {
case line == EndHeaderMarker:
case strings.HasPrefix(line, "."):
originatorInfo := strings.Fields(line[1:])
if len(originatorInfo) >= 2 {
originator = originatorInfo[0]
originatorDateTime = originatorInfo[1]
}
headerEnded = true
case strings.HasPrefix(line, BeginPartMarker) || strings.HasPrefix(line, "("):
headerEnded = true
if strings.Index(line, "NNNN") > 0 {
break
}
2024-07-20 18:55:28 +08:00
bodyAndFooter.WriteString(line + "\n")
default:
2024-07-23 16:45:03 +08:00
if o1, o2 := getOriginator(line); o1 != "" {
originatorDateTime = o1
originator = o2
} else {
secondaryAddresses = secondaryAddresses + " " + line
2024-07-23 16:45:03 +08:00
}
2024-07-20 18:55:28 +08:00
}
}
}
return secondaryAddresses, originator, originatorDateTime, bodyAndFooter.String()
}
2024-07-23 16:45:03 +08:00
func getOriginator(line string) (string, string) {
match := originator.FindStringSubmatch(line)
if len(match) >= 3 {
return match[1], match[2]
}
zap.S().Warnf("invalid originator line format: %s", line)
2024-07-23 16:45:03 +08:00
return "", ""
}
2024-07-23 19:12:08 +08:00
func parseOther(text string) map[string]string {
data := make(map[string]string)
for _, re := range otherPatterns {
2024-07-24 10:33:12 +08:00
if match := re.FindStringSubmatch(text); len(match) > 0 {
2024-07-23 19:12:08 +08:00
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
2024-07-23 19:36:11 +08:00
data[name] = strings.TrimSpace(match[i])
2024-07-23 19:12:08 +08:00
}
}
}
}
2024-07-24 10:33:12 +08:00
return data
2024-07-23 19:12:08 +08:00
}