From 41b777fd7bbae577e46fd5ed03cdb04b2b2f7986 Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Mon, 10 Aug 2026 18:51:11 +0530 Subject: [PATCH 1/8] Add address hierarchy module to aware_of_modules in config.xml --- omod/src/main/resources/config.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index e31780a..8f7af1f 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -38,6 +38,7 @@ org.openmrs.module.emrapi org.openmrs.module.metadatamapping org.openmrs.module.metadatasharing + org.openmrs.module.addresshierarchy From 72f1828de88392dd2ea67b571dba7bb66cf99294 Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Mon, 10 Aug 2026 18:51:17 +0530 Subject: [PATCH 2/8] Add address hierarchy API dependency to pom.xml --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index b91b7a9..37c9cd7 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ 3.4.0 1.6.0 1.2.2 + 2.17.0 @@ -137,5 +138,12 @@ ${metadatasharingVersion} test + + + org.openmrs.module + addresshierarchy-api + ${addresshierarchyVersion} + provided + From 2b052da65fbb91c6c22bd1b547af6bf17d589e5a Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Mon, 10 Aug 2026 18:51:25 +0530 Subject: [PATCH 3/8] Add AddressHierarchyDomainExporter and corresponding tests for address hierarchy export functionality --- .../AddressHierarchyDomainExporter.java | 222 ++++++++++++++++++ .../AddressHierarchyDomainExporterTest.java | 107 +++++++++ 2 files changed, 329 insertions(+) create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java new file mode 100644 index 0000000..449cc23 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java @@ -0,0 +1,222 @@ +/* + * 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.metadataexport.domain.addresshierarchy; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.openmrs.OpenmrsObject; +import org.openmrs.annotation.OpenmrsProfile; +import org.openmrs.api.context.Context; +import org.openmrs.layout.address.AddressSupport; +import org.openmrs.layout.address.AddressTemplate; +import org.openmrs.module.addresshierarchy.AddressField; +import org.openmrs.module.addresshierarchy.AddressHierarchyEntry; +import org.openmrs.module.addresshierarchy.AddressHierarchyLevel; +import org.openmrs.module.addresshierarchy.config.AddressComponent; +import org.openmrs.module.addresshierarchy.config.AddressConfiguration; +import org.openmrs.module.addresshierarchy.config.AddressConfigurationLoader; +import org.openmrs.module.addresshierarchy.config.AddressHierarchyFile; +import org.openmrs.module.addresshierarchy.service.AddressHierarchyService; +import org.openmrs.module.initializer.Domain; +import org.openmrs.module.metadataexport.export.DomainExporter; +import org.openmrs.module.metadataexport.export.ExportContext; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Exports the address hierarchy, which Initializer does not load through its own line-processor + * framework: {@code AddressHierarchyLoader} hands the whole directory to the addresshierarchy + * module's own {@code AddressConfigurationLoader}. So this is a directory-of-files domain, not a + * row-per-object CSV, and it implements {@link DomainExporter} directly (like the metadatasharing + * exporter) rather than extending the CSV/XML line framework. + *

+ * It reproduces the two files a fresh import needs: + *

    + *
  • {@code addressConfiguration.xml} — rebuilt from the ordered hierarchy levels and the + * live address template global property, then serialized with the module's own + * {@link AddressConfigurationLoader#writeToString(AddressConfiguration)} so the XML matches the + * import format. Each ordered level maps 1:1 to an {@code addressComponent}: the level's name is + * the component's {@code nameMapping} and the level's {@code required} flag is + * {@code requiredInHierarchy}; only {@code sizeMapping}/{@code elementDefault} and the + * {@code lineByLineFormat} come from the address template.
  • + *
  • {@code addresshierarchy.csv} — one headerless row per leaf entry, the root-to-leaf + * entry names joined by the entry delimiter, each cell optionally carrying + * {@code name^userGeneratedId}.
  • + *
+ * The entry/identifier delimiters are not persisted anywhere (they are used only at import time), + * so canonical defaults are emitted and the CSV is written to match them. + */ +@Slf4j +@Component +@OpenmrsProfile(modules = { "addresshierarchy:2.17.0" }) +public class AddressHierarchyDomainExporter implements DomainExporter { + + public static final String CONFIG_FILE_NAME = "addressConfiguration.xml"; + + public static final String ENTRIES_FILE_NAME = "addresshierarchy.csv"; + + public static final String ENTRY_DELIMITER = ","; + + public static final String IDENTIFIER_DELIMITER = "^"; + + private static final int DEFAULT_SIZE_MAPPING = 40; + + @Override + public Domain getDomain() { + return Domain.ADDRESS_HIERARCHY; + } + + @Override + public boolean handles(OpenmrsObject instance) { + return instance instanceof AddressHierarchyEntry; + } + + @Override + public Collection getAllInstances() { + AddressHierarchyService service = Context.getService(AddressHierarchyService.class); + List all = new ArrayList<>(); + for (AddressHierarchyLevel level : service.getOrderedAddressHierarchyLevels()) { + all.addAll(service.getAddressHierarchyEntriesByLevel(level)); + } + return all; + } + + @Override + public Collection getDependencies(AddressHierarchyEntry instance) { + return Collections.emptyList(); + } + + @Override + public void export(Collection instances, ExportContext context) throws IOException { + List levels = Context.getService(AddressHierarchyService.class) + .getOrderedAddressHierarchyLevels(); + if (levels.isEmpty()) { + log.warn("Address Hierarchy: no hierarchy levels are configured, nothing to export"); + return; + } + + File domainDir = new File(new File(context.getOutputDir(), "configuration"), getDomain().getName()); + domainDir.mkdirs(); + + AddressTemplate template = AddressSupport.getInstance().getDefaultLayoutTemplate(); + String configXml = buildAddressConfigurationXml(levels, template); + Files.write(new File(domainDir, CONFIG_FILE_NAME).toPath(), configXml.getBytes(StandardCharsets.UTF_8)); + + String entriesCsv = buildEntriesCsv(instances); + Files.write(new File(domainDir, ENTRIES_FILE_NAME).toPath(), entriesCsv.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Rebuilds the {@code addressConfiguration.xml} content from the ordered hierarchy levels and the + * live address template, delegating the actual serialization to the addresshierarchy module so the + * output stays in lockstep with what it parses on import. + */ + String buildAddressConfigurationXml(List levels, AddressTemplate template) { + Map sizeMappings = template == null ? Collections.emptyMap() : template.getSizeMappings(); + Map elementDefaults = template == null ? Collections.emptyMap() : template.getElementDefaults(); + + AddressConfiguration configuration = new AddressConfiguration(); + for (AddressHierarchyLevel level : levels) { + AddressField field = level.getAddressField(); + String token = field == null ? null : field.getName(); + + AddressComponent component = new AddressComponent(); + component.setField(field); + component.setNameMapping(level.getName()); + component.setSizeMapping(parseSize(sizeMappings.get(token))); + component.setElementDefault(elementDefaults.get(token)); + component.setRequiredInHierarchy(Boolean.TRUE.equals(level.getRequired())); + configuration.addAddressComponent(component); + } + + if (template != null && template.getLineByLineFormat() != null) { + configuration.setLineByLineFormat(new ArrayList<>(template.getLineByLineFormat())); + } + + AddressHierarchyFile file = new AddressHierarchyFile(); + file.setFilename(ENTRIES_FILE_NAME); + file.setEntryDelimiter(ENTRY_DELIMITER); + file.setIdentifierDelimiter(IDENTIFIER_DELIMITER); + configuration.setAddressHierarchyFile(file); + + return AddressConfigurationLoader.writeToString(configuration); + } + + /** + * Builds the headerless entries CSV from the entries alone (no extra queries): a leaf is any entry + * that is not some other entry's parent, and each leaf's row is its root-to-leaf path walked + * through {@link AddressHierarchyEntry#getParent()}. Rows are sorted for deterministic output. + */ + String buildEntriesCsv(Collection instances) { + Set parentIds = new HashSet<>(); + for (AddressHierarchyEntry entry : instances) { + AddressHierarchyEntry parent = entry.getParent(); + if (parent != null && parent.getId() != null) { + parentIds.add(parent.getId()); + } + } + + List rows = new ArrayList<>(); + for (AddressHierarchyEntry entry : instances) { + if (entry.getId() != null && parentIds.contains(entry.getId())) { + continue; // not a leaf: it is covered by its descendants' rows + } + rows.add(buildRow(entry)); + } + Collections.sort(rows); + + StringBuilder csv = new StringBuilder(); + for (String row : rows) { + csv.append(row).append('\n'); + } + return csv.toString(); + } + + private static String buildRow(AddressHierarchyEntry leaf) { + Deque cells = new ArrayDeque<>(); + for (AddressHierarchyEntry current = leaf; current != null; current = current.getParent()) { + cells.addFirst(buildCell(current)); + } + return String.join(ENTRY_DELIMITER, cells); + } + + private static String buildCell(AddressHierarchyEntry entry) { + String name = entry.getName() == null ? "" : entry.getName(); + if (StringUtils.isNotEmpty(entry.getUserGeneratedId())) { + return name + IDENTIFIER_DELIMITER + entry.getUserGeneratedId(); + } + return name; + } + + private static int parseSize(String size) { + if (StringUtils.isBlank(size)) { + return DEFAULT_SIZE_MAPPING; + } + try { + return Integer.parseInt(size.trim()); + } + catch (NumberFormatException e) { + return DEFAULT_SIZE_MAPPING; + } + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java new file mode 100644 index 0000000..9ccc9cd --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java @@ -0,0 +1,107 @@ +/* + * 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.metadataexport.domain.addresshierarchy; + +import org.junit.jupiter.api.Test; +import org.openmrs.layout.address.AddressTemplate; +import org.openmrs.module.addresshierarchy.AddressField; +import org.openmrs.module.addresshierarchy.AddressHierarchyEntry; +import org.openmrs.module.addresshierarchy.AddressHierarchyLevel; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AddressHierarchyDomainExporterTest { + + private final AddressHierarchyDomainExporter exporter = new AddressHierarchyDomainExporter(); + + private static AddressHierarchyEntry entry(int id, String name, AddressHierarchyEntry parent) { + AddressHierarchyEntry entry = new AddressHierarchyEntry(); + entry.setId(id); + entry.setName(name); + entry.setParent(parent); + return entry; + } + + private static AddressHierarchyLevel level(AddressField field, String name, boolean required) { + AddressHierarchyLevel level = new AddressHierarchyLevel(); + level.setAddressField(field); + level.setName(name); + level.setRequired(required); + return level; + } + + @Test + void buildEntriesCsv_emitsOneSortedRowPerLeafPath() { + AddressHierarchyEntry country = entry(1, "Cambodia", null); + AddressHierarchyEntry province = entry(2, "Banteay Meanchey", country); + AddressHierarchyEntry district = entry(3, "Mongkol Borei", province); + AddressHierarchyEntry otherProvince = entry(4, "Kampong Cham", country); + + String csv = exporter.buildEntriesCsv(Arrays.asList(country, province, district, otherProvince)); + + // country and the first province are interior nodes (parents), so only the two leaves are rows, + // each a full root-to-leaf path, sorted for deterministic output. + assertEquals("Cambodia,Banteay Meanchey,Mongkol Borei\n" + "Cambodia,Kampong Cham\n", csv); + } + + @Test + void buildEntriesCsv_appendsUserGeneratedIdWithIdentifierDelimiter() { + AddressHierarchyEntry country = entry(1, "Cambodia", null); + AddressHierarchyEntry province = entry(2, "Banteay Meanchey", country); + province.setUserGeneratedId("BM"); + + String csv = exporter.buildEntriesCsv(Arrays.asList(country, province)); + + assertEquals("Cambodia,Banteay Meanchey^BM\n", csv); + } + + @Test + void buildAddressConfigurationXml_reproducesComponentsFileAndFormat() { + Map sizeMappings = new HashMap<>(); + sizeMappings.put(AddressField.COUNTRY.getName(), "40"); + sizeMappings.put(AddressField.STATE_PROVINCE.getName(), "40"); + Map elementDefaults = new HashMap<>(); + elementDefaults.put(AddressField.COUNTRY.getName(), "addresshierarchy.cambodia"); + + AddressTemplate template = new AddressTemplate("addressTemplate"); + template.setSizeMappings(sizeMappings); + template.setElementDefaults(elementDefaults); + template.setLineByLineFormat(Arrays.asList("stateProvince", "country")); + + String xml = exporter + .buildAddressConfigurationXml(Arrays.asList(level(AddressField.COUNTRY, "Location.country", true), + level(AddressField.STATE_PROVINCE, "Location.province", true)), template); + + assertTrue(xml.contains("COUNTRY"), xml); + assertTrue(xml.contains("Location.country"), xml); + assertTrue(xml.contains("40"), xml); + assertTrue(xml.contains("addresshierarchy.cambodia"), xml); + assertTrue(xml.contains("true"), xml); + assertTrue(xml.contains("STATE_PROVINCE"), xml); + assertTrue(xml.contains("country"), xml); + assertTrue(xml.contains("addresshierarchy.csv"), xml); + assertTrue(xml.contains(","), xml); + assertTrue(xml.contains("^"), xml); + } + + @Test + void buildAddressConfigurationXml_defaultsSizeWhenTemplateMissing() { + String xml = exporter + .buildAddressConfigurationXml(Arrays.asList(level(AddressField.COUNTRY, "Location.country", false)), null); + + assertTrue(xml.contains("40"), xml); + assertTrue(xml.contains("false"), xml); + } +} From 522d9b08878990086a3fa11d9eec1b543804e2b4 Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Mon, 10 Aug 2026 18:51:35 +0530 Subject: [PATCH 4/8] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7fe69cd..b26799c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ Currently supported domains: * Person Attribute Types (name, description, searchable, format, foreign uuid, edit privilege) * Location Tags (name, description) * Locations (name, description, parent location, tags, address fields) — parent locations and tags - are pulled in via cross-domain closure + are pulled in via cross-domain closure. Tag membership is emitted inline as `Tag|` columns, + which is Initializer's own equivalent of the standalone `locationtagmaps` domain, so that data + needs no separate file * Drugs (name, description, strength, concept drug, concept dosage form, ingredients, mappings) — drug/dosage-form/ingredient concepts are pulled in via cross-domain closure * Order types (name, description, java class name, parent, concept classes) — parent order types and @@ -64,12 +66,14 @@ Currently supported domains: * Metadata sharing (raw zip packages already built and published through the metadatasharing module's own UI, copied out as-is; not CSV/XML — one file per package) — requires the metadatasharing module +* Address hierarchy (the `addressConfiguration.xml`, rebuilt from the ordered hierarchy levels and + the live address template, plus a headerless `addresshierarchy.csv` of one root-to-leaf path per + leaf entry; not CSV/XML rows — a whole-config directory) — requires the addresshierarchy module Domains contributed by other modules (supportable, but depend on the module being present; not yet covered): * Identifier generation (idgen, auto-generation options) -* Address hierarchy (address hierarchy entries, location tag maps) * Forms (Bahmni forms, AMPATH forms, AMPATH form translations, HTML forms) * Billing / cashier (billable services, payment modes, cash points, cashier item prices) * Appointment scheduling (specialities, service definitions, service types) From 783de897b7f94b0060fd5f163a64b583668be0a4 Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Mon, 10 Aug 2026 18:59:59 +0530 Subject: [PATCH 5/8] apply spotless --- .../domain/addresshierarchy/AddressHierarchyDomainExporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java index 449cc23..5a8abf0 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java @@ -100,7 +100,7 @@ public Collection getAllInstances() { } return all; } - + @Override public Collection getDependencies(AddressHierarchyEntry instance) { return Collections.emptyList(); From 1b710549de48f6ac226bc75208684adb0a707f91 Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Mon, 10 Aug 2026 19:20:24 +0530 Subject: [PATCH 6/8] apple spotless --- .../AddressHierarchyDomainExporter.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java index 5a8abf0..5b32b7e 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java @@ -43,29 +43,6 @@ import java.util.Map; import java.util.Set; -/** - * Exports the address hierarchy, which Initializer does not load through its own line-processor - * framework: {@code AddressHierarchyLoader} hands the whole directory to the addresshierarchy - * module's own {@code AddressConfigurationLoader}. So this is a directory-of-files domain, not a - * row-per-object CSV, and it implements {@link DomainExporter} directly (like the metadatasharing - * exporter) rather than extending the CSV/XML line framework. - *

- * It reproduces the two files a fresh import needs: - *

    - *
  • {@code addressConfiguration.xml} — rebuilt from the ordered hierarchy levels and the - * live address template global property, then serialized with the module's own - * {@link AddressConfigurationLoader#writeToString(AddressConfiguration)} so the XML matches the - * import format. Each ordered level maps 1:1 to an {@code addressComponent}: the level's name is - * the component's {@code nameMapping} and the level's {@code required} flag is - * {@code requiredInHierarchy}; only {@code sizeMapping}/{@code elementDefault} and the - * {@code lineByLineFormat} come from the address template.
  • - *
  • {@code addresshierarchy.csv} — one headerless row per leaf entry, the root-to-leaf - * entry names joined by the entry delimiter, each cell optionally carrying - * {@code name^userGeneratedId}.
  • - *
- * The entry/identifier delimiters are not persisted anywhere (they are used only at import time), - * so canonical defaults are emitted and the CSV is written to match them. - */ @Slf4j @Component @OpenmrsProfile(modules = { "addresshierarchy:2.17.0" }) From b486b3c0ffafceefece383a311538b80b949058d Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Thu, 13 Aug 2026 07:52:54 +0530 Subject: [PATCH 7/8] Handle null mappings in buildAddressConfigurationXml and add corresponding test --- .../AddressHierarchyDomainExporter.java | 10 ++++++++-- .../AddressHierarchyDomainExporterTest.java | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java index 5b32b7e..c65e107 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java @@ -109,8 +109,14 @@ public void export(Collection instances, ExportContext co * output stays in lockstep with what it parses on import. */ String buildAddressConfigurationXml(List levels, AddressTemplate template) { - Map sizeMappings = template == null ? Collections.emptyMap() : template.getSizeMappings(); - Map elementDefaults = template == null ? Collections.emptyMap() : template.getElementDefaults(); + Map sizeMappings = template == null ? null : template.getSizeMappings(); + if (sizeMappings == null) { + sizeMappings = Collections.emptyMap(); + } + Map elementDefaults = template == null ? null : template.getElementDefaults(); + if (elementDefaults == null) { + elementDefaults = Collections.emptyMap(); + } AddressConfiguration configuration = new AddressConfiguration(); for (AddressHierarchyLevel level : levels) { diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java index 9ccc9cd..2af36d1 100644 --- a/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporterTest.java @@ -104,4 +104,18 @@ void buildAddressConfigurationXml_defaultsSizeWhenTemplateMissing() { assertTrue(xml.contains("40"), xml); assertTrue(xml.contains("false"), xml); } + + @Test + void buildAddressConfigurationXml_toleratesTemplateWithNullMappings() { + // A non-null template whose size/default maps are null must not NPE; it falls back to defaults. + AddressTemplate template = new AddressTemplate("addressTemplate"); + template.setSizeMappings(null); + template.setElementDefaults(null); + + String xml = exporter.buildAddressConfigurationXml( + Arrays.asList(level(AddressField.COUNTRY, "Location.country", true)), template); + + assertTrue(xml.contains("COUNTRY"), xml); + assertTrue(xml.contains("40"), xml); + } } From a0e489a5e09873131d5a56d3154131e5e0f19b4d Mon Sep 17 00:00:00 2001 From: Bawanthathilan Date: Thu, 13 Aug 2026 08:13:38 +0530 Subject: [PATCH 8/8] Update identifier delimiter --- .../addresshierarchy/AddressHierarchyDomainExporter.java | 2 +- .../addresshierarchy/AddressHierarchyDomainExporterTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java index c65e107..1a443d2 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/addresshierarchy/AddressHierarchyDomainExporter.java @@ -54,7 +54,7 @@ public class AddressHierarchyDomainExporter implements DomainExportercountry
"), xml); assertTrue(xml.contains("addresshierarchy.csv"), xml); assertTrue(xml.contains(","), xml); - assertTrue(xml.contains("^"), xml); + assertTrue(xml.contains("%"), xml); } @Test