From 47ec353b47123072d36e8a3393322fbb376b2367 Mon Sep 17 00:00:00 2001 From: Wikum Weerakutti Date: Tue, 11 Aug 2026 19:06:32 +0530 Subject: [PATCH 1/2] ME-32: Support IDGEN Domains (identifier sources & auto generation options) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds exporters for the idgen module's two Initializer domains: - **idgen** — identifier sources, split into `idgen_sequential` / `idgen_remote` / `idgen_pool` CSVs with `_order:` headers so pools load after the sources they reference (Initializer infers the source type from which columns are present, so types can't share a file). Remote-source passwords are exported as `property:` placeholders, never plaintext. - **autogenerationoptions** — referenced identifier types, sources and locations are pulled in via cross-domain closure. --- README.md | 19 +- .../AutoGenerationOptionDomainExporter.java | 114 ++++++++++++ .../AutoGenerationOptionLineExporter.java | 41 ++++ .../idgen/IdentifierPoolLineExporter.java | 46 +++++ .../idgen/IdentifierSourceDomainExporter.java | 134 ++++++++++++++ .../idgen/IdentifierSourceLineExporter.java | 62 +++++++ .../RemoteIdentifierSourceLineExporter.java | 50 +++++ ...entialIdentifierGeneratorLineExporter.java | 45 +++++ .../export/BaseLineExporter.java | 2 - .../export/CsvDomainExporter.java | 13 +- .../metadataexport/export/CsvExporter.java | 14 +- .../export/MetadataLineExporter.java | 11 +- ...utoGenerationOptionDomainExporterTest.java | 129 +++++++++++++ .../AutoGenerationOptionLineExporterTest.java | 82 ++++++++ .../idgen/IdentifierPoolLineExporterTest.java | 86 +++++++++ .../IdentifierSourceDomainExporterTest.java | 175 ++++++++++++++++++ .../IdentifierSourceLineExporterTest.java | 103 +++++++++++ ...emoteIdentifierSourceLineExporterTest.java | 67 +++++++ ...alIdentifierGeneratorLineExporterTest.java | 100 ++++++++++ .../export/CsvDomainExporterTest.java | 33 ++++ .../export/CsvExporterTest.java | 27 +++ omod/src/main/resources/config.xml | 1 + pom.xml | 8 + 23 files changed, 1350 insertions(+), 12 deletions(-) create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporter.java create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporter.java create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporter.java create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporter.java create mode 100644 api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporter.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporterTest.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporterTest.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporterTest.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporterTest.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporterTest.java create mode 100644 api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporterTest.java diff --git a/README.md b/README.md index 7fe69cd..a64caaf 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,24 @@ 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 +* Identifier sources (identifier type, name, description; sequential: prefix, suffix, first + identifier base, min/max length, base character set; remote: url, user, password; pool: backing + source, batch size, minimum size, refill with task, sequential allocation) — written as + idgen_sequential/idgen_remote/idgen_pool CSVs with pools ordered last so backing sources load + first; remote-source passwords are never exported in plaintext — each row carries a + `property:idgen.remote.password.` placeholder, and the importing server + must define the `idgen.remote.password.` system or OpenMRS runtime + property (retired remote sources included — Initializer still requires the password when it + bootstraps them); custom identifier source types from other modules have no Initializer + representation and are skipped with a warning, as are auto generation options pointing at them; + requires the idgen module (4.6+) +* Auto generation options (identifier type, location, identifier source, manual entry enabled, + auto generation enabled) — the referenced identifier type, source and location are pulled in via + cross-domain closure; requires the idgen module (4.6+) 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) @@ -215,7 +228,9 @@ Exporters that only contribute extra columns to an existing row (not the primary `BaseLineExporter` directly instead. A CSV domain may emit more than one file by overriding `partition(instances)` (the default is one -file). +file). When the files must load in a set sequence — e.g. idgen pools after the sources they +reference — also override `order(fileName)` to stamp each file with an Initializer `_order:` +header. For an XML domain (Initializer loads some domains, such as global properties, from XML rather than CSV), extend `XmlDomainExporter` instead of `CsvDomainExporter`. Build the DOM in diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporter.java new file mode 100644 index 0000000..fe9fdbf --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporter.java @@ -0,0 +1,114 @@ +/* + * 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.idgen; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.openmrs.OpenmrsObject; +import org.openmrs.PatientIdentifierType; +import org.openmrs.annotation.OpenmrsProfile; +import org.openmrs.api.context.Context; +import org.openmrs.api.db.hibernate.HibernateUtil; +import org.openmrs.module.idgen.AutoGenerationOption; +import org.openmrs.module.idgen.IdentifierPool; +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.idgen.RemoteIdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.idgen.service.IdentifierSourceService; +import org.openmrs.module.initializer.Domain; +import org.openmrs.module.metadataexport.export.BaseLineExporter; +import org.openmrs.module.metadataexport.export.CsvDomainExporter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@Slf4j +@Component +@OpenmrsProfile(modules = { "idgen:4.6.* - 9.*" }) +public class AutoGenerationOptionDomainExporter extends CsvDomainExporter { + + @Override + protected List> chain() { + return Collections.singletonList(new AutoGenerationOptionLineExporter()); + } + + @Override + protected String fileName() { + return "autoGenerationOptions.csv"; + } + + @Override + public Domain getDomain() { + return Domain.AUTO_GENERATION_OPTIONS; + } + + @Override + public boolean handles(OpenmrsObject instance) { + return instance instanceof AutoGenerationOption; + } + + @Override + public Collection getAllInstances() { + IdentifierSourceService service = Context.getService(IdentifierSourceService.class); + List options = new ArrayList<>(); + for (PatientIdentifierType type : Context.getPatientService().getAllPatientIdentifierTypes(true)) { + List forType = service.getAutoGenerationOptions(type); + if (forType != null) { + options.addAll(forType); + } + } + return exportable(options); + } + + /** The subset of options that can round-trip through Iniz, in a stable order. */ + static List exportable(List options) { + List result = new ArrayList<>(); + for (AutoGenerationOption option : options) { + if (BooleanUtils.isTrue(option.getRetired())) { + continue; + } + IdentifierSource source = HibernateUtil.getRealObjectFromProxy(option.getSource()); + if (source != null && !(source instanceof SequentialIdentifierGenerator) + && !(source instanceof RemoteIdentifierSource) && !(source instanceof IdentifierPool)) { + log.warn("Idgen: skipping auto generation option {} whose source {} has unsupported type {}", + option.getUuid(), source.getUuid(), source.getClass().getName()); + continue; + } + result.add(option); + } + result.sort(Comparator + .comparing( + (AutoGenerationOption o) -> o.getIdentifierType() == null || o.getIdentifierType().getName() == null ? "" + : o.getIdentifierType().getName()) + .thenComparing(o -> o.getLocation() == null ? "" : o.getLocation().getName(), + Comparator.nullsFirst(Comparator.naturalOrder())) + .thenComparing(AutoGenerationOption::getUuid, Comparator.nullsFirst(Comparator.naturalOrder()))); + return result; + } + + @Override + public Collection getDependencies(AutoGenerationOption instance) { + List dependencies = new ArrayList<>(); + if (instance.getIdentifierType() != null) { + dependencies.add(instance.getIdentifierType()); + } + if (instance.getSource() != null) { + dependencies.add(instance.getSource()); + } + if (instance.getLocation() != null) { + dependencies.add(instance.getLocation()); + } + return dependencies; + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporter.java new file mode 100644 index 0000000..e24a3ee --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporter.java @@ -0,0 +1,41 @@ +/* + * 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.idgen; + +import org.openmrs.module.idgen.AutoGenerationOption; +import org.openmrs.module.initializer.api.BaseLineProcessor; +import org.openmrs.module.initializer.api.idgen.autogen.AutoGenerationOptionLineProcessor; +import org.openmrs.module.metadataexport.export.BaseLineExporter; +import org.openmrs.module.metadataexport.export.ExportLine; + +/** + * Iniz ignores {@code void/retire} for this domain, so a full row is always written (retired + * options are filtered out by the domain exporter instead). The boolean columns are always emitted + * — an absent cell becomes a null that NPEs unboxing into idgen's primitive-boolean setters. + */ +public class AutoGenerationOptionLineExporter extends BaseLineExporter { + + @Override + public void export(AutoGenerationOption option, ExportLine line) { + line.put(BaseLineProcessor.HEADER_UUID, option.getUuid()); + if (option.getIdentifierType() != null) { + line.put(AutoGenerationOptionLineProcessor.IDENTIFIER_TYPE, option.getIdentifierType().getUuid()); + } + if (option.getLocation() != null) { + line.put(AutoGenerationOptionLineProcessor.LOCATION, option.getLocation().getUuid()); + } + if (option.getSource() != null) { + line.put(AutoGenerationOptionLineProcessor.IDENTIFIER_SOURCE, option.getSource().getUuid()); + } + line.put(AutoGenerationOptionLineProcessor.MANUAL_ENTRY_ENABLED, Boolean.toString(option.isManualEntryEnabled())); + line.put(AutoGenerationOptionLineProcessor.AUTO_GEN_ENABLED, + Boolean.toString(option.isAutomaticGenerationEnabled())); + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java new file mode 100644 index 0000000..3545a90 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java @@ -0,0 +1,46 @@ +/* + * 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.idgen; + +import lombok.extern.slf4j.Slf4j; +import org.openmrs.api.db.hibernate.HibernateUtil; +import org.openmrs.module.idgen.IdentifierPool; +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.metadataexport.export.BaseLineExporter; +import org.openmrs.module.metadataexport.export.ExportLine; + +/** + * Columns specific to {@link IdentifierPool} sources. The pooled identifiers themselves are runtime + * data and are not exported. The boolean columns are always emitted — an absent cell becomes a null + * that NPEs when Iniz assigns it into idgen's primitive-backed fields. + */ +@Slf4j +public class IdentifierPoolLineExporter extends BaseLineExporter { + + @Override + public void export(IdentifierSource source, ExportLine line) { + source = HibernateUtil.getRealObjectFromProxy(source); + if (!(source instanceof IdentifierPool)) { + return; + } + + IdentifierPool pool = (IdentifierPool) source; + if (pool.getSource() == null) { + log.warn("Idgen: identifier pool {} has no backing source; Iniz requires one on import", pool.getUuid()); + } else { + line.put(IdentifierSourceLineExporter.HEADER_POOL_IDENTIFIER_SOURCE, pool.getSource().getUuid()); + } + line.put(IdentifierSourceLineExporter.HEADER_POOL_BATCH_SIZE, String.valueOf(pool.getBatchSize())); + line.put(IdentifierSourceLineExporter.HEADER_POOL_MINIMUM_SIZE, String.valueOf(pool.getMinPoolSize())); + line.put(IdentifierSourceLineExporter.HEADER_POOL_REFILL_WITH_TASK, + Boolean.toString(pool.isRefillWithScheduledTask())); + line.put(IdentifierSourceLineExporter.HEADER_POOL_SEQUENTIAL_ALLOCATION, Boolean.toString(pool.isSequential())); + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java new file mode 100644 index 0000000..a271508 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java @@ -0,0 +1,134 @@ +/* + * 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.idgen; + +import lombok.extern.slf4j.Slf4j; +import org.openmrs.OpenmrsObject; +import org.openmrs.annotation.OpenmrsProfile; +import org.openmrs.api.context.Context; +import org.openmrs.api.db.hibernate.HibernateUtil; +import org.openmrs.module.idgen.IdentifierPool; +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.idgen.RemoteIdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.idgen.service.IdentifierSourceService; +import org.openmrs.module.initializer.Domain; +import org.openmrs.module.metadataexport.export.BaseLineExporter; +import org.openmrs.module.metadataexport.export.CsvDomainExporter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Identifier sources are written as one file per source type, mirroring Iniz's own fixture layout, + * so each file carries an {@code _order:} header — pools reference their backing source by uuid, so + * the pool file must load last. Custom {@link IdentifierSource} subclasses have no Iniz + * representation and are skipped with a warning. + */ +@Slf4j +@Component +@OpenmrsProfile(modules = { "idgen:4.6.* - 9.*" }) +public class IdentifierSourceDomainExporter extends CsvDomainExporter { + + public static final String FILE_SEQUENTIAL = "idgen_sequential.csv"; + + public static final String FILE_REMOTE = "idgen_remote.csv"; + + public static final String FILE_POOL = "idgen_pool.csv"; + + @Override + protected List> chain() { + return Arrays.asList(new IdentifierSourceLineExporter(), new SequentialIdentifierGeneratorLineExporter(), + new RemoteIdentifierSourceLineExporter(), new IdentifierPoolLineExporter()); + } + + @Override + protected String fileName() { + throw new UnsupportedOperationException("idgen writes one file per source type; see partition()"); + } + + @Override + protected Map> partition(Collection instances) { + Map> files = new LinkedHashMap<>(); + for (IdentifierSource instance : instances) { + IdentifierSource real = HibernateUtil.getRealObjectFromProxy(instance); + if (real instanceof IdentifierPool) { + files.computeIfAbsent(FILE_POOL, f -> new ArrayList<>()).add(instance); + } else if (real instanceof SequentialIdentifierGenerator) { + files.computeIfAbsent(FILE_SEQUENTIAL, f -> new ArrayList<>()).add(instance); + } else if (real instanceof RemoteIdentifierSource) { + files.computeIfAbsent(FILE_REMOTE, f -> new ArrayList<>()).add(instance); + } else { + log.warn("Idgen: skipping identifier source {} of unsupported type {}", real.getUuid(), + real.getClass().getName()); + } + } + return files; + } + + @Override + protected Integer order(String fileName) { + // pools must load after the sources they reference + switch (fileName) { + case FILE_SEQUENTIAL: + return 1000; + case FILE_REMOTE: + return 2000; + case FILE_POOL: + return 3000; + default: + throw new IllegalArgumentException("Not an idgen export file: " + fileName); + } + } + + @Override + public Domain getDomain() { + return Domain.IDGEN; + } + + @Override + public boolean handles(OpenmrsObject instance) { + return instance instanceof SequentialIdentifierGenerator || instance instanceof RemoteIdentifierSource + || instance instanceof IdentifierPool; + } + + @Override + public Collection getAllInstances() { + List sources = new ArrayList<>(); + for (IdentifierSource source : Context.getService(IdentifierSourceService.class).getAllIdentifierSources(true)) { + IdentifierSource real = HibernateUtil.getRealObjectFromProxy(source); + if (handles(real)) { + sources.add(source); + } else { + log.warn("Idgen: skipping identifier source {} of unsupported type {} — no Iniz representation", + real.getUuid(), real.getClass().getName()); + } + } + return sources; + } + + @Override + public Collection getDependencies(IdentifierSource instance) { + List dependencies = new ArrayList<>(); + if (instance.getIdentifierType() != null) { + dependencies.add(instance.getIdentifierType()); + } + IdentifierSource real = HibernateUtil.getRealObjectFromProxy(instance); + if (real instanceof IdentifierPool && ((IdentifierPool) real).getSource() != null) { + dependencies.add(((IdentifierPool) real).getSource()); + } + return dependencies; + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporter.java new file mode 100644 index 0000000..133f52a --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporter.java @@ -0,0 +1,62 @@ +/* + * 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.idgen; + +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.initializer.api.BaseLineProcessor; +import org.openmrs.module.metadataexport.export.ExportLine; +import org.openmrs.module.metadataexport.export.MetadataLineExporter; + +public class IdentifierSourceLineExporter extends MetadataLineExporter { + + public static final String HEADER_IDTYPE = "Identifier type"; + + public static final String HEADER_POOL_IDENTIFIER_SOURCE = "pool identifier source"; + + public static final String HEADER_POOL_BATCH_SIZE = "pool refill batch size"; + + public static final String HEADER_POOL_MINIMUM_SIZE = "pool minimum size"; + + public static final String HEADER_POOL_REFILL_WITH_TASK = "pool refill with task"; + + public static final String HEADER_POOL_SEQUENTIAL_ALLOCATION = "pool sequential allocation"; + + public static final String HEADER_URL = "url"; + + public static final String HEADER_USER = "user"; + + public static final String HEADER_PASS = "password"; + + public static final String HEADER_PREFIX = "prefix"; + + public static final String HEADER_SUFFIX = "suffix"; + + public static final String HEADER_FIRST_ID_BASE = "first identifier base"; + + public static final String HEADER_MIN_LENGTH = "min length"; + + public static final String HEADER_MAX_LENGTH = "max length"; + + public static final String HEADER_BASE_CHAR_SET = "base character set"; + + @Override + public void export(IdentifierSource source, ExportLine line) { + if (source.getIdentifierType() != null) { + line.put(HEADER_IDTYPE, source.getIdentifierType().getUuid()); + } + line.put(BaseLineProcessor.HEADER_NAME, source.getName()); + line.put(BaseLineProcessor.HEADER_DESC, source.getDescription()); + } + + @Override + protected void writeRetiredDiscriminators(IdentifierSource source, ExportLine line) { + export(source, line); + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporter.java new file mode 100644 index 0000000..56bb36b --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporter.java @@ -0,0 +1,50 @@ +/* + * 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.idgen; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.openmrs.api.db.hibernate.HibernateUtil; +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.idgen.RemoteIdentifierSource; +import org.openmrs.module.metadataexport.export.BaseLineExporter; +import org.openmrs.module.metadataexport.export.ExportLine; + +/** + * Columns specific to {@link RemoteIdentifierSource} sources. + */ +@Slf4j +public class RemoteIdentifierSourceLineExporter extends BaseLineExporter { + + @Override + public void export(IdentifierSource source, ExportLine line) { + source = HibernateUtil.getRealObjectFromProxy(source); + if (!(source instanceof RemoteIdentifierSource)) { + return; + } + + RemoteIdentifierSource remote = (RemoteIdentifierSource) source; + if (StringUtils.isBlank(remote.getUser())) { + log.warn("Idgen: remote identifier source {} has no user; Iniz requires one on import", remote.getUuid()); + } + line.put(IdentifierSourceLineExporter.HEADER_URL, remote.getUrl()); + line.put(IdentifierSourceLineExporter.HEADER_USER, remote.getUser()); + line.put(IdentifierSourceLineExporter.HEADER_PASS, exportedPassword(remote)); + } + + /** + * The stored password is a live credential and never leaves the system: Iniz requires the column, + * so a per-source {@code property:} indirection is exported instead, resolved from the matching + * system/runtime property on the importing server. + */ + private String exportedPassword(RemoteIdentifierSource remote) { + return "property:idgen.remote.password." + remote.getUuid(); + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporter.java new file mode 100644 index 0000000..e05fa42 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporter.java @@ -0,0 +1,45 @@ +/* + * 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.idgen; + +import org.openmrs.api.db.hibernate.HibernateUtil; +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.metadataexport.export.BaseLineExporter; +import org.openmrs.module.metadataexport.export.ExportLine; + +/** + * Columns specific to {@link SequentialIdentifierGenerator} sources. {@code nextSequenceValue} is + * runtime state and is not exported. + */ +public class SequentialIdentifierGeneratorLineExporter extends BaseLineExporter { + + @Override + public void export(IdentifierSource source, ExportLine line) { + source = HibernateUtil.getRealObjectFromProxy(source); + if (!(source instanceof SequentialIdentifierGenerator)) { + return; + } + + SequentialIdentifierGenerator generator = (SequentialIdentifierGenerator) source; + line.put(IdentifierSourceLineExporter.HEADER_PREFIX, generator.getPrefix()); + line.put(IdentifierSourceLineExporter.HEADER_SUFFIX, generator.getSuffix()); + line.put(IdentifierSourceLineExporter.HEADER_FIRST_ID_BASE, generator.getFirstIdentifierBase()); + put(line, IdentifierSourceLineExporter.HEADER_MIN_LENGTH, generator.getMinLength()); + put(line, IdentifierSourceLineExporter.HEADER_MAX_LENGTH, generator.getMaxLength()); + line.put(IdentifierSourceLineExporter.HEADER_BASE_CHAR_SET, generator.getBaseCharacterSet()); + } + + private void put(ExportLine line, String header, Object value) { + if (value != null) { + line.put(header, value.toString()); + } + } +} diff --git a/api/src/main/java/org/openmrs/module/metadataexport/export/BaseLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/export/BaseLineExporter.java index c922b18..ee5e900 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/export/BaseLineExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/export/BaseLineExporter.java @@ -13,8 +13,6 @@ public abstract class BaseLineExporter { - public static final String VERSION_LHS = "_version:"; - /** Domain-specific columns for one instance. Subclasses implement this. */ public abstract void export(T instance, ExportLine line); diff --git a/api/src/main/java/org/openmrs/module/metadataexport/export/CsvDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/export/CsvDomainExporter.java index 81064d6..6da1558 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/export/CsvDomainExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/export/CsvDomainExporter.java @@ -25,7 +25,8 @@ * instances that belong in it, and each entry becomes its own CSV. The default is one file * ({@link #fileName}), but a domain may split into several (e.g. to keep the union-header width * manageable, since one unusually verbose row otherwise widens the table for every row in the - * file). + * file). When the files must load in a set sequence, {@link #order} stamps each with an Iniz + * {@code _order:} header. */ public abstract class CsvDomainExporter implements DomainExporter { @@ -37,11 +38,19 @@ protected Map> partition(Collection instances) { return Collections.singletonMap(fileName(), instances); } + /** + * Iniz within-domain load order for the given file, or null for no {@code _order:} header (Iniz + * then loads the file last). + */ + protected Integer order(String fileName) { + return null; + } + @Override public void export(Collection instances, ExportContext context) throws IOException { CsvExporter exporter = new CsvExporter<>(chain(), getDomain()); for (Map.Entry> file : partition(instances).entrySet()) { - exporter.writeCsv(file.getValue(), context.getOutputDir(), file.getKey()); + exporter.writeCsv(file.getValue(), context.getOutputDir(), file.getKey(), order(file.getKey())); } } } diff --git a/api/src/main/java/org/openmrs/module/metadataexport/export/CsvExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/export/CsvExporter.java index 0ac1dc9..14a1b75 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/export/CsvExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/export/CsvExporter.java @@ -13,6 +13,7 @@ import lombok.AllArgsConstructor; import org.openmrs.OpenmrsObject; import org.openmrs.module.initializer.Domain; +import org.openmrs.module.initializer.api.BaseLineProcessor; import java.io.File; import java.io.IOException; @@ -44,13 +45,24 @@ public List toLines(Collection instances) { } public void writeCsv(Collection instances, File outDir, String fileName) throws IOException { + writeCsv(instances, outDir, fileName, null); + } + + /** + * @param order Iniz within-domain load order, emitted as an {@code _order:} header column; null for + * no order annotation (Iniz then loads the file last). + */ + public void writeCsv(Collection instances, File outDir, String fileName, Integer order) throws IOException { List lines = toLines(instances); LinkedHashSet headers = new LinkedHashSet<>(); for (ExportLine line : lines) { headers.addAll(line.getHeaders()); } - headers.add(BaseLineExporter.VERSION_LHS + "1"); + headers.add(BaseLineProcessor.VERSION_LHS + "1"); + if (order != null) { + headers.add(BaseLineProcessor.ORDER_LHS + order); + } String[] headerRow = headers.toArray(new String[0]); File domainDir = new File(new File(outDir, "configuration"), domain.getName()); diff --git a/api/src/main/java/org/openmrs/module/metadataexport/export/MetadataLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/export/MetadataLineExporter.java index 15ec7f2..8e214f4 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/export/MetadataLineExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/export/MetadataLineExporter.java @@ -17,11 +17,12 @@ /** * Base for the primary line exporter of a metadata domain. Writes the columns every Initializer row * carries — the uuid, and for a retired object the {@code void/retire} flag — then delegates the - * domain-specific columns to {@link #export}. A retired object is emitted as uuid + flag only, so - * {@link #export} only ever sees a live instance. Domains whose Initializer parser requires - * discriminator columns (e.g. {@code entity name}) even on retired rows can override - * {@link #writeRetiredDiscriminators} to emit those columns after the retire flag. The default - * implementation is a no operation + * domain-specific columns to {@link #export}. By default a retired object is emitted as uuid + flag + * only, so {@link #export} only ever sees a live instance. Domains whose Initializer parser needs + * more from a retired row can override {@link #writeRetiredDiscriminators} (a no-op by default) to + * emit further columns after the flag — from single discriminator columns (e.g. + * {@code entity name}) up to re-dispatching to {@link #export} for parsers that bootstrap and fill + * retired rows whose uuid is unknown on the target (e.g. idgen). */ public abstract class MetadataLineExporter extends BaseLineExporter { diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporterTest.java new file mode 100644 index 0000000..35f88d4 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionDomainExporterTest.java @@ -0,0 +1,129 @@ +/* + * 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.idgen; + +import org.junit.jupiter.api.Test; +import org.openmrs.Location; +import org.openmrs.OpenmrsObject; +import org.openmrs.PatientIdentifierType; +import org.openmrs.module.idgen.AutoGenerationOption; +import org.openmrs.module.idgen.BaseIdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AutoGenerationOptionDomainExporterTest { + + private final AutoGenerationOptionDomainExporter exporter = new AutoGenerationOptionDomainExporter(); + + @Test + void dependenciesIncludeIdentifierTypeSourceAndLocation() { + PatientIdentifierType type = new PatientIdentifierType(); + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + Location location = new Location(); + + AutoGenerationOption option = new AutoGenerationOption(); + option.setIdentifierType(type); + option.setSource(source); + option.setLocation(location); + + Collection dependencies = exporter.getDependencies(option); + + assertEquals(3, dependencies.size()); + assertTrue(dependencies.contains(type)); + assertTrue(dependencies.contains(source)); + assertTrue(dependencies.contains(location)); + } + + @Test + void dependenciesOmitUnsetLocation() { + PatientIdentifierType type = new PatientIdentifierType(); + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + + AutoGenerationOption option = new AutoGenerationOption(); + option.setIdentifierType(type); + option.setSource(source); + + Collection dependencies = exporter.getDependencies(option); + + assertEquals(2, dependencies.size()); + assertTrue(dependencies.contains(type)); + assertTrue(dependencies.contains(source)); + } + + @Test + void dependenciesAreNullSafe() { + assertTrue(exporter.getDependencies(new AutoGenerationOption()).isEmpty()); + } + + @Test + void handlesAutoGenerationOptionsOnly() { + assertTrue(exporter.handles(new AutoGenerationOption())); + assertFalse(exporter.handles(new PatientIdentifierType())); + } + + @Test + void exportableFiltersRetiredOptions() { + AutoGenerationOption live = new AutoGenerationOption(); + AutoGenerationOption retired = new AutoGenerationOption(); + retired.setRetired(true); + + List result = AutoGenerationOptionDomainExporter.exportable(Arrays.asList(live, retired)); + + assertEquals(Collections.singletonList(live), result); + } + + @Test + void exportableSkipsOptionsWithUnsupportedSourceType() { + AutoGenerationOption dangling = new AutoGenerationOption(); + dangling.setSource(new BaseIdentifierSource() {}); + AutoGenerationOption kept = new AutoGenerationOption(); + kept.setSource(new SequentialIdentifierGenerator()); + + List result = AutoGenerationOptionDomainExporter.exportable(Arrays.asList(dangling, kept)); + + assertEquals(Collections.singletonList(kept), result, + "an option pointing at a source the idgen exporter skips would be a dangling reference"); + } + + @Test + void exportableSortsByTypeNameThenLocationNullSafe() { + AutoGenerationOption unnamedType = new AutoGenerationOption(); + unnamedType.setUuid("a"); + AutoGenerationOption noLocation = option("ID Type", null, "b"); + AutoGenerationOption withLocation = option("ID Type", "Ward", "c"); + + List result = AutoGenerationOptionDomainExporter + .exportable(Arrays.asList(withLocation, noLocation, unnamedType)); + + assertEquals(Arrays.asList(unnamedType, noLocation, withLocation), result); + } + + private static AutoGenerationOption option(String typeName, String locationName, String uuid) { + AutoGenerationOption option = new AutoGenerationOption(); + option.setUuid(uuid); + PatientIdentifierType type = new PatientIdentifierType(); + type.setName(typeName); + option.setIdentifierType(type); + if (locationName != null) { + Location location = new Location(); + location.setName(locationName); + option.setLocation(location); + } + return option; + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporterTest.java new file mode 100644 index 0000000..8b22b19 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/AutoGenerationOptionLineExporterTest.java @@ -0,0 +1,82 @@ +/* + * 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.idgen; + +import org.junit.jupiter.api.Test; +import org.openmrs.Location; +import org.openmrs.PatientIdentifierType; +import org.openmrs.module.idgen.AutoGenerationOption; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.metadataexport.export.ExportLine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class AutoGenerationOptionLineExporterTest { + + private static AutoGenerationOption option() { + PatientIdentifierType type = new PatientIdentifierType(); + type.setUuid("a5d38e09-efcb-4d91-a526-50ce1ba5011a"); + + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + source.setUuid("c1d8a345-3f10-11e4-adec-0800271c1b75"); + + AutoGenerationOption option = new AutoGenerationOption(); + option.setUuid("f5c1f1b2-3f10-11e4-adec-0800271c1b75"); + option.setIdentifierType(type); + option.setSource(source); + return option; + } + + @Test + void exportsAllColumnsWithUuidReferences() { + AutoGenerationOption option = option(); + Location location = new Location(); + location.setUuid("8d6c993e-c2cc-11de-8d13-0010c6dffd0f"); + option.setLocation(location); + option.setManualEntryEnabled(false); + option.setAutomaticGenerationEnabled(true); + + ExportLine line = new ExportLine(); + new AutoGenerationOptionLineExporter().writeLine(option, line); + + assertEquals("f5c1f1b2-3f10-11e4-adec-0800271c1b75", line.get("uuid")); + assertEquals("a5d38e09-efcb-4d91-a526-50ce1ba5011a", line.get("identifier type")); + assertEquals("8d6c993e-c2cc-11de-8d13-0010c6dffd0f", line.get("location")); + assertEquals("c1d8a345-3f10-11e4-adec-0800271c1b75", line.get("identifier source")); + assertEquals("false", line.get("manual entry enabled")); + assertEquals("true", line.get("auto generation enabled")); + } + + @Test + void omitsLocationWhenUnsetButAlwaysEmitsBothBooleans() { + AutoGenerationOption option = option(); + + ExportLine line = new ExportLine(); + new AutoGenerationOptionLineExporter().writeLine(option, line); + + assertNull(line.get("location"), "unset location is not written as a column"); + assertEquals("true", line.get("manual entry enabled")); + assertEquals("false", line.get("auto generation enabled")); + } + + @Test + void neverEmitsVoidRetireColumn() { + AutoGenerationOption option = option(); + option.setRetired(true); + + ExportLine line = new ExportLine(); + new AutoGenerationOptionLineExporter().writeLine(option, line); + + assertNull(line.get("void/retire")); + assertEquals("a5d38e09-efcb-4d91-a526-50ce1ba5011a", line.get("identifier type")); + assertEquals("c1d8a345-3f10-11e4-adec-0800271c1b75", line.get("identifier source")); + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporterTest.java new file mode 100644 index 0000000..e9c1e89 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporterTest.java @@ -0,0 +1,86 @@ +/* + * 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.idgen; + +import org.junit.jupiter.api.Test; +import org.openmrs.module.idgen.IdentifierPool; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.metadataexport.export.ExportLine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IdentifierPoolLineExporterTest { + + private static SequentialIdentifierGenerator backingSource() { + SequentialIdentifierGenerator backing = new SequentialIdentifierGenerator(); + backing.setUuid("c1d8a345-3f10-11e4-adec-0800271c1b75"); + return backing; + } + + @Test + void exportsAllPoolColumns() { + IdentifierPool pool = new IdentifierPool(); + pool.setSource(backingSource()); + pool.setBatchSize(250); + pool.setMinPoolSize(50); + pool.setRefillWithScheduledTask(false); + pool.setSequential(true); + + ExportLine line = new ExportLine(); + new IdentifierPoolLineExporter().writeLine(pool, line); + + assertEquals("c1d8a345-3f10-11e4-adec-0800271c1b75", line.get("pool identifier source")); + assertEquals("250", line.get("pool refill batch size")); + assertEquals("50", line.get("pool minimum size")); + assertEquals("false", line.get("pool refill with task")); + assertEquals("true", line.get("pool sequential allocation")); + } + + @Test + void defaultConstructedPoolExportsIdgenDefaults() { + IdentifierPool pool = new IdentifierPool(); + pool.setSource(backingSource()); + + ExportLine line = new ExportLine(); + new IdentifierPoolLineExporter().writeLine(pool, line); + + assertEquals("1000", line.get("pool refill batch size")); + assertEquals("500", line.get("pool minimum size")); + assertEquals("true", line.get("pool refill with task")); + assertEquals("false", line.get("pool sequential allocation")); + } + + @Test + void skipsNonPoolSources() { + SequentialIdentifierGenerator generator = new SequentialIdentifierGenerator(); + generator.setBaseCharacterSet("0123456789"); + + ExportLine line = new ExportLine(); + new IdentifierPoolLineExporter().writeLine(generator, line); + + assertTrue(line.getHeaders().isEmpty(), "non-pool sources contribute no columns"); + } + + @Test + void retiredPoolStillExportsItsColumns() { + IdentifierPool pool = new IdentifierPool(); + pool.setSource(backingSource()); + pool.setRetired(true); + + ExportLine line = new ExportLine(); + new IdentifierPoolLineExporter().writeLine(pool, line); + + assertEquals("c1d8a345-3f10-11e4-adec-0800271c1b75", line.get("pool identifier source")); + assertEquals("true", line.get("pool refill with task"), + "missing boolean cells NPE when Iniz fills a bootstrapped retired row"); + assertEquals("false", line.get("pool sequential allocation")); + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java new file mode 100644 index 0000000..a2184bb --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java @@ -0,0 +1,175 @@ +/* + * 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.idgen; + +import com.opencsv.CSVReader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.openmrs.OpenmrsObject; +import org.openmrs.PatientIdentifierType; +import org.openmrs.module.idgen.BaseIdentifierSource; +import org.openmrs.module.idgen.IdentifierPool; +import org.openmrs.module.idgen.IdentifierSource; +import org.openmrs.module.idgen.RemoteIdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.initializer.Domain; +import org.openmrs.module.metadataexport.export.ExportContext; + +import java.io.File; +import java.io.FileReader; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IdentifierSourceDomainExporterTest { + + private final IdentifierSourceDomainExporter exporter = new IdentifierSourceDomainExporter(); + + @Test + void partitionsSourcesByTypeIncludingRetiredOnes() { + SequentialIdentifierGenerator sequential = new SequentialIdentifierGenerator(); + SequentialIdentifierGenerator retiredSequential = new SequentialIdentifierGenerator(); + retiredSequential.setRetired(true); + RemoteIdentifierSource remote = new RemoteIdentifierSource(); + IdentifierPool pool = new IdentifierPool(); + + Map> files = exporter + .partition(Arrays.asList(sequential, remote, pool, retiredSequential)); + + assertEquals(3, files.size()); + assertTrue(files.get(IdentifierSourceDomainExporter.FILE_SEQUENTIAL).contains(sequential)); + assertTrue(files.get(IdentifierSourceDomainExporter.FILE_SEQUENTIAL).contains(retiredSequential), + "retired sources partition into their type file like live ones"); + assertTrue(files.get(IdentifierSourceDomainExporter.FILE_REMOTE).contains(remote)); + assertTrue(files.get(IdentifierSourceDomainExporter.FILE_POOL).contains(pool)); + } + + @Test + void partitionOmitsFilesForAbsentTypes() { + Map> files = exporter + .partition(Arrays.asList(new SequentialIdentifierGenerator())); + + assertEquals(1, files.size(), "no empty per-type files — a type-column-less file would be unclassifiable"); + assertTrue(files.containsKey(IdentifierSourceDomainExporter.FILE_SEQUENTIAL)); + } + + @Test + void partitionSkipsUnknownSourceSubclasses() { + IdentifierSource custom = new BaseIdentifierSource() {}; + IdentifierPool pool = new IdentifierPool(); + + Map> files = exporter.partition(Arrays.asList(custom, pool)); + + assertEquals(1, files.size(), "custom subclasses have no Iniz representation and land in no file"); + assertTrue(files.get(IdentifierSourceDomainExporter.FILE_POOL).contains(pool)); + } + + @Test + void poolFileIsOrderedAfterTheSourceFiles() { + Integer sequential = exporter.order(IdentifierSourceDomainExporter.FILE_SEQUENTIAL); + Integer remote = exporter.order(IdentifierSourceDomainExporter.FILE_REMOTE); + Integer pool = exporter.order(IdentifierSourceDomainExporter.FILE_POOL); + + assertEquals(1000, sequential); + assertEquals(2000, remote); + assertEquals(3000, pool); + assertTrue(pool > sequential && pool > remote, "pools reference backing sources, so they must load last"); + } + + @Test + void export_writesOneFilePerTypeWithItsColumnsAndOrder(@TempDir File outDir) throws Exception { + SequentialIdentifierGenerator sequential = new SequentialIdentifierGenerator(); + sequential.setUuid("seq-uuid"); + sequential.setName("Sequential"); + sequential.setFirstIdentifierBase("1000"); + sequential.setBaseCharacterSet("0123456789"); + RemoteIdentifierSource remote = new RemoteIdentifierSource(); + remote.setUuid("rem-uuid"); + remote.setName("Remote"); + remote.setUrl("https://idgen.example.org/generate"); + remote.setUser("idgen-user"); + IdentifierPool pool = new IdentifierPool(); + pool.setUuid("pool-uuid"); + pool.setName("Pool"); + pool.setSource(sequential); + + exporter.export(Arrays.asList(sequential, remote, pool), new ExportContext(outDir)); + + assertEquals("1000", cell(outDir, IdentifierSourceDomainExporter.FILE_SEQUENTIAL, "first identifier base"), + "the sequential secondary exporter must be in the chain"); + assertEquals("https://idgen.example.org/generate", cell(outDir, IdentifierSourceDomainExporter.FILE_REMOTE, "url"), + "the remote secondary exporter must be in the chain"); + assertEquals("seq-uuid", cell(outDir, IdentifierSourceDomainExporter.FILE_POOL, "pool identifier source"), + "the pool secondary exporter must be in the chain"); + assertEquals("", cell(outDir, IdentifierSourceDomainExporter.FILE_POOL, "_order:3000"), + "each file carries its load-order header"); + } + + /** The single data row's value under the given header of an exported idgen CSV. */ + private static String cell(File outDir, String fileName, String header) throws Exception { + File csv = outDir.toPath().resolve(Paths.get("configuration", Domain.IDGEN.getName(), fileName)).toFile(); + assertTrue(csv.exists(), "expected " + csv); + try (CSVReader reader = new CSVReader(new FileReader(csv))) { + List rows = reader.readAll(); + assertEquals(2, rows.size(), fileName + " holds exactly its one source"); + int column = Arrays.asList(rows.get(0)).indexOf(header); + assertTrue(column >= 0, fileName + " is missing header '" + header + "'"); + return rows.get(1)[column]; + } + } + + @Test + void dependenciesIncludeIdentifierType() { + PatientIdentifierType type = new PatientIdentifierType(); + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + source.setIdentifierType(type); + + Collection dependencies = exporter.getDependencies(source); + + assertEquals(1, dependencies.size()); + assertTrue(dependencies.contains(type)); + } + + @Test + void poolDependenciesIncludeBackingSource() { + PatientIdentifierType type = new PatientIdentifierType(); + SequentialIdentifierGenerator backing = new SequentialIdentifierGenerator(); + IdentifierPool pool = new IdentifierPool(); + pool.setIdentifierType(type); + pool.setSource(backing); + + Collection dependencies = exporter.getDependencies(pool); + + assertEquals(2, dependencies.size()); + assertTrue(dependencies.contains(type)); + assertTrue(dependencies.contains(backing)); + } + + @Test + void dependenciesAreNullSafe() { + assertTrue(exporter.getDependencies(new IdentifierPool()).isEmpty()); + } + + @Test + void handlesOnlyTheSourceTypesInizCanRepresent() { + assertTrue(exporter.handles(new SequentialIdentifierGenerator())); + assertTrue(exporter.handles(new RemoteIdentifierSource())); + assertTrue(exporter.handles(new IdentifierPool())); + assertFalse(exporter.handles(new PatientIdentifierType())); + assertFalse(exporter.handles(new BaseIdentifierSource() {}), + "handles() must agree with partition(), or selection puts sources in the manifest that export drops"); + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporterTest.java new file mode 100644 index 0000000..069ec23 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceLineExporterTest.java @@ -0,0 +1,103 @@ +/* + * 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.idgen; + +import org.junit.jupiter.api.Test; +import org.openmrs.PatientIdentifierType; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.initializer.api.idgen.IdentifierSourceLineProcessor; +import org.openmrs.module.metadataexport.export.ExportLine; + +import java.lang.reflect.Field; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class IdentifierSourceLineExporterTest { + + private static PatientIdentifierType identifierType(String uuid) { + PatientIdentifierType type = new PatientIdentifierType(); + type.setUuid(uuid); + return type; + } + + @Test + void exportsUuidIdentifierTypeNameAndDescription() { + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + source.setUuid("c1d8a345-3f10-11e4-adec-0800271c1b75"); + source.setIdentifierType(identifierType("a5d38e09-efcb-4d91-a526-50ce1ba5011a")); + source.setName("OpenMRS ID Generator"); + source.setDescription("Generates OpenMRS IDs"); + + ExportLine line = new ExportLine(); + new IdentifierSourceLineExporter().writeLine(source, line); + + assertEquals("c1d8a345-3f10-11e4-adec-0800271c1b75", line.get("uuid")); + assertEquals("a5d38e09-efcb-4d91-a526-50ce1ba5011a", line.get("Identifier type")); + assertEquals("OpenMRS ID Generator", line.get("name")); + assertEquals("Generates OpenMRS IDs", line.get("description")); + assertNull(line.get("void/retire")); + } + + @Test + void liveSourceWithoutDescriptionOrTypeOmitsThoseColumns() { + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + source.setUuid("c1d8a345-3f10-11e4-adec-0800271c1b75"); + source.setName("OpenMRS ID Generator"); + + ExportLine line = new ExportLine(); + new IdentifierSourceLineExporter().writeLine(source, line); + + assertEquals("OpenMRS ID Generator", line.get("name")); + assertNull(line.get("description"), "empty description is not written as a column"); + assertNull(line.get("Identifier type"), "missing identifier type is not written as a column"); + } + + @Test + void retiredSourceEmitsFullCommonRowPlusFlag() { + SequentialIdentifierGenerator source = new SequentialIdentifierGenerator(); + source.setUuid("439559c2-a3a4-4a25-b4b2-1a0299e287ee"); + source.setIdentifierType(identifierType("a5d38e09-efcb-4d91-a526-50ce1ba5011a")); + source.setName("Old Generator"); + source.setDescription("No longer used"); + source.setRetired(true); + + ExportLine line = new ExportLine(); + new IdentifierSourceLineExporter().writeLine(source, line); + + assertEquals("439559c2-a3a4-4a25-b4b2-1a0299e287ee", line.get("uuid")); + assertEquals("true", line.get("void/retire")); + assertEquals("Old Generator", line.get("name"), + "Iniz bootstraps retired rows with unknown uuids, so they carry the full row"); + assertEquals("No longer used", line.get("description")); + assertEquals("a5d38e09-efcb-4d91-a526-50ce1ba5011a", line.get("Identifier type")); + } + + @Test + void headerLiteralsStayInSyncWithInitializer() throws Exception { + int checked = 0; + for (Field ours : IdentifierSourceLineExporter.class.getDeclaredFields()) { + if (!ours.getName().startsWith("HEADER_")) { + continue; + } + Field theirs = IdentifierSourceLineProcessor.class.getDeclaredField(ours.getName()); + theirs.setAccessible(true); + assertEquals(theirs.get(null), ours.get(null), + ours.getName() + " drifted from Iniz's IdentifierSourceLineProcessor"); + checked++; + } + assertEquals(15, checked, "every re-declared header literal is checked against Iniz"); + + long inizHeaders = Arrays.stream(IdentifierSourceLineProcessor.class.getDeclaredFields()) + .filter(field -> field.getName().startsWith("HEADER_")).count(); + assertEquals(checked, inizHeaders, "Iniz declares a header this exporter does not know about"); + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporterTest.java new file mode 100644 index 0000000..ef24481 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/RemoteIdentifierSourceLineExporterTest.java @@ -0,0 +1,67 @@ +/* + * 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.idgen; + +import org.junit.jupiter.api.Test; +import org.openmrs.module.idgen.RemoteIdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.metadataexport.export.ExportLine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RemoteIdentifierSourceLineExporterTest { + + @Test + void exportsUrlUserAndPasswordPlaceholder() { + RemoteIdentifierSource remote = new RemoteIdentifierSource(); + remote.setUuid("9e1a2b3c-3f10-11e4-adec-0800271c1b75"); + remote.setUrl("https://idgen.example.org/generate"); + remote.setUser("idgen-user"); + remote.setPassword("s3cret-live-password"); + + ExportLine line = new ExportLine(); + new RemoteIdentifierSourceLineExporter().writeLine(remote, line); + + assertEquals("https://idgen.example.org/generate", line.get("url")); + assertEquals("idgen-user", line.get("user")); + assertEquals("property:idgen.remote.password.9e1a2b3c-3f10-11e4-adec-0800271c1b75", line.get("password")); + } + + @Test + void skipsNonRemoteSources() { + SequentialIdentifierGenerator generator = new SequentialIdentifierGenerator(); + generator.setBaseCharacterSet("0123456789"); + + ExportLine line = new ExportLine(); + new RemoteIdentifierSourceLineExporter().writeLine(generator, line); + + assertTrue(line.getHeaders().isEmpty(), "non-remote sources contribute no columns"); + } + + @Test + void retiredRemoteStillExportsItsColumns() { + RemoteIdentifierSource remote = new RemoteIdentifierSource(); + remote.setUuid("9e1a2b3c-3f10-11e4-adec-0800271c1b75"); + remote.setUrl("https://idgen.example.org/generate"); + remote.setUser("idgen-user"); + remote.setPassword("s3cret-live-password"); + remote.setRetired(true); + + ExportLine line = new ExportLine(); + new RemoteIdentifierSourceLineExporter().writeLine(remote, line); + + assertEquals("https://idgen.example.org/generate", line.get("url"), + "Iniz requires url/user/password even when it bootstraps a retired row"); + assertEquals("idgen-user", line.get("user")); + assertEquals("property:idgen.remote.password.9e1a2b3c-3f10-11e4-adec-0800271c1b75", line.get("password"), + "retired rows get the placeholder too, never the live credential"); + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporterTest.java new file mode 100644 index 0000000..c98b3ca --- /dev/null +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/SequentialIdentifierGeneratorLineExporterTest.java @@ -0,0 +1,100 @@ +/* + * 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.idgen; + +import org.junit.jupiter.api.Test; +import org.openmrs.module.idgen.RemoteIdentifierSource; +import org.openmrs.module.idgen.SequentialIdentifierGenerator; +import org.openmrs.module.metadataexport.export.ExportLine; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SequentialIdentifierGeneratorLineExporterTest { + + @Test + void exportsAllSequentialColumns() { + SequentialIdentifierGenerator generator = new SequentialIdentifierGenerator(); + generator.setPrefix("PRE-"); + generator.setSuffix("-SUF"); + generator.setFirstIdentifierBase("1000"); + generator.setMinLength(6); + generator.setMaxLength(10); + generator.setBaseCharacterSet("0123456789ACDEFGHJKLMNPRTUVWXY"); + + ExportLine line = new ExportLine(); + new SequentialIdentifierGeneratorLineExporter().writeLine(generator, line); + + assertEquals("PRE-", line.get("prefix")); + assertEquals("-SUF", line.get("suffix")); + assertEquals("1000", line.get("first identifier base")); + assertEquals("6", line.get("min length")); + assertEquals("10", line.get("max length")); + assertEquals("0123456789ACDEFGHJKLMNPRTUVWXY", line.get("base character set")); + } + + @Test + void omitsUnsetOptionalColumns() { + SequentialIdentifierGenerator generator = new SequentialIdentifierGenerator(); + generator.setFirstIdentifierBase("1"); + generator.setBaseCharacterSet("0123456789"); + + ExportLine line = new ExportLine(); + new SequentialIdentifierGeneratorLineExporter().writeLine(generator, line); + + assertNull(line.get("prefix"), "unset prefix is not written as a column"); + assertNull(line.get("suffix"), "unset suffix is not written as a column"); + assertNull(line.get("min length"), "unset min length is not written as a column"); + assertNull(line.get("max length"), "unset max length is not written as a column"); + } + + @Test + void neverExportsNextSequenceValue() { + SequentialIdentifierGenerator generator = new SequentialIdentifierGenerator(); + generator.setFirstIdentifierBase("1000"); + generator.setBaseCharacterSet("0123456789"); + generator.setNextSequenceValue(4711L); + + ExportLine line = new ExportLine(); + new SequentialIdentifierGeneratorLineExporter().writeLine(generator, line); + + for (String header : line.getHeaders()) { + assertNotEquals("4711", line.get(header), "next sequence value must not leak into column " + header); + } + } + + @Test + void skipsNonSequentialSources() { + RemoteIdentifierSource remote = new RemoteIdentifierSource(); + remote.setUrl("https://idgen.example.org/generate"); + + ExportLine line = new ExportLine(); + new SequentialIdentifierGeneratorLineExporter().writeLine(remote, line); + + assertTrue(line.getHeaders().isEmpty(), "non-sequential sources contribute no columns"); + } + + @Test + void retiredGeneratorStillExportsItsColumns() { + SequentialIdentifierGenerator generator = new SequentialIdentifierGenerator(); + generator.setFirstIdentifierBase("1000"); + generator.setBaseCharacterSet("0123456789"); + generator.setRetired(true); + + ExportLine line = new ExportLine(); + new SequentialIdentifierGeneratorLineExporter().writeLine(generator, line); + + assertEquals("1000", line.get("first identifier base"), + "Iniz requires this column even when it bootstraps a retired row"); + assertEquals("0123456789", line.get("base character set")); + } +} diff --git a/api/src/test/java/org/openmrs/module/metadataexport/export/CsvDomainExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/export/CsvDomainExporterTest.java index e396520..7a288bc 100644 --- a/api/src/test/java/org/openmrs/module/metadataexport/export/CsvDomainExporterTest.java +++ b/api/src/test/java/org/openmrs/module/metadataexport/export/CsvDomainExporterTest.java @@ -20,10 +20,12 @@ import java.io.File; import java.io.FileReader; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -89,6 +91,23 @@ public Collection getDependencies(Concept instance) { } } + private static class PartitionedDomainExporter extends TestDomainExporter { + + @Override + protected Map> partition(Collection instances) { + Map> files = new LinkedHashMap<>(); + for (Concept concept : instances) { + files.computeIfAbsent(concept.getUuid() + ".csv", f -> new ArrayList<>()).add(concept); + } + return files; + } + + @Override + protected Integer order(String fileName) { + return "c2.csv".equals(fileName) ? 2000 : null; + } + } + private static Concept concept(String uuid) { Concept c = new Concept(); c.setUuid(uuid); @@ -128,6 +147,20 @@ void export_mergesChainIntoUnionHeaderAndAlignsRows(@TempDir File outDir) throws } } + @Test + void export_threadsPerFileOrderIntoEachCsvHeader(@TempDir File outDir) throws Exception { + new PartitionedDomainExporter().export(Arrays.asList(concept("c1"), concept("c2")), new ExportContext(outDir)); + + try (CSVReader reader = new CSVReader( + new FileReader(outDir.toPath().resolve(Paths.get("configuration", DOMAIN_DIR, "c1.csv")).toFile()))) { + assertArrayEquals(new String[] { "uuid", "name", "_version:1" }, reader.readNext()); + } + try (CSVReader reader = new CSVReader( + new FileReader(outDir.toPath().resolve(Paths.get("configuration", DOMAIN_DIR, "c2.csv")).toFile()))) { + assertArrayEquals(new String[] { "uuid", "name", "flavor", "_version:1", "_order:2000" }, reader.readNext()); + } + } + private static Map headerIndex(String[] header) { Map index = new HashMap<>(); for (int i = 0; i < header.length; i++) { diff --git a/api/src/test/java/org/openmrs/module/metadataexport/export/CsvExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/export/CsvExporterTest.java index 0b6bc31..9ec5889 100644 --- a/api/src/test/java/org/openmrs/module/metadataexport/export/CsvExporterTest.java +++ b/api/src/test/java/org/openmrs/module/metadataexport/export/CsvExporterTest.java @@ -68,4 +68,31 @@ void writeCsv_buildsUnionHeaderAndAlignsRows() throws Exception { assertEquals(3, rows.size()); } } + + @Test + void writeCsv_withOrderAppendsOrderHeaderAfterVersion() throws Exception { + CsvExporter exporter = new CsvExporter<>(Collections.singletonList(VARYING_COLUMNS), Domain.CONCEPTS); + + exporter.writeCsv(Collections.singletonList(concept("c1")), outDir, "ordered.csv", 3000); + + File csv = new File(new File(outDir, "configuration"), Domain.CONCEPTS.getName() + "/ordered.csv"); + try (CSVReader reader = new CSVReader(new FileReader(csv))) { + List rows = reader.readAll(); + assertArrayEquals(new String[] { "colA", "_version:1", "_order:3000" }, rows.get(0)); + assertArrayEquals(new String[] { "A-c1", "", "" }, rows.get(1)); + } + } + + @Test + void writeCsv_withNullOrderLeavesHeaderUnchanged() throws Exception { + CsvExporter exporter = new CsvExporter<>(Collections.singletonList(VARYING_COLUMNS), Domain.CONCEPTS); + + exporter.writeCsv(Collections.singletonList(concept("c1")), outDir, "unordered.csv", null); + + File csv = new File(new File(outDir, "configuration"), Domain.CONCEPTS.getName() + "/unordered.csv"); + try (CSVReader reader = new CSVReader(new FileReader(csv))) { + List rows = reader.readAll(); + assertArrayEquals(new String[] { "colA", "_version:1" }, rows.get(0)); + } + } } diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index e31780a..e42e7ae 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -36,6 +36,7 @@ org.openmrs.module.legacyui org.openmrs.module.emrapi + org.openmrs.module.idgen org.openmrs.module.metadatamapping org.openmrs.module.metadatasharing diff --git a/pom.xml b/pom.xml index b91b7a9..d93e3b3 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ 3.0.10 0.8.12 3.4.0 + 4.6.0 1.6.0 1.2.2 @@ -115,6 +116,13 @@ provided + + org.openmrs.module + idgen-api + ${idgenVersion} + provided + + org.openmrs.module metadatamapping-api From 16fe8098045d248d9adf1bd19d55a6dd50296617 Mon Sep 17 00:00:00 2001 From: Wikum Weerakutti Date: Wed, 12 Aug 2026 21:48:18 +0530 Subject: [PATCH 2/2] Skip identifier pools without an importable backing source --- README.md | 5 ++- .../idgen/IdentifierPoolLineExporter.java | 6 +-- .../idgen/IdentifierSourceDomainExporter.java | 41 +++++++++++++++---- .../IdentifierSourceDomainExporterTest.java | 18 ++++++++ 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a64caaf..37de173 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,9 @@ Currently supported domains: must define the `idgen.remote.password.` system or OpenMRS runtime property (retired remote sources included — Initializer still requires the password when it bootstraps them); custom identifier source types from other modules have no Initializer - representation and are skipped with a warning, as are auto generation options pointing at them; - requires the idgen module (4.6+) + representation and are skipped with a warning, as are pools backed by them, pools with no + backing source at all, and auto generation options pointing at them; requires the idgen + module (4.6+) * Auto generation options (identifier type, location, identifier source, manual entry enabled, auto generation enabled) — the referenced identifier type, source and location are pulled in via cross-domain closure; requires the idgen module (4.6+) diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java index 3545a90..fa3412b 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierPoolLineExporter.java @@ -9,7 +9,6 @@ */ package org.openmrs.module.metadataexport.domain.idgen; -import lombok.extern.slf4j.Slf4j; import org.openmrs.api.db.hibernate.HibernateUtil; import org.openmrs.module.idgen.IdentifierPool; import org.openmrs.module.idgen.IdentifierSource; @@ -21,7 +20,6 @@ * data and are not exported. The boolean columns are always emitted — an absent cell becomes a null * that NPEs when Iniz assigns it into idgen's primitive-backed fields. */ -@Slf4j public class IdentifierPoolLineExporter extends BaseLineExporter { @Override @@ -32,9 +30,7 @@ public void export(IdentifierSource source, ExportLine line) { } IdentifierPool pool = (IdentifierPool) source; - if (pool.getSource() == null) { - log.warn("Idgen: identifier pool {} has no backing source; Iniz requires one on import", pool.getUuid()); - } else { + if (pool.getSource() != null) { line.put(IdentifierSourceLineExporter.HEADER_POOL_IDENTIFIER_SOURCE, pool.getSource().getUuid()); } line.put(IdentifierSourceLineExporter.HEADER_POOL_BATCH_SIZE, String.valueOf(pool.getBatchSize())); diff --git a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java index a271508..72605bf 100644 --- a/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java +++ b/api/src/main/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporter.java @@ -64,20 +64,47 @@ protected Map> partition(Collection> files = new LinkedHashMap<>(); for (IdentifierSource instance : instances) { IdentifierSource real = HibernateUtil.getRealObjectFromProxy(instance); + if (!exportable(real)) { + continue; + } if (real instanceof IdentifierPool) { files.computeIfAbsent(FILE_POOL, f -> new ArrayList<>()).add(instance); } else if (real instanceof SequentialIdentifierGenerator) { files.computeIfAbsent(FILE_SEQUENTIAL, f -> new ArrayList<>()).add(instance); - } else if (real instanceof RemoteIdentifierSource) { - files.computeIfAbsent(FILE_REMOTE, f -> new ArrayList<>()).add(instance); } else { - log.warn("Idgen: skipping identifier source {} of unsupported type {}", real.getUuid(), - real.getClass().getName()); + files.computeIfAbsent(FILE_REMOTE, f -> new ArrayList<>()).add(instance); } } return files; } + /** + * A source Iniz can import. Pools need a backing source that is itself exported: Iniz reads + * {@code pool identifier source} as required and resolves it by uuid, so a pool without one (legal + * in idgen's schema) or backed by a skipped custom type fails on import. + */ + private boolean exportable(IdentifierSource real) { + if (!handles(real)) { + log.warn("Idgen: skipping identifier source {} of unsupported type {} — no Iniz representation", real.getUuid(), + real.getClass().getName()); + return false; + } + if (real instanceof IdentifierPool) { + IdentifierSource backing = HibernateUtil.getRealObjectFromProxy(((IdentifierPool) real).getSource()); + if (backing == null) { + log.warn("Idgen: skipping identifier pool {} with no backing source; Iniz requires one on import", + real.getUuid()); + return false; + } + if (!handles(backing)) { + log.warn("Idgen: skipping identifier pool {} — its backing source {} has unsupported type {}", + real.getUuid(), backing.getUuid(), backing.getClass().getName()); + return false; + } + } + return true; + } + @Override protected Integer order(String fileName) { // pools must load after the sources they reference @@ -108,12 +135,8 @@ public boolean handles(OpenmrsObject instance) { public Collection getAllInstances() { List sources = new ArrayList<>(); for (IdentifierSource source : Context.getService(IdentifierSourceService.class).getAllIdentifierSources(true)) { - IdentifierSource real = HibernateUtil.getRealObjectFromProxy(source); - if (handles(real)) { + if (exportable(HibernateUtil.getRealObjectFromProxy(source))) { sources.add(source); - } else { - log.warn("Idgen: skipping identifier source {} of unsupported type {} — no Iniz representation", - real.getUuid(), real.getClass().getName()); } } return sources; diff --git a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java index a2184bb..78f5b2f 100644 --- a/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java +++ b/api/src/test/java/org/openmrs/module/metadataexport/domain/idgen/IdentifierSourceDomainExporterTest.java @@ -45,6 +45,7 @@ void partitionsSourcesByTypeIncludingRetiredOnes() { retiredSequential.setRetired(true); RemoteIdentifierSource remote = new RemoteIdentifierSource(); IdentifierPool pool = new IdentifierPool(); + pool.setSource(sequential); Map> files = exporter .partition(Arrays.asList(sequential, remote, pool, retiredSequential)); @@ -70,6 +71,7 @@ void partitionOmitsFilesForAbsentTypes() { void partitionSkipsUnknownSourceSubclasses() { IdentifierSource custom = new BaseIdentifierSource() {}; IdentifierPool pool = new IdentifierPool(); + pool.setSource(new SequentialIdentifierGenerator()); Map> files = exporter.partition(Arrays.asList(custom, pool)); @@ -77,6 +79,22 @@ void partitionSkipsUnknownSourceSubclasses() { assertTrue(files.get(IdentifierSourceDomainExporter.FILE_POOL).contains(pool)); } + @Test + void partitionSkipsPoolsWithoutAnImportableBackingSource() { + IdentifierPool sourceless = new IdentifierPool(); + IdentifierPool customBacked = new IdentifierPool(); + customBacked.setSource(new BaseIdentifierSource() {}); + IdentifierPool good = new IdentifierPool(); + good.setSource(new SequentialIdentifierGenerator()); + + Map> files = exporter.partition(Arrays.asList(sourceless, customBacked, good)); + + assertEquals(1, files.size()); + assertEquals(1, files.get(IdentifierSourceDomainExporter.FILE_POOL).size(), + "a pool row without a resolvable backing source uuid can never import"); + assertTrue(files.get(IdentifierSourceDomainExporter.FILE_POOL).contains(good)); + } + @Test void poolFileIsOrderedAfterTheSourceFiles() { Integer sequential = exporter.order(IdentifierSourceDomainExporter.FILE_SEQUENTIAL);