diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index be60dd8..21a547c 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -9,6 +9,7 @@ */ package org.openmrs.module.patientdocuments.renderer; +import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.openmrs.module.patientdocuments.reports.PatientIdStickerReportManager.DATASET_KEY_STICKER_FIELDS; @@ -16,6 +17,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,6 +48,7 @@ import org.openmrs.module.reporting.report.renderer.RenderingException; import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer; import org.openmrs.module.reporting.report.renderer.ReportRenderer; +import org.openmrs.util.OpenmrsUtil; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -113,6 +116,10 @@ protected String getStringValue(DataSetRow row, DataSetColumn column) { @Override public void render(ReportData results, String argument, OutputStream out) throws IOException, RenderingException { + render(results, argument, out, null); + } + + public void render(ReportData results, String argument, OutputStream out, byte[] defaultLogoBytes) throws IOException, RenderingException { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; try { @@ -137,7 +144,7 @@ public void render(ReportData results, String argument, OutputStream out) throws Element templatePIDElement = createStickerTemplate(doc); // Handle header configuration - configureHeader(doc, templatePIDElement); + configureHeader(doc, templatePIDElement, defaultLogoBytes); // Process data set fields processDataSetFields(results, doc, templatePIDElement); @@ -210,13 +217,11 @@ private Element createStickerTemplate(Document doc) { return templatePIDElement; } - private void configureHeader(Document doc, Element templatePIDElement) { + private void configureHeader(Document doc, Element templatePIDElement, byte[] defaultLogoBytes) { Element header = doc.createElement("header"); // Handle logo if configured String logoUrlPath = getInitializerService().getValueFromKey("report.patientIdSticker.logourl"); - if (isNotBlank(logoUrlPath)) { - configureLogo(doc, header, logoUrlPath); - } + configureLogo(doc, header, logoUrlPath, defaultLogoBytes); boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header")); if (useHeader) { @@ -238,22 +243,51 @@ private void configureHeader(Document doc, Element templatePIDElement) { templatePIDElement.appendChild(i18nStrings); } - private void configureLogo(Document doc, Element header, String logoUrlPath) { - String logoPath; - File logoFile = new File(logoUrlPath); - boolean isValidFile = logoFile.exists() && logoFile.canRead() && logoFile.isAbsolute(); + /** + * Configures the logo for the sticker document. + * + * Logo resolution priority: + * 1. Custom logo from absolute filesystem path resolved under {@code OPENMRS_APPLICATION_DATA_DIRECTORY} + * 2. Default OpenMRS logo as base64 data URI + * + * @param doc The XML document + * @param header The header element to append the logo to + * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) + * @throws RenderingException if no valid logo can be found + */ + private void configureLogo(Document doc, Element header, String logoUrlPath, byte[] defaultLogoBytes) { + String logoPath = ""; + + try { + // 1. Try custom logo + if (isNotBlank(logoUrlPath)) { + File logoFile = new File(logoUrlPath); + if (!logoFile.isAbsolute()) { + File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); + logoFile = new File(appDataDir, logoUrlPath); + } + if (logoFile.exists() && logoFile.canRead()) { + logoPath = logoFile.getAbsolutePath(); + } + } - if (isValidFile) { - logoPath = logoFile.getAbsolutePath(); - } else { - throw new RenderingException("Logo file not found or not accessible: " + logoUrlPath); + // 2. Fall back to default logo + if (isBlank(logoPath) && defaultLogoBytes != null && defaultLogoBytes.length > 0) { + String base64Image = Base64.getEncoder().encodeToString(defaultLogoBytes); + logoPath = "data:image/png;base64," + base64Image; + } + } catch (Exception e) { + throw new RenderingException("Failed to configure logo", e); } - Element branding = doc.createElement("branding"); - Element image = doc.createElement("logo"); - image.setTextContent(logoPath); - branding.appendChild(image); - header.appendChild(branding); + // Create and append logo elements if valid + if (isNotBlank(logoPath)) { + Element branding = doc.createElement("branding"); + Element image = doc.createElement("logo"); + image.setTextContent(logoPath); + branding.appendChild(image); + header.appendChild(branding); + } } private Map createConfigKeyMap() { @@ -365,15 +399,16 @@ && shouldIncludeColumn("patientdocuments.patientIdSticker.fields.secondaryIdenti // Process address if (shouldIncludeColumn("patientdocuments.patientIdSticker.fields.fulladdress")) { - Map addressData = (Map) patientData.get("preferredAddress"); - if (addressData != null) { + List> addressData = (List>) patientData.get("addresses"); + if (addressData != null && !addressData.isEmpty()) { + Map preferredAddress = addressData.get(0); StringBuilder address = new StringBuilder(); - appendIfNotNull(address, addressData.get("address1")); - appendIfNotNull(address, addressData.get("address2")); - appendIfNotNull(address, addressData.get("cityVillage")); - appendIfNotNull(address, addressData.get("stateProvince")); - appendIfNotNull(address, addressData.get("country")); - appendIfNotNull(address, addressData.get("postalCode")); + appendIfNotNull(address, preferredAddress.get("address1")); + appendIfNotNull(address, preferredAddress.get("address2")); + appendIfNotNull(address, preferredAddress.get("cityVillage")); + appendIfNotNull(address, preferredAddress.get("stateProvince")); + appendIfNotNull(address, preferredAddress.get("country")); + appendIfNotNull(address, preferredAddress.get("postalCode")); if (address.length() > 0) { addField(doc, fields, addressKey, address.toString().trim()); diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java index ece961f..fb3eb02 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java @@ -58,12 +58,12 @@ public class PatientIdStickerPdfReport { @Autowired private InitializerService initializerService; - public byte[] generatePdf(Patient patient) throws RuntimeException { + public byte[] generatePdf(Patient patient, byte[] defaultLogoBytes) throws RuntimeException { validatePatientAndPrivileges(patient); try { ReportData reportData = createReportData(patient); - byte[] xmlBytes = renderReportToXml(reportData); + byte[] xmlBytes = renderReportToXml(reportData, defaultLogoBytes); return transformXmlToPdf(xmlBytes); } catch (Exception e) { @@ -95,10 +95,10 @@ private ReportData createReportData(Patient patient) throws EvaluationException return reportData; } - private byte[] renderReportToXml(ReportData reportData) throws IOException { + private byte[] renderReportToXml(ReportData reportData, byte[] defaultLogoBytes) throws IOException { PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); try (ByteArrayOutputStream xmlOutputStream = new ByteArrayOutputStream()) { - renderer.render(reportData, null, xmlOutputStream); + renderer.render(reportData, null, xmlOutputStream, defaultLogoBytes); return xmlOutputStream.toByteArray(); } } diff --git a/api/src/main/resources/msfStickerFopStylesheet.xsl b/api/src/main/resources/msfStickerFopStylesheet.xsl index 180ec2d..c563c4c 100644 --- a/api/src/main/resources/msfStickerFopStylesheet.xsl +++ b/api/src/main/resources/msfStickerFopStylesheet.xsl @@ -80,7 +80,7 @@ + margin="3mm"> @@ -227,7 +227,7 @@ - + @@ -238,7 +238,7 @@ - + @@ -255,7 +255,7 @@ - + diff --git a/api/src/main/resources/patientIdStickerFopStylesheet.xsl b/api/src/main/resources/patientIdStickerFopStylesheet.xsl index e9b2e05..fe05894 100644 --- a/api/src/main/resources/patientIdStickerFopStylesheet.xsl +++ b/api/src/main/resources/patientIdStickerFopStylesheet.xsl @@ -7,7 +7,7 @@ - + @@ -80,7 +80,7 @@ + margin="3mm"> @@ -211,6 +211,17 @@ + + + + + + + + + + + @@ -235,17 +246,16 @@ - - - - - - - - - - + + + + + + + + diff --git a/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java b/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java index 236f7fd..79dd159 100644 --- a/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java +++ b/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java @@ -34,7 +34,7 @@ public void setup() throws Exception { public void generatePdf_shouldThrowWhenPatientIsMissing() throws Exception { Patient badPatient = null; Assertions.assertThrows(IllegalArgumentException.class, () -> { - pdfReport.generatePdf(badPatient); + pdfReport.generatePdf(badPatient, null); }); } diff --git a/omod/pom.xml b/omod/pom.xml index d555a1c..dc63196 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -17,6 +17,7 @@ 2.9 2.0.32 2.17.2 + 3.1.0 @@ -116,6 +117,12 @@ ${fasterxmlJacksonVersion} test + + javax.servlet + javax.servlet-api + ${javaxServletVersion} + test + diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index be719e0..62db228 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -12,8 +12,14 @@ import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.PATIENT_ID_STICKER_ID; import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID; +import java.io.IOException; +import java.io.InputStream; + +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.apache.commons.io.IOUtils; import org.openmrs.Patient; import org.openmrs.api.PatientService; import org.openmrs.module.patientdocuments.reports.PatientIdStickerPdfReport; @@ -37,7 +43,9 @@ public class PatientIdStickerDataPdfExportController extends BaseRestController { private static final Logger logger = LoggerFactory.getLogger(PatientIdStickerDataPdfExportController.class); - + + private static final String DEFAULT_LOGO_CLASSPATH = "/images/openmrs_logo_white_large.png"; + private PatientIdStickerPdfReport pdfReport; private PatientService ps; @@ -49,9 +57,10 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati this.pdfReport = pdfReport; } - private ResponseEntity writeResponse(Patient patient, boolean inline) { + private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { try { - byte[] pdfBytes = pdfReport.generatePdf(patient); + byte[] defaultLogoBytes = loadDefaultLogo(servletContext); + byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/pdf"); @@ -67,9 +76,27 @@ private ResponseEntity writeResponse(Patient patient, boolean inline) { .body("Error generating PDF".getBytes()); } } + + private byte[] loadDefaultLogo(ServletContext servletContext) { + if (servletContext == null) { + return null; + } + + try (InputStream logoStream = servletContext.getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) { + if (logoStream == null) { + logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH); + return null; + } + return IOUtils.toByteArray(logoStream); + } catch (IOException e) { + logger.error("Failed to load logo from: {}", DEFAULT_LOGO_CLASSPATH, e); + return null; + } + } @RequestMapping(method = RequestMethod.GET) public ResponseEntity getPatientIdSticker(HttpServletResponse response, + HttpServletRequest request, @RequestParam(value = "patientUuid") String patientUuid, @RequestParam(value = "inline", required = false, defaultValue = "true") boolean inline) { @@ -79,6 +106,7 @@ public ResponseEntity getPatientIdSticker(HttpServletResponse response, return null; } - return writeResponse(patient, inline); + ServletContext servletContext = request.getSession().getServletContext(); + return writeResponse(patient, inline, servletContext); } } diff --git a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java index 45d9b00..4318114 100644 --- a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; @@ -68,8 +69,9 @@ public void setup() throws Exception { public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Exception { Context.setLocale(Locale.ENGLISH); MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, request, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -92,8 +94,9 @@ public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Ex public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exception { Context.setLocale(new Locale("ar", "AR")); MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, request, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -114,9 +117,11 @@ public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exc @Test public void getPatientIdSticker_shouldReturn404ForInvalidPatient() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); + String invalidUuid = "invalid-uuid"; - ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, invalidUuid, false); + ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, request, invalidUuid, false); assertNull("Response entity should be null", responseEntity); assertEquals("Should return HTTP 404 status", HttpStatus.NOT_FOUND.value(), response.getStatus()); diff --git a/readme/PatientIdSticker.md b/readme/PatientIdSticker.md index 1831f21..5d43193 100644 --- a/readme/PatientIdSticker.md +++ b/readme/PatientIdSticker.md @@ -28,7 +28,7 @@ These flags control which patient information is displayed on each sticker. | Key | Type | Description | |---------------------------------------------------------------|---------|--------------------------------------------------| | `report.patientIdSticker.stylesheet` | String | XSL stylesheet to use for rendering stickers | -| `report.patientIdSticker.logourl` | String | URL of the logo image displayed on the sticker | +| `report.patientIdSticker.logourl` | String | Logo path or URL displayed on the sticker (supports a path relative to `OPENMRS_APPLICATION_DATA_DIRECTORY`) | | `report.patientIdSticker.header` | Boolean | Show a header section on each sticker | | `report.patientIdSticker.barcode` | Boolean | Show a barcode section on each sticker | | `report.patientIdSticker.pages` | Number | Number of sticker pages to generate | @@ -92,5 +92,8 @@ The module supports internationalization through message properties. Field label - The default font family is IBM Plex Sans Arabic for labels and IBM Plex Sans Arabic Bold for values. - The secondary identifier type can be configured using `report.patientIdSticker.fields.identifier.secondary.type` if needed. - Available stylesheets include `patientIdStickerFopStylesheet.xsl` (default) and `msfStickerFopStylesheet.xsl` for MSF-specific layouts. -- Logo URLs must be HTTP URLs starting with "http" to be processed. +- Logo resolution rules: + - If `report.patientIdSticker.logourl` is a relative path, it is resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` (e.g., `/openmrs/data/my_custom_logo.png`). + - If no logo is configured or the configured logo is unavailable, the default OpenMRS logo is loaded from the servlet context. + - Supported formats: an absolute filesystem path (for a custom logo) or a base64-encoded data URI (for the default OpenMRS logo), both of which are accepted by the renderer/XSL-FO processor. - The barcode is generated from the preferred patient identifier when barcode is enabled. \ No newline at end of file diff --git a/readme/PatientIdStickerXSL.md b/readme/PatientIdStickerXSL.md index c9cfcea..4d9173f 100644 --- a/readme/PatientIdStickerXSL.md +++ b/readme/PatientIdStickerXSL.md @@ -161,9 +161,12 @@ Key features: ### Header Section The optional header section can contain: -- An organizational logo on the left (loaded from HTTP URL) +- An organizational logo on the left (from a file path under `OPENMRS_APPLICATION_DATA_DIRECTORY`) - Custom header text on the right -- Automatic logo downloading and caching for HTTP URLs +- Logo handling behavior: + - Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` + - If none configured or missing, the default OpenMRS logo is loaded from the servlet context. + - Supported formats: absolute filesystem path (custom logo) or base64 data URI (default logo). The XSL-FO processor can handle both. ### Internationalization Section @@ -205,14 +208,13 @@ The stylesheet includes several responsive design elements: - **Demographic Grouping**: In MSF layout, groups Gender, DOB, and Age fields in a single row - **Secondary Identifier**: Special handling for secondary patient identifiers - **Internationalization**: Support for translated field labels and messages -- **Logo Handling**: Automatic download and caching of logos from HTTP URLs +- **Logo Handling**: Pulled from the `OPENMRS_APPLICATION_DATA_DIRECTORY` or servlet context for the default OpenMRS logo. ## Technical Requirements - **XSL-FO Processor**: Compatible with Apache FOP or similar XSL-FO processors - **Barcode4J Library**: Required for barcode generation - **Fonts**: Requires IBM Plex Sans Arabic and IBM Plex Sans Arabic Bold (or configured alternatives) -- **Network Access**: Required for logo downloading from HTTP URLs ## Examples @@ -276,6 +278,6 @@ The stylesheet includes several responsive design elements: - Configuration is managed through the Initializer module - Field visibility is controlled by boolean configuration properties - Secondary identifier type is specified by UUID in configuration -- Logo URLs must be HTTP URLs starting with "http" to be processed +- Logo input is either an absolute filesystem path or a base64-encoded data URI (e.g., `data:image/png;base64,...`). Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY`. - Barcode generation uses the preferred patient identifier - Multiple stickers can be generated based on the `pages` configuration