Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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 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 +228,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,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<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) {
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());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This branch logs that Iniz will reject the row, then writes the row anyway, and I think it has to skip the pool instead. A pool exported without a pool identifier source cell fails on import: IdentifierPoolLineProcessor reads that column with line.get(header, true) and throws, either "No identifier source found with uuid null", or an unknown-column error when no pool in the file carries a source. In Iniz's default non-throwing mode BaseFileLoader.loadUnsafe logs that error and still writes the file checksum, so the pool is simply missing on the target and a restart won't retry it; with doThrow on, the idgen load aborts. idgen_id_pool.source is nullable in idgen's schema, so this is reachable.

The same gap sits one level out in IdentifierSourceDomainExporter: a pool whose backing source is one of the custom subclasses getAllInstances() skips is still exported, and the uuid written on line 38 then names a source that is in none of the exported files, which fails on import the same way.

AutoGenerationOptionDomainExporter.exportable() already applies this guard on the other side, dropping options whose source is a type we skip (the test there calls it a dangling reference). I'd mirror it for pools, skipping with a warning when the backing source is null or isn't a type handles() accepts, so the export loses one pool rather than producing a pool file that can't be imported. Leaving the column out isn't enough on its own, since Iniz reads it with get(header, true).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done!

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,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<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 (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<IdentifierSource> getAllInstances() {
List<IdentifierSource> 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<? 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