blob: 9b6b8aefb7c7a1cbfa35ab28d0b9d5cd900ea5bf [file] [log] [blame]
Akron49ceeb42025-05-23 17:46:01 +02001package main
2
3import (
Akron49ceeb42025-05-23 17:46:01 +02004 "fmt"
5 "os"
6 "os/signal"
7 "strings"
8 "syscall"
9
Akrona00d4752025-05-26 17:34:36 +020010 "github.com/KorAP/KoralPipe-TermMapper/config"
Akronfa55bb22025-05-26 15:10:42 +020011 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron1fc750e2025-05-26 16:54:18 +020012 "github.com/alecthomas/kong"
Akron49ceeb42025-05-23 17:46:01 +020013 "github.com/gofiber/fiber/v2"
14 "github.com/rs/zerolog"
15 "github.com/rs/zerolog/log"
16)
17
Akron74e1c072025-05-26 14:38:25 +020018const (
19 maxInputLength = 1024 * 1024 // 1MB
20 maxParamLength = 1024 // 1KB
21)
22
Akrona00d4752025-05-26 17:34:36 +020023type appConfig struct {
Akron1fc750e2025-05-26 16:54:18 +020024 Port int `kong:"short='p',default='8080',help='Port to listen on'"`
25 Config string `kong:"short='c',required,help='YAML configuration file containing mapping directives'"`
26 LogLevel string `kong:"short='l',default='info',help='Log level (debug, info, warn, error)'"`
Akron49ceeb42025-05-23 17:46:01 +020027}
28
Akron40aaa632025-06-03 17:57:52 +020029// TemplateData holds data for the Kalamar plugin template
30type TemplateData struct {
31 Title string
32 Version string
Akronfc77b5e2025-06-04 11:44:43 +020033 Hash string
34 Date string
Akron40aaa632025-06-03 17:57:52 +020035 Description string
36 MappingIDs []string
37}
38
Akrona00d4752025-05-26 17:34:36 +020039func parseConfig() *appConfig {
40 cfg := &appConfig{}
Akronfc77b5e2025-06-04 11:44:43 +020041
42 desc := config.Description
43 desc += " [" + config.Version + "]"
44
Akron1fc750e2025-05-26 16:54:18 +020045 ctx := kong.Parse(cfg,
Akronfc77b5e2025-06-04 11:44:43 +020046 kong.Description(desc),
Akron1fc750e2025-05-26 16:54:18 +020047 kong.UsageOnError(),
48 )
49 if ctx.Error != nil {
50 fmt.Fprintln(os.Stderr, ctx.Error)
Akron49ceeb42025-05-23 17:46:01 +020051 os.Exit(1)
52 }
Akron49ceeb42025-05-23 17:46:01 +020053 return cfg
54}
55
56func setupLogger(level string) {
57 // Parse log level
58 lvl, err := zerolog.ParseLevel(strings.ToLower(level))
59 if err != nil {
60 log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
61 lvl = zerolog.InfoLevel
62 }
63
64 // Configure zerolog
65 zerolog.SetGlobalLevel(lvl)
66 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
67}
68
69func main() {
70 // Parse command line flags
Akron1fc750e2025-05-26 16:54:18 +020071 cfg := parseConfig()
Akron49ceeb42025-05-23 17:46:01 +020072
73 // Set up logging
Akron1fc750e2025-05-26 16:54:18 +020074 setupLogger(cfg.LogLevel)
Akron49ceeb42025-05-23 17:46:01 +020075
Akrona00d4752025-05-26 17:34:36 +020076 // Load configuration file
77 yamlConfig, err := config.LoadConfig(cfg.Config)
78 if err != nil {
79 log.Fatal().Err(err).Msg("Failed to load configuration")
80 }
81
Akron49ceeb42025-05-23 17:46:01 +020082 // Create a new mapper instance
Akrona00d4752025-05-26 17:34:36 +020083 m, err := mapper.NewMapper(yamlConfig.Lists)
Akron49ceeb42025-05-23 17:46:01 +020084 if err != nil {
85 log.Fatal().Err(err).Msg("Failed to create mapper")
86 }
87
88 // Create fiber app
89 app := fiber.New(fiber.Config{
90 DisableStartupMessage: true,
Akron74e1c072025-05-26 14:38:25 +020091 BodyLimit: maxInputLength,
Akron49ceeb42025-05-23 17:46:01 +020092 })
93
94 // Set up routes
Akron40aaa632025-06-03 17:57:52 +020095 setupRoutes(app, m, yamlConfig)
Akron49ceeb42025-05-23 17:46:01 +020096
97 // Start server
98 go func() {
Akron1fc750e2025-05-26 16:54:18 +020099 log.Info().Int("port", cfg.Port).Msg("Starting server")
100 if err := app.Listen(fmt.Sprintf(":%d", cfg.Port)); err != nil {
Akron49ceeb42025-05-23 17:46:01 +0200101 log.Fatal().Err(err).Msg("Server error")
102 }
103 }()
104
105 // Wait for interrupt signal
106 sigChan := make(chan os.Signal, 1)
107 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
108 <-sigChan
109
110 // Graceful shutdown
111 log.Info().Msg("Shutting down server")
112 if err := app.Shutdown(); err != nil {
113 log.Error().Err(err).Msg("Error during shutdown")
114 }
115}
116
Akron40aaa632025-06-03 17:57:52 +0200117func setupRoutes(app *fiber.App, m *mapper.Mapper, yamlConfig *config.MappingLists) {
Akron49ceeb42025-05-23 17:46:01 +0200118 // Health check endpoint
119 app.Get("/health", func(c *fiber.Ctx) error {
120 return c.SendString("OK")
121 })
122
123 // Transformation endpoint
124 app.Post("/:map/query", handleTransform(m))
Akron40aaa632025-06-03 17:57:52 +0200125
126 // Kalamar plugin endpoint
127 app.Get("/kalamarplugin", handleKalamarPlugin(yamlConfig))
Akron49ceeb42025-05-23 17:46:01 +0200128}
129
130func handleTransform(m *mapper.Mapper) fiber.Handler {
131 return func(c *fiber.Ctx) error {
132 // Get parameters
133 mapID := c.Params("map")
134 dir := c.Query("dir", "atob")
135 foundryA := c.Query("foundryA", "")
136 foundryB := c.Query("foundryB", "")
137 layerA := c.Query("layerA", "")
138 layerB := c.Query("layerB", "")
139
Akron74e1c072025-05-26 14:38:25 +0200140 // Validate input parameters
141 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, c.Body()); err != nil {
142 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
143 "error": err.Error(),
144 })
145 }
146
Akron49ceeb42025-05-23 17:46:01 +0200147 // Validate direction
148 if dir != "atob" && dir != "btoa" {
149 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
150 "error": "invalid direction, must be 'atob' or 'btoa'",
151 })
152 }
153
154 // Parse request body
Akron2cbdab52025-05-23 17:57:10 +0200155 var jsonData any
Akron49ceeb42025-05-23 17:46:01 +0200156 if err := c.BodyParser(&jsonData); err != nil {
157 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
158 "error": "invalid JSON in request body",
159 })
160 }
161
Akrona1a183f2025-05-26 17:47:33 +0200162 // Parse direction
163 direction, err := mapper.ParseDirection(dir)
164 if err != nil {
165 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
166 "error": err.Error(),
167 })
168 }
169
Akron49ceeb42025-05-23 17:46:01 +0200170 // Apply mappings
Akron7b4984e2025-05-26 19:12:20 +0200171 result, err := m.ApplyQueryMappings(mapID, mapper.MappingOptions{
Akrona1a183f2025-05-26 17:47:33 +0200172 Direction: direction,
Akron49ceeb42025-05-23 17:46:01 +0200173 FoundryA: foundryA,
174 FoundryB: foundryB,
175 LayerA: layerA,
176 LayerB: layerB,
177 }, jsonData)
178
179 if err != nil {
180 log.Error().Err(err).
181 Str("mapID", mapID).
182 Str("direction", dir).
183 Msg("Failed to apply mappings")
184
185 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
186 "error": err.Error(),
187 })
188 }
189
190 return c.JSON(result)
191 }
192}
Akron74e1c072025-05-26 14:38:25 +0200193
194// validateInput checks if the input parameters are valid
195func validateInput(mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) error {
Akron69d43bf2025-05-26 17:09:00 +0200196 // Define parameter checks
197 params := []struct {
Akron74e1c072025-05-26 14:38:25 +0200198 name string
199 value string
200 }{
201 {"mapID", mapID},
202 {"dir", dir},
203 {"foundryA", foundryA},
204 {"foundryB", foundryB},
205 {"layerA", layerA},
206 {"layerB", layerB},
Akron69d43bf2025-05-26 17:09:00 +0200207 }
208
209 for _, param := range params {
210 // Check input lengths
211 if len(param.value) > maxParamLength {
212 return fmt.Errorf("%s too long (max %d bytes)", param.name, maxParamLength)
213 }
214 // Check for invalid characters in parameters
Akron74e1c072025-05-26 14:38:25 +0200215 if strings.ContainsAny(param.value, "<>{}[]\\") {
216 return fmt.Errorf("%s contains invalid characters", param.name)
217 }
218 }
219
Akron69d43bf2025-05-26 17:09:00 +0200220 if len(body) > maxInputLength {
221 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
222 }
223
Akron74e1c072025-05-26 14:38:25 +0200224 return nil
225}
Akron40aaa632025-06-03 17:57:52 +0200226
227func handleKalamarPlugin(yamlConfig *config.MappingLists) fiber.Handler {
228 return func(c *fiber.Ctx) error {
229 // Get list of available mapping IDs
230 var mappingIDs []string
231 for _, list := range yamlConfig.Lists {
232 mappingIDs = append(mappingIDs, list.ID)
233 }
234
235 // Prepare template data
236 data := TemplateData{
Akronfc77b5e2025-06-04 11:44:43 +0200237 Title: config.Title,
238 Version: config.Version,
239 Hash: config.Buildhash,
240 Date: config.Buildtime,
241 Description: config.Description,
Akron40aaa632025-06-03 17:57:52 +0200242 MappingIDs: mappingIDs,
243 }
244
245 // Generate HTML
246 html := generateKalamarPluginHTML(data)
247
248 c.Set("Content-Type", "text/html")
249 return c.SendString(html)
250 }
251}
252
253// generateKalamarPluginHTML creates the HTML template for the Kalamar plugin page
254// This function can be easily modified to change the appearance and content
255func generateKalamarPluginHTML(data TemplateData) string {
256 html := `<!DOCTYPE html>
257<html lang="en">
258<head>
259 <meta charset="UTF-8">
260 <meta name="viewport" content="width=device-width, initial-scale=1.0">
261 <title>` + data.Title + `</title>
262</head>
263<body>
264 <div class="container">
265 <h1>` + data.Title + `</h1>
266
267 <div>
268 <h2>Plugin Information</h2>
269 <p><strong>Version:</strong> ` + data.Version + `</p>
Akronfc77b5e2025-06-04 11:44:43 +0200270 <p><strong>Build Date:</strong> ` + data.Date + `</p>
271 <p><strong>Build Hash:</strong> ` + data.Hash + `</p>
Akron40aaa632025-06-03 17:57:52 +0200272 <p><strong>Description:</strong> ` + data.Description + `</p>
273 </div>
274
275 <div class="plugin-info">
276 <h2>Available API Endpoints</h2>
277 <div class="api-endpoint">
278 <strong>POST</strong> /:map/query?dir=atob&foundryA=&foundryB=&layerA=&layerB=
279 <br><small>Transform JSON objects using term mapping rules</small>
280 </div>
281 <div class="api-endpoint">
282 <strong>GET</strong> /health
283 <br><small>Health check endpoint</small>
284 </div>
285 <div class="api-endpoint">
286 <strong>GET</strong> /kalamarplugin
287 <br><small>This entry point for Kalamar integration</small>
288 </div>
289 </div>
290
291 <div class="plugin-info">
292 <h2>Available Term Mappings</h2>
293 <ul>`
294
295 for _, id := range data.MappingIDs {
296 html += `
297 <li>` + id + `</li>`
298 }
299
300 html += `
301 </ul>
302 </div>
303</body>
304</html>`
305
306 return html
307}