blob: 5e2d65057f5f8f6026d3673d00d12b9713a1df88 [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
Akrona00d4752025-05-26 17:34:36 +020061func parseConfig() *appConfig {
62 cfg := &appConfig{}
Akronfc77b5e2025-06-04 11:44:43 +020063
64 desc := config.Description
65 desc += " [" + config.Version + "]"
66
Akron1fc750e2025-05-26 16:54:18 +020067 ctx := kong.Parse(cfg,
Akronfc77b5e2025-06-04 11:44:43 +020068 kong.Description(desc),
Akron1fc750e2025-05-26 16:54:18 +020069 kong.UsageOnError(),
70 )
71 if ctx.Error != nil {
72 fmt.Fprintln(os.Stderr, ctx.Error)
Akron49ceeb42025-05-23 17:46:01 +020073 os.Exit(1)
74 }
Akron49ceeb42025-05-23 17:46:01 +020075 return cfg
76}
77
78func setupLogger(level string) {
79 // Parse log level
80 lvl, err := zerolog.ParseLevel(strings.ToLower(level))
81 if err != nil {
82 log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
83 lvl = zerolog.InfoLevel
84 }
85
86 // Configure zerolog
87 zerolog.SetGlobalLevel(lvl)
88 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
89}
90
Akron3caee162025-07-01 17:44:58 +020091// setupFiberLogger configures fiber's logger middleware to integrate with zerolog
92func setupFiberLogger() fiber.Handler {
93 // Check if HTTP request logging should be enabled based on current log level
94 currentLevel := zerolog.GlobalLevel()
95
96 // Only enable HTTP request logging if log level is debug or info
97 if currentLevel > zerolog.InfoLevel {
98 return func(c *fiber.Ctx) error {
99 return c.Next()
100 }
101 }
102
103 return func(c *fiber.Ctx) error {
104 // Record start time
105 start := time.Now()
106
107 // Process request
108 err := c.Next()
109
110 // Calculate latency
111 latency := time.Since(start)
112 status := c.Response().StatusCode()
113
114 // Determine log level based on status code
115 logEvent := log.Info()
116 if status >= 400 && status < 500 {
117 logEvent = log.Warn()
118 } else if status >= 500 {
119 logEvent = log.Error()
120 }
121
122 // Log the request
123 logEvent.
124 Int("status", status).
125 Dur("latency", latency).
126 Str("method", c.Method()).
127 Str("path", c.Path()).
128 Str("ip", c.IP()).
129 Str("user_agent", c.Get("User-Agent")).
130 Msg("HTTP request")
131
132 return err
133 }
134}
135
Akron49ceeb42025-05-23 17:46:01 +0200136func main() {
137 // Parse command line flags
Akron1fc750e2025-05-26 16:54:18 +0200138 cfg := parseConfig()
Akron49ceeb42025-05-23 17:46:01 +0200139
Akrone1cff7c2025-06-04 18:43:32 +0200140 // Validate command line arguments
141 if cfg.Config == "" && len(cfg.Mappings) == 0 {
142 log.Fatal().Msg("At least one configuration source must be provided: use -c for main config file or -m for mapping files")
143 }
144
Akron14678dc2025-06-05 13:01:38 +0200145 // Expand glob patterns in mapping files
146 expandedMappings, err := expandGlobs(cfg.Mappings)
147 if err != nil {
148 log.Fatal().Err(err).Msg("Failed to expand glob patterns in mapping files")
149 }
150
Akrone1cff7c2025-06-04 18:43:32 +0200151 // Load configuration from multiple sources
Akron14678dc2025-06-05 13:01:38 +0200152 yamlConfig, err := config.LoadFromSources(cfg.Config, expandedMappings)
Akrona00d4752025-05-26 17:34:36 +0200153 if err != nil {
154 log.Fatal().Err(err).Msg("Failed to load configuration")
155 }
156
Akrona8a66ce2025-06-05 10:50:17 +0200157 finalPort := yamlConfig.Port
158 finalLogLevel := yamlConfig.LogLevel
159
160 // Use command line values if provided (they override config file)
161 if cfg.Port != nil {
162 finalPort = *cfg.Port
163 }
164 if cfg.LogLevel != nil {
165 finalLogLevel = *cfg.LogLevel
166 }
167
168 // Set up logging with the final log level
169 setupLogger(finalLogLevel)
170
Akron49ceeb42025-05-23 17:46:01 +0200171 // Create a new mapper instance
Akrona00d4752025-05-26 17:34:36 +0200172 m, err := mapper.NewMapper(yamlConfig.Lists)
Akron49ceeb42025-05-23 17:46:01 +0200173 if err != nil {
174 log.Fatal().Err(err).Msg("Failed to create mapper")
175 }
176
177 // Create fiber app
178 app := fiber.New(fiber.Config{
179 DisableStartupMessage: true,
Akron74e1c072025-05-26 14:38:25 +0200180 BodyLimit: maxInputLength,
Akronafbe86d2025-07-01 08:45:13 +0200181 ReadBufferSize: 64 * 1024, // 64KB - increase header size limit
182 WriteBufferSize: 64 * 1024, // 64KB - increase response buffer size
Akron49ceeb42025-05-23 17:46:01 +0200183 })
184
Akron3caee162025-07-01 17:44:58 +0200185 // Add zerolog-integrated logger middleware
186 app.Use(setupFiberLogger())
187
Akron49ceeb42025-05-23 17:46:01 +0200188 // Set up routes
Akron40aaa632025-06-03 17:57:52 +0200189 setupRoutes(app, m, yamlConfig)
Akron49ceeb42025-05-23 17:46:01 +0200190
191 // Start server
192 go func() {
Akrona8a66ce2025-06-05 10:50:17 +0200193 log.Info().Int("port", finalPort).Msg("Starting server")
Akronae3ffde2025-06-05 14:04:06 +0200194
195 for _, list := range yamlConfig.Lists {
196 log.Info().Str("id", list.ID).Str("desc", list.Description).Msg("Loaded mapping")
197 }
198
Akrona8a66ce2025-06-05 10:50:17 +0200199 if err := app.Listen(fmt.Sprintf(":%d", finalPort)); err != nil {
Akron49ceeb42025-05-23 17:46:01 +0200200 log.Fatal().Err(err).Msg("Server error")
201 }
202 }()
203
204 // Wait for interrupt signal
205 sigChan := make(chan os.Signal, 1)
206 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
207 <-sigChan
208
209 // Graceful shutdown
210 log.Info().Msg("Shutting down server")
211 if err := app.Shutdown(); err != nil {
212 log.Error().Err(err).Msg("Error during shutdown")
213 }
214}
215
Akron06d21f02025-06-04 14:36:07 +0200216func setupRoutes(app *fiber.App, m *mapper.Mapper, yamlConfig *config.MappingConfig) {
Akron49ceeb42025-05-23 17:46:01 +0200217 // Health check endpoint
218 app.Get("/health", func(c *fiber.Ctx) error {
219 return c.SendString("OK")
220 })
221
222 // Transformation endpoint
223 app.Post("/:map/query", handleTransform(m))
Akron40aaa632025-06-03 17:57:52 +0200224
Akron4de47a92025-06-27 11:58:11 +0200225 // Response transformation endpoint
226 app.Post("/:map/response", handleResponseTransform(m))
227
Akron40aaa632025-06-03 17:57:52 +0200228 // Kalamar plugin endpoint
Akronc471c0a2025-06-04 11:56:22 +0200229 app.Get("/", handleKalamarPlugin(yamlConfig))
Akronc376dcc2025-06-04 17:00:18 +0200230 app.Get("/:map", handleKalamarPlugin(yamlConfig))
Akron49ceeb42025-05-23 17:46:01 +0200231}
232
233func handleTransform(m *mapper.Mapper) fiber.Handler {
234 return func(c *fiber.Ctx) error {
235 // Get parameters
236 mapID := c.Params("map")
237 dir := c.Query("dir", "atob")
238 foundryA := c.Query("foundryA", "")
239 foundryB := c.Query("foundryB", "")
240 layerA := c.Query("layerA", "")
241 layerB := c.Query("layerB", "")
242
Akron74e1c072025-05-26 14:38:25 +0200243 // Validate input parameters
244 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, c.Body()); err != nil {
245 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
246 "error": err.Error(),
247 })
248 }
249
Akron49ceeb42025-05-23 17:46:01 +0200250 // Validate direction
251 if dir != "atob" && dir != "btoa" {
252 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
253 "error": "invalid direction, must be 'atob' or 'btoa'",
254 })
255 }
256
257 // Parse request body
Akron2cbdab52025-05-23 17:57:10 +0200258 var jsonData any
Akron49ceeb42025-05-23 17:46:01 +0200259 if err := c.BodyParser(&jsonData); err != nil {
260 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
261 "error": "invalid JSON in request body",
262 })
263 }
264
Akrona1a183f2025-05-26 17:47:33 +0200265 // Parse direction
266 direction, err := mapper.ParseDirection(dir)
267 if err != nil {
268 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
269 "error": err.Error(),
270 })
271 }
272
Akron49ceeb42025-05-23 17:46:01 +0200273 // Apply mappings
Akron7b4984e2025-05-26 19:12:20 +0200274 result, err := m.ApplyQueryMappings(mapID, mapper.MappingOptions{
Akrona1a183f2025-05-26 17:47:33 +0200275 Direction: direction,
Akron49ceeb42025-05-23 17:46:01 +0200276 FoundryA: foundryA,
277 FoundryB: foundryB,
278 LayerA: layerA,
279 LayerB: layerB,
280 }, jsonData)
281
282 if err != nil {
283 log.Error().Err(err).
284 Str("mapID", mapID).
285 Str("direction", dir).
286 Msg("Failed to apply mappings")
287
288 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
289 "error": err.Error(),
290 })
291 }
292
293 return c.JSON(result)
294 }
295}
Akron74e1c072025-05-26 14:38:25 +0200296
Akron4de47a92025-06-27 11:58:11 +0200297func handleResponseTransform(m *mapper.Mapper) fiber.Handler {
298 return func(c *fiber.Ctx) error {
299 // Get parameters
300 mapID := c.Params("map")
301 dir := c.Query("dir", "atob")
302 foundryA := c.Query("foundryA", "")
303 foundryB := c.Query("foundryB", "")
304 layerA := c.Query("layerA", "")
305 layerB := c.Query("layerB", "")
306
307 // Validate input parameters
308 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, c.Body()); err != nil {
309 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
310 "error": err.Error(),
311 })
312 }
313
314 // Validate direction
315 if dir != "atob" && dir != "btoa" {
316 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
317 "error": "invalid direction, must be 'atob' or 'btoa'",
318 })
319 }
320
321 // Parse request body
322 var jsonData any
323 if err := c.BodyParser(&jsonData); err != nil {
324 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
325 "error": "invalid JSON in request body",
326 })
327 }
328
329 // Parse direction
330 direction, err := mapper.ParseDirection(dir)
331 if err != nil {
332 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
333 "error": err.Error(),
334 })
335 }
336
337 // Apply response mappings
338 result, err := m.ApplyResponseMappings(mapID, mapper.MappingOptions{
339 Direction: direction,
340 FoundryA: foundryA,
341 FoundryB: foundryB,
342 LayerA: layerA,
343 LayerB: layerB,
344 }, jsonData)
345
346 if err != nil {
347 log.Error().Err(err).
348 Str("mapID", mapID).
349 Str("direction", dir).
350 Msg("Failed to apply response mappings")
351
352 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
353 "error": err.Error(),
354 })
355 }
356
357 return c.JSON(result)
358 }
359}
360
Akron74e1c072025-05-26 14:38:25 +0200361// validateInput checks if the input parameters are valid
362func validateInput(mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) error {
Akron69d43bf2025-05-26 17:09:00 +0200363 // Define parameter checks
364 params := []struct {
Akron74e1c072025-05-26 14:38:25 +0200365 name string
366 value string
367 }{
368 {"mapID", mapID},
369 {"dir", dir},
370 {"foundryA", foundryA},
371 {"foundryB", foundryB},
372 {"layerA", layerA},
373 {"layerB", layerB},
Akron69d43bf2025-05-26 17:09:00 +0200374 }
375
376 for _, param := range params {
377 // Check input lengths
378 if len(param.value) > maxParamLength {
379 return fmt.Errorf("%s too long (max %d bytes)", param.name, maxParamLength)
380 }
381 // Check for invalid characters in parameters
Akron74e1c072025-05-26 14:38:25 +0200382 if strings.ContainsAny(param.value, "<>{}[]\\") {
383 return fmt.Errorf("%s contains invalid characters", param.name)
384 }
385 }
386
Akron69d43bf2025-05-26 17:09:00 +0200387 if len(body) > maxInputLength {
388 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
389 }
390
Akron74e1c072025-05-26 14:38:25 +0200391 return nil
392}
Akron40aaa632025-06-03 17:57:52 +0200393
Akron06d21f02025-06-04 14:36:07 +0200394func handleKalamarPlugin(yamlConfig *config.MappingConfig) fiber.Handler {
Akron40aaa632025-06-03 17:57:52 +0200395 return func(c *fiber.Ctx) error {
Akronc376dcc2025-06-04 17:00:18 +0200396 mapID := c.Params("map")
397
Akroncb51f812025-06-30 15:24:20 +0200398 // Get query parameters
399 dir := c.Query("dir", "atob")
400 foundryA := c.Query("foundryA", "")
401 foundryB := c.Query("foundryB", "")
402 layerA := c.Query("layerA", "")
403 layerB := c.Query("layerB", "")
404
405 // Validate input parameters (reuse existing validation)
406 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, []byte{}); err != nil {
407 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
408 "error": err.Error(),
409 })
410 }
411
412 // Validate direction
413 if dir != "atob" && dir != "btoa" {
414 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
415 "error": "invalid direction, must be 'atob' or 'btoa'",
416 })
417 }
418
Akrondab27112025-06-05 13:52:43 +0200419 // Get list of available mappings
420 var mappings []TemplateMapping
Akron40aaa632025-06-03 17:57:52 +0200421 for _, list := range yamlConfig.Lists {
Akrondab27112025-06-05 13:52:43 +0200422 mappings = append(mappings, TemplateMapping{
423 ID: list.ID,
424 Description: list.Description,
425 })
Akron40aaa632025-06-03 17:57:52 +0200426 }
427
Akron06d21f02025-06-04 14:36:07 +0200428 // Use values from config (defaults are already applied during parsing)
429 server := yamlConfig.Server
430 sdk := yamlConfig.SDK
431
Akron40aaa632025-06-03 17:57:52 +0200432 // Prepare template data
433 data := TemplateData{
Akronfc77b5e2025-06-04 11:44:43 +0200434 Title: config.Title,
435 Version: config.Version,
436 Hash: config.Buildhash,
437 Date: config.Buildtime,
438 Description: config.Description,
Akron06d21f02025-06-04 14:36:07 +0200439 Server: server,
440 SDK: sdk,
Akron2ac2ec02025-06-05 15:26:42 +0200441 ServiceURL: yamlConfig.ServiceURL,
Akronc376dcc2025-06-04 17:00:18 +0200442 MapID: mapID,
Akrondab27112025-06-05 13:52:43 +0200443 Mappings: mappings,
Akron40aaa632025-06-03 17:57:52 +0200444 }
445
Akroncb51f812025-06-30 15:24:20 +0200446 // Add query parameters to template data
447 queryParams := QueryParams{
448 Dir: dir,
449 FoundryA: foundryA,
450 FoundryB: foundryB,
451 LayerA: layerA,
452 LayerB: layerB,
453 }
454
Akron40aaa632025-06-03 17:57:52 +0200455 // Generate HTML
Akroncb51f812025-06-30 15:24:20 +0200456 html := generateKalamarPluginHTML(data, queryParams)
Akron40aaa632025-06-03 17:57:52 +0200457
458 c.Set("Content-Type", "text/html")
459 return c.SendString(html)
460 }
461}
462
463// generateKalamarPluginHTML creates the HTML template for the Kalamar plugin page
464// This function can be easily modified to change the appearance and content
Akroncb51f812025-06-30 15:24:20 +0200465func generateKalamarPluginHTML(data TemplateData, queryParams QueryParams) string {
Akron40aaa632025-06-03 17:57:52 +0200466 html := `<!DOCTYPE html>
467<html lang="en">
468<head>
469 <meta charset="UTF-8">
Akron40aaa632025-06-03 17:57:52 +0200470 <title>` + data.Title + `</title>
Akron06d21f02025-06-04 14:36:07 +0200471 <script src="` + data.SDK + `"
472 data-server="` + data.Server + `"></script>
Akron40aaa632025-06-03 17:57:52 +0200473</head>
474<body>
475 <div class="container">
476 <h1>` + data.Title + `</h1>
Akronc376dcc2025-06-04 17:00:18 +0200477 <p>` + data.Description + `</p>`
478
479 if data.MapID != "" {
480 html += `<p>Map ID: ` + data.MapID + `</p>`
481 }
482
483 html += ` <h2>Plugin Information</h2>
Akronc471c0a2025-06-04 11:56:22 +0200484 <p><strong>Version:</strong> <tt>` + data.Version + `</tt></p>
485 <p><strong>Build Date:</strong> <tt>` + data.Date + `</tt></p>
486 <p><strong>Build Hash:</strong> <tt>` + data.Hash + `</tt></p>
Akron40aaa632025-06-03 17:57:52 +0200487
Akronc471c0a2025-06-04 11:56:22 +0200488 <h2>Available API Endpoints</h2>
489 <dl>
Akron40aaa632025-06-03 17:57:52 +0200490
Akronc376dcc2025-06-04 17:00:18 +0200491 <dt><tt><strong>GET</strong> /:map</tt></dt>
492 <dd><small>Kalamar integration</small></dd>
Akrone1cff7c2025-06-04 18:43:32 +0200493
494 <dt><tt><strong>POST</strong> /:map/query</tt></dt>
Akronc376dcc2025-06-04 17:00:18 +0200495 <dd><small>Transform JSON query objects using term mapping rules</small></dd>
Akron4de47a92025-06-27 11:58:11 +0200496
497 <dt><tt><strong>POST</strong> /:map/response</tt></dt>
498 <dd><small>Transform JSON response objects using term mapping rules</small></dd>
Akronc376dcc2025-06-04 17:00:18 +0200499
Akronc471c0a2025-06-04 11:56:22 +0200500 </dl>
Akronc376dcc2025-06-04 17:00:18 +0200501
502 <h2>Available Term Mappings</h2>
Akrondab27112025-06-05 13:52:43 +0200503 <dl>`
Akron40aaa632025-06-03 17:57:52 +0200504
Akrondab27112025-06-05 13:52:43 +0200505 for _, m := range data.Mappings {
506 html += `<dt><tt>` + m.ID + `</tt></dt>`
507 html += `<dd>` + m.Description + `</dd>`
Akron40aaa632025-06-03 17:57:52 +0200508 }
509
510 html += `
Akroncb51f812025-06-30 15:24:20 +0200511 </dl></div>`
Akron06d21f02025-06-04 14:36:07 +0200512
Akronc376dcc2025-06-04 17:00:18 +0200513 if data.MapID != "" {
Akron80067202025-06-06 14:16:25 +0200514
Akrond0c88602025-06-27 16:57:21 +0200515 queryServiceURL, err := url.Parse(data.ServiceURL)
Akron80067202025-06-06 14:16:25 +0200516 if err != nil {
517 log.Warn().Err(err).Msg("Failed to join URL path")
518 }
519
520 // Use path.Join to normalize the path part
Akrond0c88602025-06-27 16:57:21 +0200521 queryServiceURL.Path = path.Join(queryServiceURL.Path, data.MapID+"/query")
Akroncb51f812025-06-30 15:24:20 +0200522
523 // Build query parameters for query URL
524 queryParamString := buildQueryParams(queryParams.Dir, queryParams.FoundryA, queryParams.FoundryB, queryParams.LayerA, queryParams.LayerB)
525 queryServiceURL.RawQuery = queryParamString
Akrond0c88602025-06-27 16:57:21 +0200526
527 responseServiceURL, err := url.Parse(data.ServiceURL)
528 if err != nil {
529 log.Warn().Err(err).Msg("Failed to join URL path")
530 }
531
532 // Use path.Join to normalize the path part
533 responseServiceURL.Path = path.Join(responseServiceURL.Path, data.MapID+"/response")
Akron80067202025-06-06 14:16:25 +0200534
Akroncb51f812025-06-30 15:24:20 +0200535 reversedDir := "btoa"
536 if queryParams.Dir == "btoa" {
537 reversedDir = "atob"
538 }
539
540 // Build query parameters for response URL (with reversed direction)
541 responseParamString := buildQueryParams(reversedDir, queryParams.FoundryA, queryParams.FoundryB, queryParams.LayerA, queryParams.LayerB)
542 responseServiceURL.RawQuery = responseParamString
543
544 html += `<script>
Akron06d21f02025-06-04 14:36:07 +0200545 <!-- activates/deactivates Mapper. -->
546
Akron3caee162025-07-01 17:44:58 +0200547 let qdata = {
548 'action' : 'pipe',
549 'service' : '` + queryServiceURL.String() + `'
550 };
Akron06d21f02025-06-04 14:36:07 +0200551
Akron3caee162025-07-01 17:44:58 +0200552 let rdata = {
553 'action' : 'pipe',
554 'service' : '` + responseServiceURL.String() + `'
555 };
Akrond0c88602025-06-27 16:57:21 +0200556
557
Akron3caee162025-07-01 17:44:58 +0200558 function pluginit (p) {
559 p.onMessage = function(msg) {
560 if (msg.key == 'termmapper') {
561 if (msg.value) {
562 qdata['job'] = 'add';
563 }
564 else {
565 qdata['job'] = 'del';
566 };
567 KorAPlugin.sendMsg(qdata);
Akrond0c88602025-06-27 16:57:21 +0200568 if (msg.value) {
Akron3caee162025-07-01 17:44:58 +0200569 rdata['job'] = 'add-after';
570 }
571 else {
572 rdata['job'] = 'del-after';
573 };
574 KorAPlugin.sendMsg(rdata);
575 };
576 };
577 };
578 </script>`
Akronc376dcc2025-06-04 17:00:18 +0200579 }
580
581 html += ` </body>
Akron40aaa632025-06-03 17:57:52 +0200582</html>`
583
584 return html
585}
Akron14678dc2025-06-05 13:01:38 +0200586
Akroncb51f812025-06-30 15:24:20 +0200587// buildQueryParams builds a query string from the provided parameters
588func buildQueryParams(dir, foundryA, foundryB, layerA, layerB string) string {
589 params := url.Values{}
590 if dir != "" {
591 params.Add("dir", dir)
592 }
593 if foundryA != "" {
594 params.Add("foundryA", foundryA)
595 }
596 if foundryB != "" {
597 params.Add("foundryB", foundryB)
598 }
599 if layerA != "" {
600 params.Add("layerA", layerA)
601 }
602 if layerB != "" {
603 params.Add("layerB", layerB)
604 }
605 return params.Encode()
606}
607
Akron14678dc2025-06-05 13:01:38 +0200608// expandGlobs expands glob patterns in the slice of file paths
609// Returns the expanded list of files or an error if glob expansion fails
610func expandGlobs(patterns []string) ([]string, error) {
611 var expanded []string
612
613 for _, pattern := range patterns {
614 // Use filepath.Glob which works cross-platform
615 matches, err := filepath.Glob(pattern)
616 if err != nil {
617 return nil, fmt.Errorf("failed to expand glob pattern '%s': %w", pattern, err)
618 }
619
620 // If no matches found, treat as literal filename (consistent with shell behavior)
621 if len(matches) == 0 {
622 log.Warn().Str("pattern", pattern).Msg("Glob pattern matched no files, treating as literal filename")
623 expanded = append(expanded, pattern)
624 } else {
625 expanded = append(expanded, matches...)
626 }
627 }
628
629 return expanded, nil
630}