@@ -184,6 +184,35 @@ func GenerateUsageReportPDF(db *database.DB, queryCtx *models.QueryContext, hasP
184184 return service .generateUsageReportPDF (userID )
185185}
186186
187+ // recompileTemplate compiles a .jrxml file using JasperStarter
188+ // This is called when a .jasper file is missing or corrupt (e.g., Studio-compiled with UUID suffix)
189+ func recompileTemplate (templateDir , baseTemplateName string ) error {
190+ jrxmlPath := filepath .Join (templateDir , baseTemplateName + ".jrxml" )
191+
192+ logrus .WithFields (logrus.Fields {
193+ "template" : baseTemplateName ,
194+ }).Info ("Recompiling template from .jrxml source with JasperStarter" )
195+
196+ cmd := exec .Command ("/opt/jasperstarter/bin/jasperstarter" , "compile" , jrxmlPath )
197+ cmd .Dir = templateDir
198+ output , err := cmd .CombinedOutput ()
199+ if err != nil {
200+ logrus .WithFields (logrus.Fields {
201+ "template" : baseTemplateName ,
202+ "error" : err ,
203+ "output" : string (output ),
204+ }).Error ("Failed to recompile template" )
205+ return fmt .Errorf ("failed to recompile template %s: %w" , baseTemplateName , err )
206+ }
207+
208+ logrus .WithFields (logrus.Fields {
209+ "template" : baseTemplateName ,
210+ "output" : string (output ),
211+ }).Info ("Successfully recompiled template from .jrxml source" )
212+
213+ return nil
214+ }
215+
187216func generateReportPDF (config models.PDFConfig , filterSummary []models.PDFFilterLine , templateName string ) ([]byte , error ) {
188217 type ReportData struct {
189218 Rows [][]string `json:"rows"`
@@ -203,6 +232,17 @@ func generateReportPDF(config models.PDFConfig, filterSummary []models.PDFFilter
203232 return nil , fmt .Errorf ("failed to marshal report data: %w" , err )
204233 }
205234
235+ // IMPORTANT: The go-jasper library wraps string parameter values in quotes for command-line safety.
236+ // See: https://github.com/evertonvps/go-jasper/blob/main/go-jasper.go#L88
237+ //
238+ // This causes JasperReports to receive parameter values like "\"Attendance\"" instead of "Attendance".
239+ // We CANNOT strip quotes here because the backend values don't have quotes yet!
240+ // The quotes are added by go-jasper when generating command-line args.
241+ //
242+ // Solution: JRXML templates must use .replaceAll("\"", "") to strip quotes at display time.
243+ // Future developers: ALL command-line parameters (ReportTitle, GeneratedDate, FilterSummary, etc.)
244+ // MUST include .replaceAll("\"", "") in their JRXML template fields.
245+ // Table data from JSON does NOT need quote stripping (not passed as command-line args).
206246 title := config .Title
207247 if title == "" {
208248 title = "Report"
@@ -273,16 +313,45 @@ func generateReportPDF(config models.PDFConfig, filterSummary []models.PDFFilter
273313 templateDir = "/templates"
274314 }
275315 compiledTemplatePath := filepath .Join (templateDir , templateName + ".jasper" )
316+ jrxmlTemplatePath := filepath .Join (templateDir , templateName + ".jrxml" )
276317
277318 pdfBytes , err := gj .Process (compiledTemplatePath )
278319 if err != nil {
279- logrus .WithFields (logrus.Fields {
280- "template_path" : compiledTemplatePath ,
281- "template_dir" : templateDir ,
282- "error" : err ,
283- }).Error ("Failed to process compiled template" )
320+ // Check if this is a Jaspersoft Studio compilation error (NoClassDefFoundError with UUID suffix)
321+ // If so, recompile from .jrxml source and retry
322+ errStr := err .Error ()
323+ if strings .Contains (errStr , "NoClassDefFoundError" ) && strings .Contains (errStr , "_" ) {
324+ logrus .WithFields (logrus.Fields {
325+ "template" : templateName ,
326+ "error" : err ,
327+ "jrxml_path" : jrxmlTemplatePath ,
328+ }).Warn ("Detected Jaspersoft Studio-compiled .jasper file (UUID suffix), recompiling with JasperStarter" )
329+
330+ if recompileErr := recompileTemplate (templateDir , templateName ); recompileErr != nil {
331+ return nil , fmt .Errorf ("failed to recover from Studio compilation: %w (original error: %w)" , recompileErr , err )
332+ }
333+
334+ pdfBytes , err = gj .Process (compiledTemplatePath )
335+ if err != nil {
336+ logrus .WithFields (logrus.Fields {
337+ "template_path" : compiledTemplatePath ,
338+ "template_dir" : templateDir ,
339+ "error" : err ,
340+ }).Error ("Failed to process compiled template after recompilation" )
341+ return nil , fmt .Errorf ("failed to process template after recompilation: %w" , err )
342+ }
284343
285- return nil , fmt .Errorf ("failed to process template: %w" , err )
344+ logrus .WithFields (logrus.Fields {
345+ "template" : templateName ,
346+ }).Info ("Successfully recovered from Studio compilation and generated PDF" )
347+ } else {
348+ logrus .WithFields (logrus.Fields {
349+ "template_path" : compiledTemplatePath ,
350+ "template_dir" : templateDir ,
351+ "error" : err ,
352+ }).Error ("Failed to process compiled template" )
353+ return nil , fmt .Errorf ("failed to process template: %w" , err )
354+ }
286355 }
287356
288357 return pdfBytes , nil
0 commit comments