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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,25 @@ 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.<identifier source uuid>` placeholder, and the importing server
must define the `idgen.remote.password.<identifier source uuid>` 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 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+)

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)
Expand Down Expand Up @@ -215,7 +229,9 @@ Exporters that only contribute extra columns to an existing row (not the primary
`BaseLineExporter<T>` 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<T>` instead of `CsvDomainExporter<T>`. Build the DOM in
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AutoGenerationOption> {

@Override
protected List<BaseLineExporter<AutoGenerationOption>> 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<AutoGenerationOption> getAllInstances() {
IdentifierSourceService service = Context.getService(IdentifierSourceService.class);
List<AutoGenerationOption> options = new ArrayList<>();
for (PatientIdentifierType type : Context.getPatientService().getAllPatientIdentifierTypes(true)) {
List<AutoGenerationOption> 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<AutoGenerationOption> exportable(List<AutoGenerationOption> options) {
List<AutoGenerationOption> result = new ArrayList<>();
for (AutoGenerationOption option : options) {
if (BooleanUtils.isTrue(option.getRetired())) {
continue;
}
Comment on lines +78 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoGenerationOption has no retired column. idgen maps only id, uuid, identifier_type, location, source, manual_entry_enabled and automatic_generation_enabled (IdentifierSource.hbm.xml, and the liquibase changesets add nothing else), so a persisted option always reports the in-memory BaseOpenmrsMetadata default and this branch never fires. exportableFiltersRetiredOptions passes only because it retires an option that never came from the database.

Small thing, but together with the "retired options are filtered out by the domain exporter instead" note in AutoGenerationOptionLineExporter it reads as though retirement were supported for this domain, which AutoGenerationOptionsCsvParser.setRetired explicitly refuses on the import side.

IdentifierSource source = HibernateUtil.getRealObjectFromProxy(option.getSource());
if (source != null && !(source instanceof SequentialIdentifierGenerator)
&& !(source instanceof RemoteIdentifierSource) && !(source instanceof IdentifierPool)) {
Comment on lines +82 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that IdentifierSourceDomainExporter.exportable() drops pools as well as custom types, this check no longer matches what the idgen domain will actually write. A pool with no backing source (the case the new guard was added for) is skipped there, but source instanceof IdentifierPool still passes here, so the option is exported.

If merged as-is, a server with an auto generation option on such a pool gets an autogenerationoptions.csv row whose Identifier Source uuid appears in none of the exported idgen files. On import, AutoGenerationOptionLineProcessor.fill resolves it through getIdentifierSourceByUuid, gets null, and calls setSource(null); source is not-null="true" in idgen's IdentifierSource.hbm.xml, so the save fails. With Iniz's default non-throwing mode the error is logged and BaseFileLoader still writes the file checksum, so the option is quietly missing on the target and a restart won't retry it.

Asking the idgen exporter whether it will really export the source, instead of repeating a type test here, would stop the two sides drifting again. It would also make the README's "auto generation options pointing at them" line true for the pool cases it now lists.

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<? extends OpenmrsObject> getDependencies(AutoGenerationOption instance) {
List<OpenmrsObject> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<AutoGenerationOption> {

@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()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.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.
*/
public class IdentifierPoolLineExporter extends BaseLineExporter<IdentifierSource> {

@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) {
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()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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<IdentifierSource> {

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<BaseLineExporter<IdentifierSource>> 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<String, Collection<IdentifierSource>> partition(Collection<IdentifierSource> instances) {
Map<String, Collection<IdentifierSource>> files = new LinkedHashMap<>();
for (IdentifierSource instance : instances) {
IdentifierSource real = HibernateUtil.getRealObjectFromProxy(instance);
if (!exportable(real)) {
continue;
}
Comment on lines +67 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partition() runs after selection, so a pool dropped here has already been through Selector and is sitting in the ExportManifest. BuildManifest.of turns that manifest into the resolvedItems of the package.json at the zip root, so a REST-triggered build lists the pool as exported while idgen_pool.csv has no row for it.

Nothing fails, so I would not hold the PR for it, but this is the invariant handlesOnlyTheSourceTypesInizCanRepresent spells out ("handles() must agree with partition(), or selection puts sources in the manifest that export drops"), and that test still passes because it only covers the type rule. Moving the whole exportable() check into handles() would have Selector skip the pool outright and keep the manifest honest.

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 {
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
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<IdentifierSource> getAllInstances() {
List<IdentifierSource> sources = new ArrayList<>();
for (IdentifierSource source : Context.getService(IdentifierSourceService.class).getAllIdentifierSources(true)) {
if (exportable(HibernateUtil.getRealObjectFromProxy(source))) {
sources.add(source);
}
}
return sources;
}

@Override
public Collection<? extends OpenmrsObject> getDependencies(IdentifierSource instance) {
List<OpenmrsObject> 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;
}
}
Loading