blob: c1382cb563bc7fcfdf8718e8c9d23693e8d58292 [file] [log] [blame]
Akron49ceeb42025-05-23 17:46:01 +02001package main
2
3import (
Akron49ceeb42025-05-23 17:46:01 +02004 "fmt"
Akron80067202025-06-06 14:16:25 +02005 "net/url"
Akron49ceeb42025-05-23 17:46:01 +02006 "os"
7 "os/signal"
Akron80067202025-06-06 14:16:25 +02008 "path"
Akron14678dc2025-06-05 13:01:38 +02009 "path/filepath"
Akron49ceeb42025-05-23 17:46:01 +020010 "strings"
11 "syscall"
Akron3caee162025-07-01 17:44:58 +020012 "time"
Akron49ceeb42025-05-23 17:46:01 +020013
Akrona00d4752025-05-26 17:34:36 +020014 "github.com/KorAP/KoralPipe-TermMapper/config"
Akronfa55bb22025-05-26 15:10:42 +020015 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron1fc750e2025-05-26 16:54:18 +020016 "github.com/alecthomas/kong"
Akron49ceeb42025-05-23 17:46:01 +020017 "github.com/gofiber/fiber/v2"
18 "github.com/rs/zerolog"
19 "github.com/rs/zerolog/log"
20)
21
Akron74e1c072025-05-26 14:38:25 +020022const (
23 maxInputLength = 1024 * 1024 // 1MB
24 maxParamLength = 1024 // 1KB
25)
26
Akrona00d4752025-05-26 17:34:36 +020027type appConfig struct {
Akrona8a66ce2025-06-05 10:50:17 +020028 Port *int `kong:"short='p',help='Port to listen on'"`
Akrone1cff7c2025-06-04 18:43:32 +020029 Config string `kong:"short='c',help='YAML configuration file containing mapping directives and global settings'"`
Akron14678dc2025-06-05 13:01:38 +020030 Mappings []string `kong:"short='m',help='Individual YAML mapping files to load (supports glob patterns like dir/*.yaml)'"`
Akrona8a66ce2025-06-05 10:50:17 +020031 LogLevel *string `kong:"short='l',help='Log level (debug, info, warn, error)'"`
Akron49ceeb42025-05-23 17:46:01 +020032}
33
Akrondab27112025-06-05 13:52:43 +020034type TemplateMapping struct {
35 ID string
36 Description string
37}
38
Akron40aaa632025-06-03 17:57:52 +020039// TemplateData holds data for the Kalamar plugin template
40type TemplateData struct {
41 Title string
42 Version string
Akronfc77b5e2025-06-04 11:44:43 +020043 Hash string
44 Date string
Akron40aaa632025-06-03 17:57:52 +020045 Description string
Akron06d21f02025-06-04 14:36:07 +020046 Server string
47 SDK string
Akron2ac2ec02025-06-05 15:26:42 +020048 ServiceURL string
Akronc376dcc2025-06-04 17:00:18 +020049 MapID string
Akrondab27112025-06-05 13:52:43 +020050 Mappings []TemplateMapping
Akron40aaa632025-06-03 17:57:52 +020051}
52
Akroncb51f812025-06-30 15:24:20 +020053type QueryParams struct {
54 Dir string
55 FoundryA string
56 FoundryB string
57 LayerA string
58 LayerB string
59}
60
Akron49b525c2025-07-03 15:17:06 +020061// requestParams holds common request parameters
62type requestParams struct {
63 MapID string
64 Dir string
65 FoundryA string
66 FoundryB string
67 LayerA string
68 LayerB string
69}
70
Akrona00d4752025-05-26 17:34:36 +020071func parseConfig() *appConfig {
72 cfg := &appConfig{}
Akronfc77b5e2025-06-04 11:44:43 +020073
74 desc := config.Description
75 desc += " [" + config.Version + "]"
76
Akron1fc750e2025-05-26 16:54:18 +020077 ctx := kong.Parse(cfg,
Akronfc77b5e2025-06-04 11:44:43 +020078 kong.Description(desc),
Akron1fc750e2025-05-26 16:54:18 +020079 kong.UsageOnError(),
80 )
81 if ctx.Error != nil {
82 fmt.Fprintln(os.Stderr, ctx.Error)
Akron49ceeb42025-05-23 17:46:01 +020083 os.Exit(1)
84 }
Akron49ceeb42025-05-23 17:46:01 +020085 return cfg
86}
87
88func setupLogger(level string) {
89 // Parse log level
90 lvl, err := zerolog.ParseLevel(strings.ToLower(level))
91 if err != nil {
92 log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
93 lvl = zerolog.InfoLevel
94 }
95
96 // Configure zerolog
97 zerolog.SetGlobalLevel(lvl)
98 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
99}
100
Akron3caee162025-07-01 17:44:58 +0200101// setupFiberLogger configures fiber's logger middleware to integrate with zerolog
102func setupFiberLogger() fiber.Handler {
103 // Check if HTTP request logging should be enabled based on current log level
104 currentLevel := zerolog.GlobalLevel()
105
106 // Only enable HTTP request logging if log level is debug or info
107 if currentLevel > zerolog.InfoLevel {
108 return func(c *fiber.Ctx) error {
109 return c.Next()
110 }
111 }
112
113 return func(c *fiber.Ctx) error {
114 // Record start time
115 start := time.Now()
116
117 // Process request
118 err := c.Next()
119
120 // Calculate latency
121 latency := time.Since(start)
122 status := c.Response().StatusCode()
123
124 // Determine log level based on status code
125 logEvent := log.Info()
126 if status >= 400 && status < 500 {
127 logEvent = log.Warn()
128 } else if status >= 500 {
129 logEvent = log.Error()
130 }
131
132 // Log the request
133 logEvent.
134 Int("status", status).
135 Dur("latency", latency).
136 Str("method", c.Method()).
137 Str("path", c.Path()).
138 Str("ip", c.IP()).
139 Str("user_agent", c.Get("User-Agent")).
140 Msg("HTTP request")
141
142 return err
143 }
144}
145
Akron49b525c2025-07-03 15:17:06 +0200146// extractRequestParams extracts and validates common request parameters
147func extractRequestParams(c *fiber.Ctx) (*requestParams, error) {
148 params := &requestParams{
149 MapID: c.Params("map"),
150 Dir: c.Query("dir", "atob"),
151 FoundryA: c.Query("foundryA", ""),
152 FoundryB: c.Query("foundryB", ""),
153 LayerA: c.Query("layerA", ""),
154 LayerB: c.Query("layerB", ""),
155 }
156
157 // Validate input parameters
158 if err := validateInput(params.MapID, params.Dir, params.FoundryA, params.FoundryB, params.LayerA, params.LayerB, c.Body()); err != nil {
159 return nil, err
160 }
161
162 // Validate direction
163 if params.Dir != "atob" && params.Dir != "btoa" {
164 return nil, fmt.Errorf("invalid direction, must be 'atob' or 'btoa'")
165 }
166
167 return params, nil
168}
169
170// parseRequestBody parses JSON request body and direction
171func parseRequestBody(c *fiber.Ctx, dir string) (any, mapper.Direction, error) {
172 var jsonData any
173 if err := c.BodyParser(&jsonData); err != nil {
174 return nil, mapper.BtoA, fmt.Errorf("invalid JSON in request body")
175 }
176
177 direction, err := mapper.ParseDirection(dir)
178 if err != nil {
179 return nil, mapper.BtoA, err
180 }
181
182 return jsonData, direction, nil
183}
184
Akron49ceeb42025-05-23 17:46:01 +0200185func main() {
186 // Parse command line flags
Akron1fc750e2025-05-26 16:54:18 +0200187 cfg := parseConfig()
Akron49ceeb42025-05-23 17:46:01 +0200188
Akrone1cff7c2025-06-04 18:43:32 +0200189 // Validate command line arguments
190 if cfg.Config == "" && len(cfg.Mappings) == 0 {
191 log.Fatal().Msg("At least one configuration source must be provided: use -c for main config file or -m for mapping files")
192 }
193
Akron14678dc2025-06-05 13:01:38 +0200194 // Expand glob patterns in mapping files
195 expandedMappings, err := expandGlobs(cfg.Mappings)
196 if err != nil {
197 log.Fatal().Err(err).Msg("Failed to expand glob patterns in mapping files")
198 }
199
Akrone1cff7c2025-06-04 18:43:32 +0200200 // Load configuration from multiple sources
Akron14678dc2025-06-05 13:01:38 +0200201 yamlConfig, err := config.LoadFromSources(cfg.Config, expandedMappings)
Akrona00d4752025-05-26 17:34:36 +0200202 if err != nil {
203 log.Fatal().Err(err).Msg("Failed to load configuration")
204 }
205
Akrona8a66ce2025-06-05 10:50:17 +0200206 finalPort := yamlConfig.Port
207 finalLogLevel := yamlConfig.LogLevel
208
209 // Use command line values if provided (they override config file)
210 if cfg.Port != nil {
211 finalPort = *cfg.Port
212 }
213 if cfg.LogLevel != nil {
214 finalLogLevel = *cfg.LogLevel
215 }
216
217 // Set up logging with the final log level
218 setupLogger(finalLogLevel)
219
Akron49ceeb42025-05-23 17:46:01 +0200220 // Create a new mapper instance
Akrona00d4752025-05-26 17:34:36 +0200221 m, err := mapper.NewMapper(yamlConfig.Lists)
Akron49ceeb42025-05-23 17:46:01 +0200222 if err != nil {
223 log.Fatal().Err(err).Msg("Failed to create mapper")
224 }
225
226 // Create fiber app
227 app := fiber.New(fiber.Config{
228 DisableStartupMessage: true,
Akron74e1c072025-05-26 14:38:25 +0200229 BodyLimit: maxInputLength,
Akronafbe86d2025-07-01 08:45:13 +0200230 ReadBufferSize: 64 * 1024, // 64KB - increase header size limit
231 WriteBufferSize: 64 * 1024, // 64KB - increase response buffer size
Akron49ceeb42025-05-23 17:46:01 +0200232 })
233
Akron3caee162025-07-01 17:44:58 +0200234 // Add zerolog-integrated logger middleware
235 app.Use(setupFiberLogger())
236
Akron49ceeb42025-05-23 17:46:01 +0200237 // Set up routes
Akron40aaa632025-06-03 17:57:52 +0200238 setupRoutes(app, m, yamlConfig)
Akron49ceeb42025-05-23 17:46:01 +0200239
240 // Start server
241 go func() {
Akrona8a66ce2025-06-05 10:50:17 +0200242 log.Info().Int("port", finalPort).Msg("Starting server")
Akronae3ffde2025-06-05 14:04:06 +0200243
244 for _, list := range yamlConfig.Lists {
245 log.Info().Str("id", list.ID).Str("desc", list.Description).Msg("Loaded mapping")
246 }
247
Akrona8a66ce2025-06-05 10:50:17 +0200248 if err := app.Listen(fmt.Sprintf(":%d", finalPort)); err != nil {
Akron49ceeb42025-05-23 17:46:01 +0200249 log.Fatal().Err(err).Msg("Server error")
250 }
251 }()
252
253 // Wait for interrupt signal
254 sigChan := make(chan os.Signal, 1)
255 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
256 <-sigChan
257
258 // Graceful shutdown
259 log.Info().Msg("Shutting down server")
260 if err := app.Shutdown(); err != nil {
261 log.Error().Err(err).Msg("Error during shutdown")
262 }
263}
264
Akron06d21f02025-06-04 14:36:07 +0200265func setupRoutes(app *fiber.App, m *mapper.Mapper, yamlConfig *config.MappingConfig) {
Akron49ceeb42025-05-23 17:46:01 +0200266 // Health check endpoint
267 app.Get("/health", func(c *fiber.Ctx) error {
268 return c.SendString("OK")
269 })
270
271 // Transformation endpoint
272 app.Post("/:map/query", handleTransform(m))
Akron40aaa632025-06-03 17:57:52 +0200273
Akron4de47a92025-06-27 11:58:11 +0200274 // Response transformation endpoint
275 app.Post("/:map/response", handleResponseTransform(m))
276
Akron40aaa632025-06-03 17:57:52 +0200277 // Kalamar plugin endpoint
Akronc471c0a2025-06-04 11:56:22 +0200278 app.Get("/", handleKalamarPlugin(yamlConfig))
Akronc376dcc2025-06-04 17:00:18 +0200279 app.Get("/:map", handleKalamarPlugin(yamlConfig))
Akron49ceeb42025-05-23 17:46:01 +0200280}
281
282func handleTransform(m *mapper.Mapper) fiber.Handler {
283 return func(c *fiber.Ctx) error {
Akron49b525c2025-07-03 15:17:06 +0200284 // Extract and validate parameters
285 params, err := extractRequestParams(c)
286 if err != nil {
Akron74e1c072025-05-26 14:38:25 +0200287 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
288 "error": err.Error(),
289 })
290 }
291
Akron49ceeb42025-05-23 17:46:01 +0200292 // Parse request body
Akron49b525c2025-07-03 15:17:06 +0200293 jsonData, direction, err := parseRequestBody(c, params.Dir)
Akrona1a183f2025-05-26 17:47:33 +0200294 if err != nil {
295 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
296 "error": err.Error(),
297 })
298 }
299
Akron49ceeb42025-05-23 17:46:01 +0200300 // Apply mappings
Akron49b525c2025-07-03 15:17:06 +0200301 result, err := m.ApplyQueryMappings(params.MapID, mapper.MappingOptions{
Akrona1a183f2025-05-26 17:47:33 +0200302 Direction: direction,
Akron49b525c2025-07-03 15:17:06 +0200303 FoundryA: params.FoundryA,
304 FoundryB: params.FoundryB,
305 LayerA: params.LayerA,
306 LayerB: params.LayerB,
Akron49ceeb42025-05-23 17:46:01 +0200307 }, jsonData)
308
309 if err != nil {
310 log.Error().Err(err).
Akron49b525c2025-07-03 15:17:06 +0200311 Str("mapID", params.MapID).
312 Str("direction", params.Dir).
Akron49ceeb42025-05-23 17:46:01 +0200313 Msg("Failed to apply mappings")
314
315 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
316 "error": err.Error(),
317 })
318 }
319
320 return c.JSON(result)
321 }
322}
Akron74e1c072025-05-26 14:38:25 +0200323
Akron4de47a92025-06-27 11:58:11 +0200324func handleResponseTransform(m *mapper.Mapper) fiber.Handler {
325 return func(c *fiber.Ctx) error {
Akron49b525c2025-07-03 15:17:06 +0200326 // Extract and validate parameters
327 params, err := extractRequestParams(c)
328 if err != nil {
Akron4de47a92025-06-27 11:58:11 +0200329 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
330 "error": err.Error(),
331 })
332 }
333
Akron4de47a92025-06-27 11:58:11 +0200334 // Parse request body
Akron49b525c2025-07-03 15:17:06 +0200335 jsonData, direction, err := parseRequestBody(c, params.Dir)
Akron4de47a92025-06-27 11:58:11 +0200336 if err != nil {
337 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
338 "error": err.Error(),
339 })
340 }
341
342 // Apply response mappings
Akron49b525c2025-07-03 15:17:06 +0200343 result, err := m.ApplyResponseMappings(params.MapID, mapper.MappingOptions{
Akron4de47a92025-06-27 11:58:11 +0200344 Direction: direction,
Akron49b525c2025-07-03 15:17:06 +0200345 FoundryA: params.FoundryA,
346 FoundryB: params.FoundryB,
347 LayerA: params.LayerA,
348 LayerB: params.LayerB,
Akron4de47a92025-06-27 11:58:11 +0200349 }, jsonData)
350
351 if err != nil {
352 log.Error().Err(err).
Akron49b525c2025-07-03 15:17:06 +0200353 Str("mapID", params.MapID).
354 Str("direction", params.Dir).
Akron4de47a92025-06-27 11:58:11 +0200355 Msg("Failed to apply response mappings")
356
357 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
358 "error": err.Error(),
359 })
360 }
361
362 return c.JSON(result)
363 }
364}
365
Akron74e1c072025-05-26 14:38:25 +0200366// validateInput checks if the input parameters are valid
367func validateInput(mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) error {
Akron69d43bf2025-05-26 17:09:00 +0200368 // Define parameter checks
369 params := []struct {
Akron74e1c072025-05-26 14:38:25 +0200370 name string
371 value string
372 }{
373 {"mapID", mapID},
374 {"dir", dir},
375 {"foundryA", foundryA},
376 {"foundryB", foundryB},
377 {"layerA", layerA},
378 {"layerB", layerB},
Akron69d43bf2025-05-26 17:09:00 +0200379 }
380
381 for _, param := range params {
Akron49b525c2025-07-03 15:17:06 +0200382 // Check input lengths and invalid characters in one combined condition
Akron69d43bf2025-05-26 17:09:00 +0200383 if len(param.value) > maxParamLength {
384 return fmt.Errorf("%s too long (max %d bytes)", param.name, maxParamLength)
385 }
Akron74e1c072025-05-26 14:38:25 +0200386 if strings.ContainsAny(param.value, "<>{}[]\\") {
387 return fmt.Errorf("%s contains invalid characters", param.name)
388 }
389 }
390
Akron69d43bf2025-05-26 17:09:00 +0200391 if len(body) > maxInputLength {
392 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
393 }
394
Akron74e1c072025-05-26 14:38:25 +0200395 return nil
396}
Akron40aaa632025-06-03 17:57:52 +0200397
Akron06d21f02025-06-04 14:36:07 +0200398func handleKalamarPlugin(yamlConfig *config.MappingConfig) fiber.Handler {
Akron40aaa632025-06-03 17:57:52 +0200399 return func(c *fiber.Ctx) error {
Akronc376dcc2025-06-04 17:00:18 +0200400 mapID := c.Params("map")
401
Akroncb51f812025-06-30 15:24:20 +0200402 // Get query parameters
403 dir := c.Query("dir", "atob")
404 foundryA := c.Query("foundryA", "")
405 foundryB := c.Query("foundryB", "")
406 layerA := c.Query("layerA", "")
407 layerB := c.Query("layerB", "")
408
Akron49b525c2025-07-03 15:17:06 +0200409 // Validate input parameters and direction in one step
Akroncb51f812025-06-30 15:24:20 +0200410 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, []byte{}); err != nil {
411 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
412 "error": err.Error(),
413 })
414 }
415
Akroncb51f812025-06-30 15:24:20 +0200416 if dir != "atob" && dir != "btoa" {
417 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
418 "error": "invalid direction, must be 'atob' or 'btoa'",
419 })
420 }
421
Akrondab27112025-06-05 13:52:43 +0200422 // Get list of available mappings
423 var mappings []TemplateMapping
Akron40aaa632025-06-03 17:57:52 +0200424 for _, list := range yamlConfig.Lists {
Akrondab27112025-06-05 13:52:43 +0200425 mappings = append(mappings, TemplateMapping{
426 ID: list.ID,
427 Description: list.Description,
428 })
Akron40aaa632025-06-03 17:57:52 +0200429 }
430
431 // Prepare template data
432 data := TemplateData{
Akronfc77b5e2025-06-04 11:44:43 +0200433 Title: config.Title,
434 Version: config.Version,
435 Hash: config.Buildhash,
436 Date: config.Buildtime,
437 Description: config.Description,
Akron49b525c2025-07-03 15:17:06 +0200438 Server: yamlConfig.Server,
439 SDK: yamlConfig.SDK,
Akron2ac2ec02025-06-05 15:26:42 +0200440 ServiceURL: yamlConfig.ServiceURL,
Akronc376dcc2025-06-04 17:00:18 +0200441 MapID: mapID,
Akrondab27112025-06-05 13:52:43 +0200442 Mappings: mappings,
Akron40aaa632025-06-03 17:57:52 +0200443 }
444
Akroncb51f812025-06-30 15:24:20 +0200445 // Add query parameters to template data
446 queryParams := QueryParams{
447 Dir: dir,
448 FoundryA: foundryA,
449 FoundryB: foundryB,
450 LayerA: layerA,
451 LayerB: layerB,
452 }
453
Akron40aaa632025-06-03 17:57:52 +0200454 // Generate HTML
Akroncb51f812025-06-30 15:24:20 +0200455 html := generateKalamarPluginHTML(data, queryParams)
Akron40aaa632025-06-03 17:57:52 +0200456
457 c.Set("Content-Type", "text/html")
458 return c.SendString(html)
459 }
460}
461
462// generateKalamarPluginHTML creates the HTML template for the Kalamar plugin page
463// This function can be easily modified to change the appearance and content
Akroncb51f812025-06-30 15:24:20 +0200464func generateKalamarPluginHTML(data TemplateData, queryParams QueryParams) string {
Akron40aaa632025-06-03 17:57:52 +0200465 html := `<!DOCTYPE html>
466<html lang="en">
467<head>
468 <meta charset="UTF-8">
Akron40aaa632025-06-03 17:57:52 +0200469 <title>` + data.Title + `</title>
Akron06d21f02025-06-04 14:36:07 +0200470 <script src="` + data.SDK + `"
471 data-server="` + data.Server + `"></script>
Akron40aaa632025-06-03 17:57:52 +0200472</head>
473<body>
474 <div class="container">
475 <h1>` + data.Title + `</h1>
Akronc376dcc2025-06-04 17:00:18 +0200476 <p>` + data.Description + `</p>`
477
478 if data.MapID != "" {
479 html += `<p>Map ID: ` + data.MapID + `</p>`
480 }
481
482 html += ` <h2>Plugin Information</h2>
Akronc471c0a2025-06-04 11:56:22 +0200483 <p><strong>Version:</strong> <tt>` + data.Version + `</tt></p>
484 <p><strong>Build Date:</strong> <tt>` + data.Date + `</tt></p>
485 <p><strong>Build Hash:</strong> <tt>` + data.Hash + `</tt></p>
Akron40aaa632025-06-03 17:57:52 +0200486
Akronc471c0a2025-06-04 11:56:22 +0200487 <h2>Available API Endpoints</h2>
488 <dl>
Akron40aaa632025-06-03 17:57:52 +0200489
Akronc376dcc2025-06-04 17:00:18 +0200490 <dt><tt><strong>GET</strong> /:map</tt></dt>
491 <dd><small>Kalamar integration</small></dd>
Akrone1cff7c2025-06-04 18:43:32 +0200492
493 <dt><tt><strong>POST</strong> /:map/query</tt></dt>
Akronc376dcc2025-06-04 17:00:18 +0200494 <dd><small>Transform JSON query objects using term mapping rules</small></dd>
Akron4de47a92025-06-27 11:58:11 +0200495
496 <dt><tt><strong>POST</strong> /:map/response</tt></dt>
497 <dd><small>Transform JSON response objects using term mapping rules</small></dd>
Akronc376dcc2025-06-04 17:00:18 +0200498
Akronc471c0a2025-06-04 11:56:22 +0200499 </dl>
Akronc376dcc2025-06-04 17:00:18 +0200500
501 <h2>Available Term Mappings</h2>
Akrondab27112025-06-05 13:52:43 +0200502 <dl>`
Akron40aaa632025-06-03 17:57:52 +0200503
Akrondab27112025-06-05 13:52:43 +0200504 for _, m := range data.Mappings {
505 html += `<dt><tt>` + m.ID + `</tt></dt>`
506 html += `<dd>` + m.Description + `</dd>`
Akron40aaa632025-06-03 17:57:52 +0200507 }
508
509 html += `
Akroncb51f812025-06-30 15:24:20 +0200510 </dl></div>`
Akron06d21f02025-06-04 14:36:07 +0200511
Akronc376dcc2025-06-04 17:00:18 +0200512 if data.MapID != "" {
Akron80067202025-06-06 14:16:25 +0200513
Akrond0c88602025-06-27 16:57:21 +0200514 queryServiceURL, err := url.Parse(data.ServiceURL)
Akron80067202025-06-06 14:16:25 +0200515 if err != nil {
516 log.Warn().Err(err).Msg("Failed to join URL path")
517 }
518
519 // Use path.Join to normalize the path part
Akrond0c88602025-06-27 16:57:21 +0200520 queryServiceURL.Path = path.Join(queryServiceURL.Path, data.MapID+"/query")
Akroncb51f812025-06-30 15:24:20 +0200521
522 // Build query parameters for query URL
523 queryParamString := buildQueryParams(queryParams.Dir, queryParams.FoundryA, queryParams.FoundryB, queryParams.LayerA, queryParams.LayerB)
524 queryServiceURL.RawQuery = queryParamString
Akrond0c88602025-06-27 16:57:21 +0200525
526 responseServiceURL, err := url.Parse(data.ServiceURL)
527 if err != nil {
528 log.Warn().Err(err).Msg("Failed to join URL path")
529 }
530
531 // Use path.Join to normalize the path part
532 responseServiceURL.Path = path.Join(responseServiceURL.Path, data.MapID+"/response")
Akron80067202025-06-06 14:16:25 +0200533
Akroncb51f812025-06-30 15:24:20 +0200534 reversedDir := "btoa"
535 if queryParams.Dir == "btoa" {
536 reversedDir = "atob"
537 }
538
539 // Build query parameters for response URL (with reversed direction)
540 responseParamString := buildQueryParams(reversedDir, queryParams.FoundryA, queryParams.FoundryB, queryParams.LayerA, queryParams.LayerB)
541 responseServiceURL.RawQuery = responseParamString
542
543 html += `<script>
Akron06d21f02025-06-04 14:36:07 +0200544 <!-- activates/deactivates Mapper. -->
545
Akron3caee162025-07-01 17:44:58 +0200546 let qdata = {
547 'action' : 'pipe',
548 'service' : '` + queryServiceURL.String() + `'
549 };
Akron06d21f02025-06-04 14:36:07 +0200550
Akron3caee162025-07-01 17:44:58 +0200551 let rdata = {
552 'action' : 'pipe',
553 'service' : '` + responseServiceURL.String() + `'
554 };
Akrond0c88602025-06-27 16:57:21 +0200555
556
Akron3caee162025-07-01 17:44:58 +0200557 function pluginit (p) {
558 p.onMessage = function(msg) {
559 if (msg.key == 'termmapper') {
560 if (msg.value) {
561 qdata['job'] = 'add';
562 }
563 else {
564 qdata['job'] = 'del';
565 };
566 KorAPlugin.sendMsg(qdata);
Akrond0c88602025-06-27 16:57:21 +0200567 if (msg.value) {
Akron3caee162025-07-01 17:44:58 +0200568 rdata['job'] = 'add-after';
569 }
570 else {
571 rdata['job'] = 'del-after';
572 };
573 KorAPlugin.sendMsg(rdata);
574 };
575 };
576 };
577 </script>`
Akronc376dcc2025-06-04 17:00:18 +0200578 }
579
580 html += ` </body>
Akron40aaa632025-06-03 17:57:52 +0200581</html>`
582
583 return html
584}
Akron14678dc2025-06-05 13:01:38 +0200585
Akroncb51f812025-06-30 15:24:20 +0200586// buildQueryParams builds a query string from the provided parameters
587func buildQueryParams(dir, foundryA, foundryB, layerA, layerB string) string {
588 params := url.Values{}
589 if dir != "" {
590 params.Add("dir", dir)
591 }
592 if foundryA != "" {
593 params.Add("foundryA", foundryA)
594 }
595 if foundryB != "" {
596 params.Add("foundryB", foundryB)
597 }
598 if layerA != "" {
599 params.Add("layerA", layerA)
600 }
601 if layerB != "" {
602 params.Add("layerB", layerB)
603 }
604 return params.Encode()
605}
606
Akron14678dc2025-06-05 13:01:38 +0200607// expandGlobs expands glob patterns in the slice of file paths
608// Returns the expanded list of files or an error if glob expansion fails
609func expandGlobs(patterns []string) ([]string, error) {
610 var expanded []string
611
612 for _, pattern := range patterns {
613 // Use filepath.Glob which works cross-platform
614 matches, err := filepath.Glob(pattern)
615 if err != nil {
616 return nil, fmt.Errorf("failed to expand glob pattern '%s': %w", pattern, err)
617 }
618
619 // If no matches found, treat as literal filename (consistent with shell behavior)
620 if len(matches) == 0 {
621 log.Warn().Str("pattern", pattern).Msg("Glob pattern matched no files, treating as literal filename")
622 expanded = append(expanded, pattern)
623 } else {
624 expanded = append(expanded, matches...)
625 }
626 }
627
628 return expanded, nil
629}