From ac0fa3d9f47d5926a4c696a8a358f409a189090e Mon Sep 17 00:00:00 2001 From: FikriMilano Date: Tue, 11 Jun 2024 21:49:08 +0700 Subject: [PATCH] QR to HTML Population (#3259) * Create HtmlPopulator class * Create some helper extensions for QR items * Allow specific date formatting on extension functions * Test HtmlPopulator class * spotlessApply * Add notes to CHANGELOG.md * Refactor HtmlPopulator - Only use 1 iteration (more efficient) - Use regex to find and match tags (easier to handle complex string patterns, and the code is more readable) - Use index for substitution (more efficient) - Use StringBuilder to avoid creating new String instances when doing substitution (using kotlin replace) - Keep using the while loop, as using forEach will skip a character following a replacement (See line 47 in HtmlPopulator class) * spotless * Add proper documentation --------- Co-authored-by: Martin Ndegwa --- CHANGELOG.md | 1 + .../fhircore/engine/pdf/HtmlPopulator.kt | 210 ++++++++ .../util/extension/DateTimeExtension.kt | 6 +- .../QuestionnaireResponseExtension.kt | 22 + .../util/extension/ResourceExtension.kt | 4 +- .../fhircore/engine/pdf/HtmlPopulatorTest.kt | 500 ++++++++++++++++++ 6 files changed, 738 insertions(+), 5 deletions(-) create mode 100644 android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt create mode 100644 android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e86d322f..458ba75bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added a new class (PdfGenerator) for generating PDF documents from HTML content using Android's WebView and PrintManager +- Introduced a new class (HtmlPopulator) to populate HTML templates with data from a Questionnaire Response ## [1.1.0] - 2024-02-15 diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt new file mode 100644 index 0000000000..28aaf581ed --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.pdf + +import java.util.regex.Matcher +import java.util.regex.Pattern +import org.hl7.fhir.r4.model.BaseDateTimeType +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.smartregister.fhircore.engine.util.extension.allItems +import org.smartregister.fhircore.engine.util.extension.formatDate +import org.smartregister.fhircore.engine.util.extension.makeItReadable +import org.smartregister.fhircore.engine.util.extension.valueToString + +/** + * HtmlPopulator class is responsible for processing an HTML template by replacing custom tags with + * data from a QuestionnaireResponse. The class uses various regex patterns to find and replace + * custom tags such as @is-not-empty, @answer-as-list, @answer, @submitted-date, and @contains. + * + * @property questionnaireResponse The QuestionnaireResponse object containing data for replacement. + */ +class HtmlPopulator( + private val questionnaireResponse: QuestionnaireResponse, +) { + + // Map to store questionnaire response items keyed by their linkId + private val questionnaireResponseItemMap = + questionnaireResponse.allItems.associateBy( + keySelector = { it.linkId }, + valueTransform = { it.answer }, + ) + + /** + * Populates the provided HTML template with data from the QuestionnaireResponse. + * + * After a tag got replaced, the current index will be used twice, adding an increment will skip a + * character right after the current index. + * + * @param rawHtml The raw HTML template containing custom tags to be replaced. + * @return The populated HTML with all custom tags replaced by corresponding data. + */ + fun populateHtml(rawHtml: String): String { + val html = StringBuilder(rawHtml) + var i = 0 + while (i < html.length) { + when { + html.startsWith("@is-not-empty", i) -> { + val matcher = isNotEmptyPattern.matcher(html.substring(i)) + if (matcher.find()) processIsNotEmpty(i, html, matcher) else i++ + } + html.startsWith("@answer-as-list", i) -> { + val matcher = answerAsListPattern.matcher(html.substring(i)) + if (matcher.find()) processAnswerAsList(i, html, matcher) else i++ + } + html.startsWith("@answer", i) -> { + val matcher = answerPattern.matcher(html.substring(i)) + if (matcher.find()) processAnswer(i, html, matcher) else i++ + } + html.startsWith("@submitted-date", i) -> { + val matcher = submittedDatePattern.matcher(html.substring(i)) + if (matcher.find()) processSubmittedDate(i, html, matcher) else i++ + } + html.startsWith("@contains", i) -> { + val matcher = containsPattern.matcher(html.substring(i)) + if (matcher.find()) processContains(i, html, matcher) else i++ + } + else -> i++ + } + } + return html.toString() + } + + /** + * Processes the @is-not-empty tag by checking if the specified linkId has an answer. Replaces the + * tag with the content if the answer exists, otherwise removes the tag. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processIsNotEmpty(i: Int, html: StringBuilder, matcher: Matcher) { + val linkId = matcher.group(1) + val content = matcher.group(2) ?: "" + val doesAnswerExist = questionnaireResponseItemMap.getOrDefault(linkId, listOf()).isNotEmpty() + if (doesAnswerExist) { + html.replace(i, matcher.end() + i, content) + // Start index is the index of '@' symbol, End index is the index after the ')' symbol. + // For example: @is-not-empty('link')Text@is-not-empty('link') + // The args we put the the replace function: The Start index is 0, the '@' symbol. The + // End index is the index after the ')' symbol. + // Note: The ones that are going to be replaced are from the Start index which is an '@' of + // the first tag, until the index before the End index which is an ')' of the second tag. + } else { + html.replace(i, matcher.end() + i, "") + } + } + + /** + * Processes the @answer-as-list tag by replacing it with a list of answers for the specified + * linkId. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processAnswerAsList(i: Int, html: StringBuilder, matcher: Matcher) { + val linkId = matcher.group(1) + val answerAsList = + questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString(separator = "") { + answer -> + "
  • ${answer.value.valueToString()}
  • " + } + html.replace(i, matcher.end() + i, answerAsList) + } + + /** + * Processes the @answer tag by replacing it with the answer for the specified linkId. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processAnswer(i: Int, html: StringBuilder, matcher: Matcher) { + val linkId = matcher.group(1) + val dateFormat = matcher.group(2) + val answer = + questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString { answer -> + if (dateFormat == null) { + answer.value.valueToString() + } else answer.value.valueToString(dateFormat) + } + html.replace(i, matcher.end() + i, answer) + } + + /** + * Processes the @submitted-date tag by replacing it with the formatted date. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processSubmittedDate(i: Int, html: StringBuilder, matcher: Matcher) { + val dateFormat = matcher.group(1) + val date = + if (dateFormat == null) { + questionnaireResponse.meta.lastUpdated.formatDate() + } else { + questionnaireResponse.meta.lastUpdated.formatDate(dateFormat) + } + html.replace(i, matcher.end() + i, date) + } + + /** + * Processes the @contains tag by checking if the specified linkId contains the indicator. + * Replaces the tag with the content if the indicator is found, otherwise removes the tag. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processContains(i: Int, html: StringBuilder, matcher: Matcher) { + val linkId = matcher.group(1) + val indicator = matcher.group(2) ?: "" + val content = matcher.group(3) ?: "" + val doesAnswerExist = + questionnaireResponseItemMap.getOrDefault(linkId, listOf()).any { + when { + it.hasValueCoding() -> it.valueCoding.code == indicator + it.hasValueStringType() -> it.valueStringType.value.contains(indicator) + it.hasValueIntegerType() -> it.valueIntegerType.value == indicator.toInt() + it.hasValueDecimalType() -> it.valueDecimalType.value == indicator.toBigDecimal() + it.hasValueBooleanType() -> it.valueBooleanType.value == indicator.toBoolean() + it.hasValueQuantity() -> + "${it.valueQuantity.value.toPlainString()} ${it.valueQuantity.unit}" == indicator + it.hasValueDateType() || it.hasValueDateTimeType() -> + (it.value as BaseDateTimeType).value.makeItReadable() == indicator + else -> false + } + } + if (doesAnswerExist) { + html.replace(i, matcher.end() + i, content) + } else { + html.replace(i, matcher.end() + i, "") + } + } + + companion object { + // Compile regex patterns for different tags + private val isNotEmptyPattern = + Pattern.compile("@is-not-empty\\('([^']+)'\\)((?s).*?)@is-not-empty\\('\\1'\\)") + private val answerAsListPattern = Pattern.compile("@answer-as-list\\('([^']+)'\\)") + private val answerPattern = Pattern.compile("@answer\\('([^']+)'(?:,'([^']+)')?\\)") + private val submittedDatePattern = Pattern.compile("@submitted-date(?:\\('([^']+)'\\))?") + private val containsPattern = + Pattern.compile("@contains\\('([^']+)','([^']+)'\\)((?s).*?)@contains\\('\\1'\\)") + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/DateTimeExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/DateTimeExtension.kt index ad5506e7c3..9b05fd749f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/DateTimeExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/DateTimeExtension.kt @@ -44,7 +44,7 @@ fun yesterday(): Date = DateTimeType.now().apply { add(Calendar.DATE, -1) }.valu fun today(): Date = DateTimeType.today().value -fun Date.formatDate(pattern: String): String = +fun Date.formatDate(pattern: String = "dd-MMM-yyyy"): String = SimpleDateFormat(pattern, Locale.ENGLISH).format(this) fun Date.isToday() = this.formatDate(SDF_YYYY_MM_DD) == today().formatDate(SDF_YYYY_MM_DD) @@ -55,11 +55,11 @@ fun SimpleDateFormat.tryParse(date: String): Date? = .runCatching { SimpleDateFormat(this.toPattern(), Locale.ENGLISH).parse(date) } .getOrNull() -fun Date?.makeItReadable(): String { +fun Date?.makeItReadable(pattern: String = "dd-MMM-yyyy"): String { return if (this == null) { "N/A" } else { - SimpleDateFormat("dd-MMM-yyyy", Locale.getDefault()).run { format(this@makeItReadable) } + SimpleDateFormat(pattern, Locale.getDefault()).run { format(this@makeItReadable) } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireResponseExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireResponseExtension.kt index 7f606a66e6..7e4acaceec 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireResponseExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireResponseExtension.kt @@ -33,3 +33,25 @@ private fun List.clearText() { } } } + +/** Pre-order list of all questionnaire response items in the questionnaire. */ +val QuestionnaireResponse.allItems: List + get() = item.flatMap { it.descendant } + +/** + * Pre-order list of descendants of the questionnaire response item (inclusive of the current item). + */ +val QuestionnaireResponse.QuestionnaireResponseItemComponent.descendant: + List + get() = + mutableListOf().also { + appendDescendantTo(it) + } + +private fun QuestionnaireResponse.QuestionnaireResponseItemComponent.appendDescendantTo( + output: MutableList, +) { + output.add(this) + item.forEach { it.appendDescendantTo(output) } + answer.forEach { answer -> answer.item.forEach { it.appendDescendantTo(output) } } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt index 6ddcb2752e..b4414a662c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt @@ -75,10 +75,10 @@ const val REFERENCE = "reference" const val PARTOF = "part-of" private val fhirR4JsonParser = FhirContext.forR4Cached().getCustomJsonParser() -fun Base?.valueToString(): String { +fun Base?.valueToString(datePattern: String = "dd-MMM-yyyy"): String { return when { this == null -> return "" - this.isDateTime -> (this as BaseDateTimeType).value.makeItReadable() + this.isDateTime -> (this as BaseDateTimeType).value.makeItReadable(datePattern) this.isPrimitive -> (this as PrimitiveType<*>).asStringValue() this is Coding -> display ?: code this is CodeableConcept -> this.stringValue() diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt new file mode 100644 index 0000000000..2a348b4376 --- /dev/null +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt @@ -0,0 +1,500 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.pdf + +import java.util.Calendar +import java.util.Date +import org.hl7.fhir.r4.model.BooleanType +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.DateTimeType +import org.hl7.fhir.r4.model.DecimalType +import org.hl7.fhir.r4.model.IntegerType +import org.hl7.fhir.r4.model.Meta +import org.hl7.fhir.r4.model.Quantity +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent +import org.hl7.fhir.r4.model.StringType +import org.junit.Assert +import org.junit.Test + +class HtmlPopulatorTest { + + @Test + fun testIsNotEmptyShouldShowContentWhenAnswerExistInQR() { + val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

    Text

    ", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldHideContentWhenAnswerIsEmptyInQR() { + val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = emptyList() + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldHideContentWhenAnswerNotExistInQR() { + val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldHideContentWhenLinkIdNotExistInQR() { + val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = QuestionnaireResponse() + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldShowMalformedTagAndContentIfLinkIdOfBothTagDoesNotMatch() { + val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-b')" + val questionnaireResponse = QuestionnaireResponse() + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("@is-not-empty('link-a')

    Text

    @is-not-empty('link-b')", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldShowMalformedTagAndContentIfOnly1TagExist() { + val html = "@is-not-empty('link-a')

    Text

    " + val questionnaireResponse = QuestionnaireResponse() + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("@is-not-empty('link-a')

    Text

    ", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldShowContentAndNestedMalformedTagIfAnswerOfRootTagExist() { + val html = "@is-not-empty('link-a')@is-not-empty('link-b')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("@is-not-empty('link-b')

    Text

    ", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldHideContentAndNestedMalformedTagIfAnswerOfRootTagIsNotExist() { + val html = "@is-not-empty('link-a')@is-not-empty('link-b')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldHideContentAndNestedMalformedTagIfAnswerOfRootTagIsEmpty() { + val html = "@is-not-empty('link-a')@is-not-empty('link-b')

    Text

    @is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = emptyList() + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testIsNotEmptyShouldShowEmptyContentIfAnswerExist() { + val html = "@is-not-empty('link-a')@is-not-empty('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testProcessAnswerAsListShouldShowAnswerAsListWhenAnswerExistInQR() { + val html = "
      @answer-as-list('link-a')
    " + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 2", "code 2", "display 2") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("
    • display 1
    • display 2
    ", populatedHtml) + } + + @Test + fun testProcessAnswerAsListShouldShowEmptyAnswerAsListWhenAnswerNotExistInQR() { + val html = "
      @answer-as-list('link-a')
    " + val questionnaireResponse = + QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("
      ", populatedHtml) + } + + @Test + fun testProcessAnswerAsListShouldShowEmptyAnswerAsListWhenLinkIdNotExistInQR() { + val html = "
        @answer-as-list('link-a')
      " + val questionnaireResponse = QuestionnaireResponse() + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("
        ", populatedHtml) + } + + @Test + fun testProcessAnswerShouldShowAnswerWhenAnswerExistInQR() { + val html = "

        @answer('link-a')

        " + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = StringType("string 1") }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        string 1

        ", populatedHtml) + } + + @Test + fun testProcessAnswerShouldShowEmptyAnswerWhenAnswerNotExistInQR() { + val html = "

        @answer('link-a')

        " + val questionnaireResponse = + QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        ", populatedHtml) + } + + @Test + fun testProcessAnswerShouldShowEmptyAnswerWhenLinkIdNotExistInQR() { + val html = "

        @answer('link-a')

        " + val questionnaireResponse = QuestionnaireResponse() + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        ", populatedHtml) + } + + @Test + fun testProcessAnswerShouldShowDateAnswerWhenAnswerOfTypeDateExistInQR() { + val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } + val specificDate: Date = calendar.time + val html = "

        @answer('link-a')

        " + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(specificDate) + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        14-May-2024

        ", populatedHtml) + } + + @Test + fun testProcessAnswerShouldShowDateAnswerWithFormatWhenDateFormatExistInTheTag() { + val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } + val specificDate: Date = calendar.time + val html = "

        @answer('link-a','MMMM d, yyyy')

        " + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(specificDate) + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        May 14, 2024

        ", populatedHtml) + } + + @Test + fun testProcessSubmittedDateShouldShow() { + val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } + val specificDate: Date = calendar.time + val html = "

        @submitted-date

        " + val questionnaireResponse = + QuestionnaireResponse().apply { meta = Meta().apply { lastUpdated = specificDate } } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        14-May-2024

        ", populatedHtml) + } + + @Test + fun testProcessSubmittedDateShouldShowWithFormatWhenDateFormatExistInTheTag() { + val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } + val specificDate: Date = calendar.time + val html = "

        @submitted-date('MMMM d, yyyy')

        " + val questionnaireResponse = + QuestionnaireResponse().apply { meta = Meta().apply { lastUpdated = specificDate } } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        May 14, 2024

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorCodeMatchesWithAnswerOfTypeCoding() { + val html = "@contains('link-a','code 2')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 2", "code 2", "display 2") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldHideContentWhenIndicatorCodeDoesNotMatchWithAnswerOfTypeCoding() { + val html = "@contains('link-a','code 3')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 2", "code 2", "display 2") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorStringIsContainedInAnswerOfTypeString() { + val html = "@contains('link-a','basket')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = StringType("basketball") }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorIntegerMatchesAnswerOfTypeInteger() { + val html = "@contains('link-a','10')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = IntegerType("10") }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorDecimalMatchesAnswerOfTypeDecimal() { + val html = "@contains('link-a','1.5')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = DecimalType("1.5") }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorBooleanMatchesAnswerOfTypeBoolean() { + val html = "@contains('link-a','true')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = BooleanType("true") }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorQuantityMatchesAnswerOfTypeQuantity() { + val html = "@contains('link-a','3 years')

        Text

        @contains('link-a')" + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Quantity(null, 3, "system", "years", "years") + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessContainsShouldShowContentWhenIndicatorDateMatchesAnswerOfTypeDate() { + val html = "@contains('link-a','14-May-2024')

        Text

        @contains('link-a')" + val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } + val specificDate: Date = calendar.time + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(specificDate) + }, + ) + } + } + } + val htmlPopulator = HtmlPopulator(questionnaireResponse) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } +}