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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.patientdocuments.common;

import java.text.SimpleDateFormat;
import java.util.Date;

public final class DateUtil {

public static final String DATE_PATTERN = "yyyy-MM-dd";

public static final String TIME_PATTERN = "HH:mm";

public static final String DATE_TIME_PATTERN = DATE_PATTERN + " " + TIME_PATTERN;

private DateUtil() {
}

public static String formatDate(Date date) {
return date == null ? null : new SimpleDateFormat(DATE_PATTERN).format(date);
}

public static String formatTime(Date date) {
return date == null ? null : new SimpleDateFormat(TIME_PATTERN).format(date);
}

public static String formatDateTime(Date date) {
return date == null ? null : new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.FopFactoryBuilder;
import org.apache.fop.apps.MimeConstants;
import org.apache.fop.configuration.Configuration;
import org.apache.fop.configuration.DefaultConfigurationBuilder;
import org.openmrs.Encounter;
import org.openmrs.annotation.Handler;
import org.openmrs.api.EncounterService;
Expand All @@ -26,6 +29,7 @@
import org.openmrs.module.reporting.report.ReportRequest;
import org.openmrs.module.reporting.report.renderer.RenderingException;
import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer;
import org.openmrs.util.OpenmrsClassLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
Expand All @@ -43,7 +47,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -55,6 +59,10 @@ public class EncounterPdfReportRenderer extends ReportDesignRenderer {

private static final Logger log = LoggerFactory.getLogger(EncounterPdfReportRenderer.class);

private static final String FOP_CONFIG_PATH = "conf/fop.xconf.xml";

private static final String FONT_BASE_PATH = "fonts/";

@Override
public String getFilename(ReportRequest request) {
return "EncountersReport.pdf";
Expand Down Expand Up @@ -101,8 +109,8 @@ private List<Encounter> collectEncounters(String encounterUuids) {
return encounters;
}

private void transformXmlToPdf(String xmlData, OutputStream outStream) throws Exception {
FopFactory fopFactory = FopFactory.newInstance(new URI("."));
void transformXmlToPdf(String xmlData, OutputStream outStream) throws Exception {
FopFactory fopFactory = buildFopFactory();
FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, outStream);

Expand All @@ -123,6 +131,20 @@ private void transformXmlToPdf(String xmlData, OutputStream outStream) throws Ex
}
}

private FopFactory buildFopFactory() throws Exception {
URL fontBaseUrl = OpenmrsClassLoader.getInstance().getResource(FONT_BASE_PATH);
if (fontBaseUrl == null) {
throw new IllegalStateException("Bundled font directory not found on classpath: " + FONT_BASE_PATH);
}
try (InputStream fopConfigStream = Helper.getInputStreamByResource(FOP_CONFIG_PATH)) {
if (fopConfigStream == null) {
throw new IllegalStateException("Bundled FOP configuration not found on classpath: " + FOP_CONFIG_PATH);
}
Configuration cfg = new DefaultConfigurationBuilder().build(fopConfigStream);
return new FopFactoryBuilder(fontBaseUrl.toURI()).setConfiguration(cfg).build();
}
}

InputStream getStylesheetStream() throws IOException {
return getStylesheetStream(getConfiguredStylesheetPath());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.openmrs.Concept;
import org.openmrs.ConceptDatatype;
import org.openmrs.ConceptName;
import org.openmrs.Encounter;
import org.openmrs.Obs;
Expand All @@ -26,6 +27,7 @@
import org.openmrs.messagesource.MessageSourceService;
import org.openmrs.module.initializer.api.InitializerService;
import org.openmrs.module.o3forms.api.O3FormsService;
import org.openmrs.module.patientdocuments.common.DateUtil;
import org.openmrs.module.patientdocuments.common.Helper;
import org.openmrs.module.patientdocuments.common.PatientDocumentsConstants;
import org.openmrs.module.webservices.rest.SimpleObject;
Expand All @@ -41,9 +43,11 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;


Expand All @@ -53,14 +57,29 @@

private static final String QUESTIONS_SECTION = "questions";

private static final String SECTIONS_SECTION = "sections";

private static final String ANSWERS_FIELD = "answers";

private static final String CONCEPT_FIELD = "concept";

private static final String LABEL_FIELD = "label";

private static final String ID_FIELD = "id";

/**
* Prefix the O3 React form engine writes into Obs.formFieldPath, e.g. a field with id = skinColor_1 produces
* the path rfe-forms-skinColor_1. We use this to map each rendered question occurrence to the specific observation
* it captured, so questions that reuse the same concept across repeated sections each show their own single value.
*/
private static final String FORM_FIELD_PATH_PREFIX = "rfe-forms-";

private InitializerService initializerService;

private final Set<String> allFieldIds = new HashSet<>();

private final Map<String, Integer> conceptRefUsage = new HashMap<>();

private static final Logger log = LoggerFactory.getLogger(EncounterXmlBuilder.class);

private InitializerService getInitializerService() {
Expand Down Expand Up @@ -115,15 +134,15 @@
StringBuilder xml = new StringBuilder();
xml.append("<encounter>");

xml.append(buildHeaderContent(encounter, locale));
xml.append(buildHeaderContent(encounter));
xml.append(buildMainContent(encounter, locale, o3FormsService));
xml.append(buildFooterContent(user, locale));
xml.append(buildFooterContent(user));

xml.append("</encounter>");
return xml.toString();
}

private String buildHeaderContent(Encounter encounter, Locale locale) {
private String buildHeaderContent(Encounter encounter) {

Check failure on line 145 in api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterXmlBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-module-patientdocuments&issues=AZ9wFkR6bECyrV8M0NGg&open=AZ9wFkR6bECyrV8M0NGg&pullRequest=28
StringBuilder xml = new StringBuilder();
Visit visit = encounter.getVisit();
Patient patient = encounter.getPatient();
Expand Down Expand Up @@ -153,13 +172,13 @@

if (isHeaderFieldEnabled("encounterDate")) {
xml.append("<encounterDate>")
.append(escape(OpenmrsUtil.getDateFormat(locale).format(encounter.getEncounterDatetime())))
.append(escape(DateUtil.formatDate(encounter.getEncounterDatetime())))
.append("</encounterDate>");
}

if (isHeaderFieldEnabled("visitStartDate")) {
String visitStartDate = (visit != null && visit.getStartDatetime() != null)
? OpenmrsUtil.getDateFormat(locale).format(visit.getStartDatetime())
String visitStartDate = (visit != null && visit.getStartDatetime() != null)
? DateUtil.formatDate(visit.getStartDatetime())
: PatientDocumentsConstants.MISSING_VALUE_PLACEHOLDER;
xml.append("<visitStartDate>")
.append(escape(visitStartDate))
Expand All @@ -168,7 +187,7 @@

if (isHeaderFieldEnabled("visitEndDate")) {
String visitEndDate = (visit != null && visit.getStopDatetime() != null)
? OpenmrsUtil.getDateFormat(locale).format(visit.getStopDatetime())
? DateUtil.formatDate(visit.getStopDatetime())
: PatientDocumentsConstants.MISSING_VALUE_PLACEHOLDER;
xml.append("<visitEndDate>")
.append(escape(visitEndDate))
Expand Down Expand Up @@ -199,16 +218,12 @@
return xml.toString();
}

private String buildFooterContent(User user, Locale locale) {
private String buildFooterContent(User user) {
StringBuilder xml = new StringBuilder();

String userName = (user != null && user.getPersonName() != null) ? user.getPersonName().getFullName() : "System";
String systemId = (user != null && user.getSystemId() != null) ? user.getSystemId() : "Unknown";
// JDK 20+ locale-aware date formats use U+202F (narrow no-break space) before AM/PM.
// The font embedded by Apache FOP lacks that glyph and renders it as "#" in the PDF,
// so we normalize it (and U+00A0) to a regular ASCII space.
String printTimestamp = OpenmrsUtil.getDateTimeFormat(locale).format(new Date())
.replace('\u202F', ' ').replace('\u00A0', ' ');
String printTimestamp = DateUtil.formatDateTime(new Date());

String printedBy = String.format("Printed by %s (%s) at %s", userName, systemId, printTimestamp);
xml.append("<printedBy>").append(escape(printedBy)).append("</printedBy>");
Expand Down Expand Up @@ -237,6 +252,7 @@

xml.append("<pages>");
List<Map<String, Object>> pages = schema.get("pages");
collectFieldIds(pages);
if (pages != null) {
for (Map<String, Object> page : pages) {
xml.append(renderPage(page, obsMap, locale));
Expand Down Expand Up @@ -268,7 +284,7 @@

xml.append("<page label=\"").append(escape(localizedLabel)).append("\">");

List<Map<String, Object>> sections = (List<Map<String, Object>>) page.get("sections");
List<Map<String, Object>> sections = (List<Map<String, Object>>) page.get(SECTIONS_SECTION);
if (sections != null) {
for (Map<String, Object> section : sections) {
xml.append(renderSection(section, obsMap, locale));
Expand Down Expand Up @@ -402,7 +418,12 @@
return PatientDocumentsConstants.NO_DATA_RECORDED_PLACEHOLDER;
}

List<Obs> observations = obsMap.get(concept.getUuid());
List<Obs> conceptObs = obsMap.get(concept.getUuid());
if (conceptObs.isEmpty()) {
return PatientDocumentsConstants.NO_DATA_RECORDED_PLACEHOLDER;
}

List<Obs> observations = selectObsForQuestion(question, conceptObs);
if (observations.isEmpty()) {
return PatientDocumentsConstants.NO_DATA_RECORDED_PLACEHOLDER;
}
Expand All @@ -412,6 +433,99 @@
.collect(Collectors.joining(", "));
}

List<Obs> selectObsForQuestion(Map<String, Object> question, List<Obs> conceptObs) {
String fieldId = (String) question.get(ID_FIELD);
if (StringUtils.isBlank(fieldId)) {
return conceptObs;
}

boolean anyFormFieldPath = conceptObs.stream()
.anyMatch(obs -> StringUtils.isNotBlank(obs.getFormFieldPath()));
if (!anyFormFieldPath) {
return conceptObs;
}

List<Obs> matched = new ArrayList<>();
for (Obs obs : conceptObs) {
if (matchesField(obs, fieldId)) {
matched.add(obs);
}
}

if (!matched.isEmpty()) {
return matched;
}

return isConceptSharedByMultipleQuestions(question) ? Collections.emptyList() : conceptObs;
}

private boolean isConceptSharedByMultipleQuestions(Map<String, Object> question) {
String conceptRef = extractConceptRef(question);
return conceptRef != null && conceptRefUsage.getOrDefault(conceptRef, 0) > 1;
}

private boolean matchesField(Obs obs, String fieldId) {
String formFieldPath = obs.getFormFieldPath();
if (StringUtils.isBlank(formFieldPath) || !formFieldPath.startsWith(FORM_FIELD_PATH_PREFIX)) {
return false;
}
String core = formFieldPath.substring(FORM_FIELD_PATH_PREFIX.length());
if (core.equals(fieldId)) {
return true;
}

if (core.startsWith(fieldId + "_") && !allFieldIds.contains(core)) {
return isAllDigits(core.substring(fieldId.length() + 1));
}
return false;
}

private boolean isAllDigits(String value) {
if (value.isEmpty()) {
return false;
}
for (int i = 0; i < value.length(); i++) {
if (!Character.isDigit(value.charAt(i))) {
return false;
}
}
return true;
}

void collectFieldIds(List<Map<String, Object>> pages) {
allFieldIds.clear();
conceptRefUsage.clear();
if (pages == null) {
return;
}
for (Map<String, Object> page : pages) {
List<Map<String, Object>> sections = (List<Map<String, Object>>) page.get(SECTIONS_SECTION);
if (sections == null) {
continue;
}
for (Map<String, Object> section : sections) {
collectQuestionIds((List<Map<String, Object>>) section.get(QUESTIONS_SECTION));
}
}
}

private void collectQuestionIds(List<Map<String, Object>> questions) {
if (questions == null) {
return;
}
for (Map<String, Object> question : questions) {
Object id = question.get(ID_FIELD);
if (id instanceof String) {
allFieldIds.add((String) id);
}
String conceptRef = extractConceptRef(question);
if (conceptRef != null) {
conceptRefUsage.merge(conceptRef, 1, Integer::sum);
}
collectQuestionIds((List<Map<String, Object>>) question.get(QUESTIONS_SECTION));
}
}

private String escape(String input) {
return StringEscapeUtils.escapeXml10(StringUtils.defaultString(input));
}
Expand All @@ -430,9 +544,34 @@
return obs.getValueCoded().getDisplayString();
}
}

String formattedDate = formatDateObsValue(obs);
if (formattedDate != null) {
return formattedDate;
}

return obs.getValueAsString(locale);
}

String formatDateObsValue(Obs obs) {
Date value = obs.getValueDatetime();
if (value == null || obs.getConcept() == null || obs.getConcept().getDatatype() == null) {
return null;
}

ConceptDatatype datatype = obs.getConcept().getDatatype();
if (datatype.isDate()) {
return DateUtil.formatDate(value);
}
if (datatype.isTime()) {
return DateUtil.formatTime(value);
}
if (datatype.isDateTime()) {
return DateUtil.formatDateTime(value);
}
return null;
}

private String findAnswerLabel(Map<String, Object> question, String obsConceptUuid, Locale locale) {
for (Map<String, Object> answer : extractAnswers(question)) {
String answerConceptRef = (String) answer.get(CONCEPT_FIELD);
Expand Down
Loading