2024-07-20 18:26:17 +08:00
|
|
|
package parsers
|
|
|
|
|
|
|
|
|
|
import (
|
2024-07-20 22:21:12 +08:00
|
|
|
"caatsm/internal/config"
|
2024-07-20 18:26:17 +08:00
|
|
|
"caatsm/internal/domain"
|
2024-07-20 22:21:12 +08:00
|
|
|
"caatsm/pkg/utils"
|
|
|
|
|
"fmt"
|
2024-07-20 22:33:22 +08:00
|
|
|
"regexp"
|
2024-07-20 18:26:17 +08:00
|
|
|
"strings"
|
2024-07-20 22:25:28 +08:00
|
|
|
"time"
|
2024-07-20 18:26:17 +08:00
|
|
|
)
|
|
|
|
|
|
2024-07-20 18:55:28 +08:00
|
|
|
const (
|
|
|
|
|
StartIndicatorPrefix = "ZCZC"
|
|
|
|
|
EndHeaderMarker = "."
|
|
|
|
|
BeginPartMarker = "BEGIN PART"
|
|
|
|
|
)
|
|
|
|
|
|
2024-07-22 08:12:18 +08:00
|
|
|
var (
|
|
|
|
|
categoryRegex = regexp.MustCompile(`\(([A-Z]{3})(.*)\)`)
|
|
|
|
|
emptyLineRemove = regexp.MustCompile(`(?m)^\s*$`)
|
|
|
|
|
bodyOnly = regexp.MustCompile(`^(ZCZC(.|\n)*)NNNN$`)
|
|
|
|
|
)
|
2024-07-22 00:01:27 +08:00
|
|
|
|
2024-07-20 22:21:12 +08:00
|
|
|
type BodyParser struct {
|
2024-07-20 22:33:22 +08:00
|
|
|
bodyPatterns map[string]config.BodyConfig
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// NewBodyParser initializes a BodyParser with the default body patterns.
|
|
|
|
|
func NewBodyParser() *BodyParser {
|
|
|
|
|
return &BodyParser{bodyPatterns: config.GetBodyPatterns()}
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// GetBodyPatterns returns the body patterns used by the parser.
|
2024-07-20 22:21:12 +08:00
|
|
|
func (bp *BodyParser) GetBodyPatterns() map[string]config.BodyConfig {
|
2024-07-20 22:33:22 +08:00
|
|
|
return bp.bodyPatterns
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// SetBodyPatterns sets the body patterns for the parser.
|
2024-07-20 22:21:12 +08:00
|
|
|
func (bp *BodyParser) SetBodyPatterns(patterns map[string]config.BodyConfig) {
|
2024-07-20 22:33:22 +08:00
|
|
|
bp.bodyPatterns = patterns
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// Parse attempts to parse the body text using the configured patterns.
|
2024-07-20 22:21:12 +08:00
|
|
|
func (bp *BodyParser) Parse(body string) (interface{}, error) {
|
|
|
|
|
log := utils.Logger
|
2024-07-22 00:01:27 +08:00
|
|
|
body = strings.TrimSpace(body)
|
|
|
|
|
log.Info("Parsing body text", body)
|
|
|
|
|
category := findCategory(body)
|
|
|
|
|
if category == "" {
|
|
|
|
|
log.Error("No category found in body text")
|
|
|
|
|
return nil, fmt.Errorf("no category found in body text")
|
|
|
|
|
}
|
|
|
|
|
patters := bp.GetBodyPatterns()
|
|
|
|
|
log.Infof("body config [%s] %v\n", category, patters[category])
|
|
|
|
|
if patterConfig := patters[category]; patterConfig.Patterns != nil {
|
|
|
|
|
|
|
|
|
|
for _, p := range patterConfig.Patterns {
|
|
|
|
|
log.Infof("Trying pattern %s\n%s\n", p.Comments, p.Pattern)
|
|
|
|
|
|
2024-07-20 22:21:12 +08:00
|
|
|
re := p.Expression
|
|
|
|
|
match := re.FindStringSubmatch(body)
|
2024-07-22 00:01:27 +08:00
|
|
|
log.Info("Match: ", match)
|
2024-07-20 22:21:12 +08:00
|
|
|
if match != nil {
|
2024-07-22 00:01:27 +08:00
|
|
|
log.Infof("Matched: %v\n", match)
|
2024-07-20 22:33:22 +08:00
|
|
|
data := extractData(match, re)
|
|
|
|
|
return createBodyData(data)
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
2024-07-22 00:01:27 +08:00
|
|
|
log.Infof("No match for pattern %s\n", p.Comments)
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
2024-07-22 00:01:27 +08:00
|
|
|
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
2024-07-22 00:01:27 +08:00
|
|
|
return nil, fmt.Errorf(" no matching pattern found for body: %s", body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findCategory(body string) string {
|
|
|
|
|
match := categoryRegex.FindStringSubmatch(body)
|
|
|
|
|
utils.Logger.Infof("Match: %v\n", match)
|
|
|
|
|
if match != nil {
|
|
|
|
|
return match[1]
|
|
|
|
|
}
|
|
|
|
|
return ""
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// extractData extracts named groups from the regex match.
|
|
|
|
|
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 != "" {
|
|
|
|
|
data[name] = match[i]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createBodyData creates the appropriate domain object based on the type of message.
|
|
|
|
|
func createBodyData(data map[string]string) (interface{}, error) {
|
2024-07-21 00:25:16 +08:00
|
|
|
switch data["category"] {
|
2024-07-20 22:21:12 +08:00
|
|
|
case "ARR":
|
|
|
|
|
return &domain.ARR{
|
2024-07-21 00:25:16 +08:00
|
|
|
Category: data["category"],
|
2024-07-20 22:21:12 +08:00
|
|
|
AircraftID: data["number"],
|
|
|
|
|
SSRModeAndCode: data["ssr"],
|
|
|
|
|
DepartureAirport: data["departure"],
|
|
|
|
|
ArrivalAirport: data["arrival"],
|
2024-07-22 00:01:27 +08:00
|
|
|
ArrivalTime: data["time"],
|
2024-07-20 22:21:12 +08:00
|
|
|
}, nil
|
|
|
|
|
case "DEP":
|
|
|
|
|
return &domain.DEP{
|
2024-07-21 00:25:16 +08:00
|
|
|
Category: data["category"],
|
2024-07-20 22:21:12 +08:00
|
|
|
AircraftID: data["number"],
|
|
|
|
|
SSRModeAndCode: data["ssr"],
|
|
|
|
|
DepartureAirport: data["departure"],
|
|
|
|
|
DepartureTime: data["departure_time"],
|
|
|
|
|
Destination: data["arrival"],
|
|
|
|
|
}, nil
|
|
|
|
|
case "FPL":
|
|
|
|
|
return &domain.FPL{
|
2024-07-21 00:25:16 +08:00
|
|
|
Category: data["category"],
|
2024-07-20 22:21:12 +08:00
|
|
|
FlightNumber: data["number"],
|
|
|
|
|
ReferenceData: data["reference_data"],
|
|
|
|
|
AircraftID: data["aircraft"],
|
|
|
|
|
SSRModeAndCode: data["surve"],
|
|
|
|
|
FlightRulesAndType: data["indicator"],
|
|
|
|
|
CruisingSpeedAndLevel: data["speed"] + data["level"],
|
|
|
|
|
DepartureAirport: data["departure"],
|
|
|
|
|
DepartureTime: data["departure_time"],
|
|
|
|
|
Route: data["route"],
|
|
|
|
|
DestinationAndTotalTime: data["destination"] + data["estt"],
|
|
|
|
|
AlternateAirport: data["alter"],
|
|
|
|
|
OtherInfo: fmt.Sprintf("%s %s REG/%s EET/%s SEL/%s PER/%s RIF/%s",
|
|
|
|
|
data["pbn"], data["nav"], data["reg"], data["eet"], data["sel"], data["performance"], data["rif"]),
|
|
|
|
|
SupplementaryInfo: "RMK/" + data["remark"],
|
|
|
|
|
EstimatedArrivalTime: data["estimated_arrival_time"],
|
|
|
|
|
PBN: data["pbn"],
|
|
|
|
|
NavigationEquipment: data["nav"],
|
|
|
|
|
EstimatedElapsedTime: data["eet"],
|
|
|
|
|
SELCALCode: data["sel"],
|
|
|
|
|
PerformanceCategory: data["performance"],
|
|
|
|
|
RerouteInformation: data["rif"],
|
|
|
|
|
Remarks: data["remark"],
|
|
|
|
|
}, nil
|
|
|
|
|
default:
|
2024-07-21 00:25:16 +08:00
|
|
|
return nil, fmt.Errorf("invalid message type: %s", data["category"])
|
2024-07-20 22:21:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// Parse parses the raw text message and returns a ParsedMessage.
|
2024-07-20 22:21:12 +08:00
|
|
|
func Parse(rawText string) (*domain.ParsedMessage, error) {
|
|
|
|
|
message, err := ParseHeader(rawText)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-07-20 22:33:22 +08:00
|
|
|
bodyParser := NewBodyParser()
|
2024-07-20 22:21:12 +08:00
|
|
|
bodyData, err := bodyParser.Parse(message.BodyAndFooter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-07-20 22:25:28 +08:00
|
|
|
message.ParsedAt = time.Now()
|
2024-07-20 22:21:12 +08:00
|
|
|
message.BodyData = bodyData
|
|
|
|
|
return &message, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-22 00:01:27 +08:00
|
|
|
// removeEmptyLines removes empty lines from a given text.
|
2024-07-22 08:12:18 +08:00
|
|
|
func clean(text string) string {
|
|
|
|
|
cleanedMatch := emptyLineRemove.ReplaceAllString(text, "")
|
|
|
|
|
cleanText := strings.ReplaceAll(cleanedMatch, "\n\n", "\n")
|
|
|
|
|
if bodyOnly != nil {
|
|
|
|
|
bodyOnly := bodyOnly.FindStringSubmatch(cleanText)[1]
|
|
|
|
|
removeLast := bodyOnly[:len(bodyOnly)-1]
|
|
|
|
|
return removeLast
|
|
|
|
|
}
|
|
|
|
|
return ""
|
2024-07-22 00:01:27 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// parseHeader parses the header of the message and returns a ParsedMessage struct.
|
2024-07-20 18:55:28 +08:00
|
|
|
func ParseHeader(fullMessage string) (domain.ParsedMessage, error) {
|
2024-07-22 08:12:18 +08:00
|
|
|
fullMessage = clean(fullMessage)
|
2024-07-22 00:01:27 +08:00
|
|
|
// fullMessage = strings.TrimSpace(fullMessage)
|
2024-07-20 22:33:22 +08:00
|
|
|
lines := strings.Split(fullMessage, "\n")
|
2024-07-20 18:26:17 +08:00
|
|
|
|
2024-07-20 18:55:28 +08:00
|
|
|
startIndicator, messageID, dateTime, err := parseStartIndicator(lines[0])
|
|
|
|
|
if err != nil {
|
2024-07-20 22:33:22 +08:00
|
|
|
return domain.ParsedMessage{}, err
|
2024-07-20 18:26:17 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-22 00:01:27 +08:00
|
|
|
// priorityIndicator, primaryAddress, err := parsePriorityAndPrimary(lines[1])
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return domain.ParsedMessage{}, err
|
|
|
|
|
// }
|
|
|
|
|
priorityIndicator, primaryAddress := parsePriorityAndPrimary(lines[1])
|
2024-07-20 18:55:28 +08:00
|
|
|
|
|
|
|
|
secondaryAddresses, originator, originatorDateTime, bodyAndFooter := parseRemainingLines(lines[2:])
|
|
|
|
|
|
2024-07-20 18:26:17 +08:00
|
|
|
return domain.ParsedMessage{
|
|
|
|
|
StartIndicator: startIndicator,
|
|
|
|
|
MessageID: messageID,
|
|
|
|
|
DateTime: dateTime,
|
|
|
|
|
PriorityIndicator: priorityIndicator,
|
|
|
|
|
PrimaryAddress: primaryAddress,
|
|
|
|
|
SecondaryAddresses: secondaryAddresses,
|
|
|
|
|
Originator: originator,
|
|
|
|
|
OriginatorDateTime: originatorDateTime,
|
2024-07-20 18:55:28 +08:00
|
|
|
BodyAndFooter: bodyAndFooter,
|
2024-07-20 22:25:28 +08:00
|
|
|
ReceivedAt: time.Now(),
|
2024-07-20 18:55:28 +08:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// parseStartIndicator parses the start indicator line.
|
2024-07-20 18:55:28 +08:00
|
|
|
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
|
2024-07-20 18:26:17 +08:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// parsePriorityAndPrimary parses the priority indicator and primary address line.
|
2024-07-22 00:01:27 +08:00
|
|
|
func parsePriorityAndPrimary(line string) (string, string) {
|
2024-07-20 18:55:28 +08:00
|
|
|
parts := strings.Fields(line)
|
|
|
|
|
if len(parts) >= 2 {
|
2024-07-22 00:01:27 +08:00
|
|
|
return parts[0], parts[1]
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
2024-07-22 00:01:27 +08:00
|
|
|
return "", ""
|
2024-07-20 18:55:28 +08:00
|
|
|
}
|
|
|
|
|
|
2024-07-20 22:33:22 +08:00
|
|
|
// parseRemainingLines parses the remaining lines of the message.
|
2024-07-20 18:55:28 +08:00
|
|
|
func parseRemainingLines(lines []string) ([]string, string, string, string) {
|
|
|
|
|
var (
|
|
|
|
|
secondaryAddresses []string
|
|
|
|
|
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:
|
2024-07-20 22:33:22 +08:00
|
|
|
// End header marker, do nothing
|
2024-07-20 18:55:28 +08:00
|
|
|
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
|
2024-07-22 00:01:27 +08:00
|
|
|
if strings.Index(line, "NNNN") > 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
2024-07-20 18:55:28 +08:00
|
|
|
bodyAndFooter.WriteString(line + "\n")
|
|
|
|
|
default:
|
|
|
|
|
secondaryAddresses = append(secondaryAddresses, line)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return secondaryAddresses, originator, originatorDateTime, bodyAndFooter.String()
|
2024-07-20 18:26:17 +08:00
|
|
|
}
|