diff --git a/platform/platform-plugins/build.gradle.kts b/platform/platform-plugins/build.gradle.kts index 219df99..6fc9958 100644 --- a/platform/platform-plugins/build.gradle.kts +++ b/platform/platform-plugins/build.gradle.kts @@ -30,6 +30,12 @@ dependencies { implementation(project(":platform:platform-security")) // for PermissionEvaluator refresh wiring implementation(project(":platform:platform-workflow")) // for per-plug-in TaskHandler registration implementation(project(":platform:platform-jobs")) // for per-plug-in JobHandler registration + // Note: platform-files and platform-reports are NOT needed here + // because DefaultPluginContext accepts api.v1 FileStorage + + // ReportRenderer interfaces (both in api-v1), and the concrete + // beans are wired by Spring at distribution boot. Adding them + // as dependencies would work too but would unnecessarily couple + // platform-plugins to two more modules. implementation(libs.spring.boot.starter) implementation(libs.spring.boot.starter.web) // for @RestController on the dispatcher diff --git a/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/DefaultPluginContext.kt b/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/DefaultPluginContext.kt index de69cdb..ab3c7cd 100644 --- a/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/DefaultPluginContext.kt +++ b/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/DefaultPluginContext.kt @@ -9,8 +9,10 @@ import org.vibeerp.api.v1.persistence.Transaction import org.vibeerp.api.v1.plugin.PluginContext import org.vibeerp.api.v1.plugin.PluginEndpointRegistrar import org.vibeerp.api.v1.plugin.PluginJdbc +import org.vibeerp.api.v1.files.FileStorage import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar import org.vibeerp.api.v1.plugin.PluginLogger +import org.vibeerp.api.v1.reports.ReportRenderer import org.vibeerp.api.v1.security.PermissionCheck import org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar @@ -51,6 +53,8 @@ internal class DefaultPluginContext( private val sharedLocaleProvider: LocaleProvider, private val scopedTaskHandlers: PluginTaskHandlerRegistrar, private val scopedJobHandlers: PluginJobHandlerRegistrar, + private val sharedFileStorage: FileStorage, + private val sharedReportRenderer: ReportRenderer, ) : PluginContext { override val logger: PluginLogger = Slf4jPluginLogger(pluginId, delegateLogger) @@ -119,6 +123,24 @@ internal class DefaultPluginContext( */ override val jobs: PluginJobHandlerRegistrar = scopedJobHandlers + /** + * Shared host [FileStorage]. Plug-ins persist generated + * report PDFs, uploaded attachments, exports, and any other + * binary blobs through this. Wired in P1.9 + the plug-in + * loader follow-up; backed by `platform-files`' + * [org.vibeerp.platform.files.LocalDiskFileStorage] by default. + */ + override val files: FileStorage = sharedFileStorage + + /** + * Shared host [ReportRenderer]. Plug-ins render JRXML + * templates (loaded from their own classpath, the file store, + * or a database row) into PDF bytes. Wired alongside + * [files] in the same chunk so a plug-in can do + * "render → store → return handle" in one method. + */ + override val reports: ReportRenderer = sharedReportRenderer + // ─── Not yet implemented ─────────────────────────────────────── override val transaction: Transaction diff --git a/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/VibeErpPluginManager.kt b/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/VibeErpPluginManager.kt index 7f512bd..8dfde88 100644 --- a/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/VibeErpPluginManager.kt +++ b/platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/VibeErpPluginManager.kt @@ -8,8 +8,10 @@ import org.springframework.beans.factory.InitializingBean import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component import org.vibeerp.api.v1.event.EventBus +import org.vibeerp.api.v1.files.FileStorage import org.vibeerp.api.v1.i18n.LocaleProvider import org.vibeerp.api.v1.plugin.PluginJdbc +import org.vibeerp.api.v1.reports.ReportRenderer import org.vibeerp.platform.i18n.IcuTranslator import org.vibeerp.platform.metadata.MetadataLoader import org.vibeerp.platform.metadata.customfield.CustomFieldRegistry @@ -73,6 +75,8 @@ class VibeErpPluginManager( private val taskHandlerRegistry: TaskHandlerRegistry, private val pluginProcessDeployer: PluginProcessDeployer, private val jobHandlerRegistry: JobHandlerRegistry, + private val fileStorage: FileStorage, + private val reportRenderer: ReportRenderer, ) : DefaultPluginManager(Paths.get(properties.directory)), InitializingBean, DisposableBean { private val log = LoggerFactory.getLogger(VibeErpPluginManager::class.java) @@ -285,6 +289,8 @@ class VibeErpPluginManager( sharedLocaleProvider = localeProvider, scopedTaskHandlers = ScopedTaskHandlerRegistrar(taskHandlerRegistry, pluginId), scopedJobHandlers = ScopedJobHandlerRegistrar(jobHandlerRegistry, pluginId), + sharedFileStorage = fileStorage, + sharedReportRenderer = reportRenderer, ) try { vibeErpPlugin.start(context) diff --git a/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt b/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt index c261c3a..fd176c3 100644 --- a/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt +++ b/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt @@ -270,7 +270,89 @@ class PrintingShopPlugin(wrapper: PluginWrapper) : Pf4jPlugin(wrapper), VibeErpP ) } - context.logger.info("registered 7 endpoints under /api/v1/plugins/printing-shop/") + // ─── POST /plates/{id}/generate-quote-pdf ───────────────── + // + // Renders a quote PDF for a plate by composing every + // extension seam the framework offers: context.jdbc reads + // the plate from the plug-in's own table, context.reports + // compiles + fills a JRXML template loaded from the + // plug-in's OWN classloader (not the host classpath, so + // Jasper's classpath auto-deploy doesn't see it), and + // context.files persists the PDF under a plug-in-scoped + // key so a future GET /api/v1/files/download?key=... can + // retrieve it. The response returns the file handle so the + // caller can download it immediately. + context.endpoints.register(HttpMethod.POST, "/plates/{id}/generate-quote-pdf") { request -> + val rawId = request.pathParameters["id"] ?: return@register PluginResponse(400, mapOf("detail" to "missing id")) + val id = try { UUID.fromString(rawId) } catch (ex: IllegalArgumentException) { + return@register PluginResponse(400, mapOf("detail" to "id is not a valid UUID")) + } + val body = parseJsonObject(request.body.orEmpty()) + val customerName = body["customerName"] as? String ?: "" + + val plate = context.jdbc.queryForObject( + "SELECT code, name, width_mm, height_mm, status FROM plugin_printingshop__plate WHERE id = :id", + mapOf("id" to id), + ) { row -> + mapOf( + "code" to row.string("code"), + "name" to row.string("name"), + "widthMm" to row.int("width_mm"), + "heightMm" to row.int("height_mm"), + "status" to row.string("status"), + ) + } ?: return@register PluginResponse(404, mapOf("detail" to "plate not found: $id")) + + // Load the JRXML from THIS plug-in's classloader, not + // the host's. PF4J's PluginClassLoader is parent-first + // for anything not in the plug-in jar, so the plug-in's + // own resources/reports/quote-template.jrxml is reached + // via the local classloader; the host's ping-report is + // invisible to the plug-in, and vice versa. + val templateStream = this::class.java.classLoader + .getResourceAsStream("reports/quote-template.jrxml") + ?: return@register PluginResponse(500, mapOf("detail" to "quote template missing from plug-in classpath")) + + val pdfBytes = templateStream.use { stream -> + context.reports.renderPdf( + template = stream, + data = mapOf( + "plateCode" to (plate["code"] as String), + "plateName" to (plate["name"] as String), + "widthMm" to (plate["widthMm"] as Int), + "heightMm" to (plate["heightMm"] as Int), + "status" to (plate["status"] as String), + "customerName" to customerName, + ), + ) + } + + val fileKey = "plugin-printing-shop/quotes/quote-${plate["code"]}.pdf" + val handle = context.files.put( + key = fileKey, + contentType = "application/pdf", + content = java.io.ByteArrayInputStream(pdfBytes), + ) + + context.logger.info( + "generated quote PDF for plate ${plate["code"]} (${pdfBytes.size} bytes) → $fileKey", + ) + + PluginResponse( + status = 201, + body = mapOf( + "plateId" to id.toString(), + "plateCode" to plate["code"], + "customerName" to customerName, + "fileKey" to handle.key, + "fileSize" to handle.size, + "fileContentType" to handle.contentType, + "downloadUrl" to "/api/v1/files/download?key=${handle.key}", + ), + ) + } + + context.logger.info("registered 8 endpoints under /api/v1/plugins/printing-shop/") // ─── Workflow task handlers (P2.1 plug-in-loaded registration) ── // The host's VibeErpPluginManager wires context.taskHandlers to a diff --git a/reference-customer/plugin-printing-shop/src/main/resources/reports/quote-template.jrxml b/reference-customer/plugin-printing-shop/src/main/resources/reports/quote-template.jrxml new file mode 100644 index 0000000..64a5322 --- /dev/null +++ b/reference-customer/plugin-printing-shop/src/main/resources/reports/quote-template.jrxml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + <band height="80"> + <staticText> + <reportElement x="0" y="0" width="535" height="36"/> + <textElement textAlignment="Center"> + <font size="24" isBold="true"/> + </textElement> + <text><![CDATA[Printing shop — plate quote]]></text> + </staticText> + <textField> + <reportElement x="0" y="44" width="535" height="20"/> + <textElement textAlignment="Center"> + <font size="12"/> + </textElement> + <textFieldExpression><![CDATA["Prepared for " + $P{customerName}]]></textFieldExpression> + </textField> + </band> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +