Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ It bundles two groups of reports:
## List of Embedded Reports:
### General use reports (always activated)
* [Patient Identifier Sticker](readme/PatientIdSticker.md) - an easy-to-read report for generating printable patient ID stickers, rendered as a PDF using the [Patient ID Sticker XSLT Template](readme/PatientIdStickerXSL.md)
* [Remote Logo URL Support](readme/RemoteLogoUrl.md) - documentation for fetching logos from HTTP/HTTPS URLs with caching and validation
8 changes: 8 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
</properties>

<dependencies>
<!-- Mockito inline is required for static mocking support in tests -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.openmrs.module</groupId>
<artifactId>calculation-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.openmrs.messagesource.MessageSourceService;
import org.openmrs.module.initializer.api.InitializerService;
import org.openmrs.module.patientdocuments.common.PatientDocumentsConstants;
import org.openmrs.module.patientdocuments.service.RemoteLogoService;
import org.openmrs.module.reporting.common.Localized;
import org.openmrs.module.reporting.dataset.DataSet;
import org.openmrs.module.reporting.dataset.DataSetColumn;
Expand Down Expand Up @@ -78,6 +79,8 @@ public class PatientIdStickerXmlReportRenderer extends ReportDesignRenderer {

private InitializerService initializerService;

private RemoteLogoService remoteLogoService;

private MessageSourceService getMessageSourceService() {

if (mss == null) {
Expand All @@ -96,6 +99,15 @@ private InitializerService getInitializerService() {
return initializerService;
}

private RemoteLogoService getRemoteLogoService() {

if (remoteLogoService == null) {
remoteLogoService = Context.getRegisteredComponent("remoteLogoService", RemoteLogoService.class);
}

return remoteLogoService;
}

/**
* @see ReportRenderer#getFilename(org.openmrs.module.reporting.report.ReportRequest)
*/
Expand Down Expand Up @@ -253,37 +265,60 @@ private void configureHeader(Document doc, Element templatePIDElement) {
/**
* Configures the logo for the sticker document.
*
* Loads a custom logo from {@code logoUrlPath} (relative to the {@code OPENMRS_APPLICATION_DATA_DIRECTORY}.
* If not found, falls back to the OpenMRS logo from the classpath.
* Supports both remote HTTP/HTTPS URLs and local file paths.
* For remote URLs: Downloads, validates, and caches the logo with fallback support.
* For local paths: Loads from {@code OPENMRS_APPLICATION_DATA_DIRECTORY} (relative paths only).
* If neither is available, falls back to the default OpenMRS logo from the classpath.
*
* @param doc The XML document
* @param header The header element to append the logo to
* @param logoUrlPath User-configured logo path (must be relative to app data dir)
* @param logoUrlPath User-configured logo URL or path
*/
private void configureLogo(Document doc, Element header, String logoUrlPath) {
String logoContent = null;

// 1. Try custom logo
if (isNotBlank(logoUrlPath)) {
// 1. Try remote HTTP/HTTPS URL
if (isNotBlank(logoUrlPath) && isRemoteUrl(logoUrlPath)) {
File remoteLogo = getRemoteLogoService().fetchRemoteLogo(logoUrlPath);
if (remoteLogo != null && remoteLogo.exists() && remoteLogo.canRead() && remoteLogo.isFile()) {
try {
byte[] remoteLogoBytes = OpenmrsUtil.getFileAsBytes(remoteLogo);
if (remoteLogoBytes != null && remoteLogoBytes.length > 0) {
String base64Image = Base64.getEncoder().encodeToString(remoteLogoBytes);
logoContent = "data:image/png;base64," + base64Image;
log.info("Successfully loaded remote logo from URL: {}", logoUrlPath);
}
} catch (IOException e) {
log.error("Failed to read remote logo file: {}", remoteLogo.getAbsolutePath(), e);
}
} else {
log.warn("Failed to fetch remote logo from URL: {}", logoUrlPath);
}
}
// 2. Try local file path (relative to app data directory)
else if (isNotBlank(logoUrlPath)) {
File logoFile = resolveSecureLogoPath(logoUrlPath);
if (logoFile != null && logoFile.exists() && logoFile.canRead() && logoFile.isFile()) {
try {
byte[] customLogoBytes = OpenmrsUtil.getFileAsBytes(logoFile);
if (customLogoBytes != null && customLogoBytes.length > 0) {
String base64Image = Base64.getEncoder().encodeToString(customLogoBytes);
logoContent = "data:image/png;base64," + base64Image;
log.info("Successfully loaded local logo from path: {}", logoUrlPath);
}
} catch (IOException e) {
log.error("Failed to load custom logo from file: {}", logoFile.getAbsolutePath(), e);
}
}
}

// 3. Fallback to default logo from classpath
if (isBlank(logoContent)) {
byte[] defaultLogoBytes = loadDefaultLogoFromClasspath();
if (defaultLogoBytes != null && defaultLogoBytes.length > 0) {
String base64Image = Base64.getEncoder().encodeToString(defaultLogoBytes);
logoContent = "data:image/png;base64," + base64Image;
log.debug("Using default logo from classpath");
}
}

Expand All @@ -299,6 +334,17 @@ else if (isNotBlank(logoUrlPath)) {
log.error("Failed to configure logo: unresolved path '{}' and no default provided", logoUrlPath);
}
}

/**
* Checks if the given string is a remote HTTP/HTTPS URL.
*/
private boolean isRemoteUrl(String urlOrPath) {
if (isBlank(urlOrPath)) {
return false;
}
String lower = urlOrPath.trim().toLowerCase();
return lower.startsWith("http://") || lower.startsWith("https://");
}

private byte[] loadDefaultLogoFromClasspath() {
try (InputStream logoStream = OpenmrsClassLoader.getInstance().getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) {
Expand Down
Loading